@sip-protocol/sdk 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/{TransportWebUSB-YQMAGJAJ.mjs → TransportWebUSB-2KITI5HD.mjs} +24 -12
- package/dist/browser.d.mts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +1358 -847
- package/dist/browser.mjs +13 -3
- package/dist/{chunk-64AYA5F5.mjs → chunk-G3TBBG2K.mjs} +221 -146
- package/dist/{chunk-4GRJ5MAW.mjs → chunk-KXETSSKP.mjs} +4 -0
- package/dist/{chunk-YWGJ77A2.mjs → chunk-PT2DNA7E.mjs} +335 -310
- package/dist/{constants-LHAAUC2T.mjs → constants-DCJYTIU3.mjs} +5 -1
- package/dist/{dist-2OGQ7FED.mjs → dist-PYEXZNFD.mjs} +609 -221
- package/dist/{index-DeE1ZzA4.d.mts → index-B1d8pihL.d.mts} +117 -33
- package/dist/{index-DXh2IGkz.d.ts → index-UQhQJZbM.d.ts} +117 -33
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1348 -837
- package/dist/index.mjs +13 -3
- package/dist/{interface-Bf7w1PLW.d.mts → interface-CQi0-WfS.d.mts} +2 -2
- package/dist/{interface-Bf7w1PLW.d.ts → interface-CQi0-WfS.d.ts} +2 -2
- package/dist/{noir-kzbLVTei.d.mts → noir-CwPIyBLj.d.mts} +1 -1
- package/dist/{noir-kzbLVTei.d.ts → noir-CwPIyBLj.d.ts} +1 -1
- package/dist/proofs/halo2.d.mts +1 -1
- package/dist/proofs/halo2.d.ts +1 -1
- package/dist/proofs/kimchi.d.mts +1 -1
- package/dist/proofs/kimchi.d.ts +1 -1
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/{solana-U3MEGU7W.mjs → solana-ZWNIQTSU.mjs} +6 -6
- package/package.json +32 -32
- package/src/adapters/gelato-relay.ts +386 -0
- package/src/adapters/index.ts +28 -0
- package/src/adapters/oneinch.ts +126 -0
- package/src/chains/ethereum/constants.ts +33 -1
- package/src/chains/ethereum/index.ts +2 -1
- package/src/chains/ethereum/privacy-adapter.ts +44 -26
- package/src/chains/ethereum/stealth.ts +84 -30
- package/src/chains/ethereum/types.ts +4 -0
- package/src/chains/near/privacy-adapter.ts +8 -5
- package/src/chains/near/resolver.ts +22 -8
- package/src/chains/near/stealth.ts +9 -9
- package/src/chains/solana/constants.ts +13 -1
- package/src/chains/solana/ephemeral-keys.ts +3 -257
- package/src/chains/solana/index.ts +2 -3
- package/src/chains/solana/providers/helius-enhanced.ts +6 -6
- package/src/chains/solana/providers/webhook.ts +2 -2
- package/src/chains/solana/scan.ts +9 -8
- package/src/chains/solana/stealth-scanner.ts +3 -3
- package/src/chains/solana/types.ts +18 -4
- package/src/cosmos/ibc-stealth.ts +6 -6
- package/src/index.ts +6 -0
- package/src/move/aptos.ts +15 -9
- package/src/move/sui.ts +15 -9
- package/src/nft/private-nft.ts +10 -6
- package/src/privacy-backends/shadowwire.ts +13 -0
- package/src/stealth/ed25519.ts +173 -12
- package/src/stealth/index.ts +47 -4
- package/src/stealth/secp256k1.ts +144 -7
- package/src/stealth.ts +7 -0
- package/src/wallet/ethereum/privacy-adapter.ts +1 -1
- package/src/wallet/hardware/ledger-privacy.ts +2 -2
- package/src/wallet/near/adapter.ts +2 -2
- package/src/wallet/near/meteor-wallet.ts +2 -2
- package/src/wallet/near/my-near-wallet.ts +2 -2
- package/src/wallet/near/wallet-selector.ts +2 -2
- package/src/wallet/solana/privacy-adapter.ts +9 -9
|
@@ -42,41 +42,6 @@ export interface EphemeralKeypair {
|
|
|
42
42
|
publicKeyBase58: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Result of using an ephemeral keypair for stealth address generation
|
|
47
|
-
*/
|
|
48
|
-
export interface EphemeralKeyUsageResult {
|
|
49
|
-
/**
|
|
50
|
-
* Shared secret derived from ECDH
|
|
51
|
-
*/
|
|
52
|
-
sharedSecret: HexString
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* View tag (first byte of shared secret hash)
|
|
56
|
-
*/
|
|
57
|
-
viewTag: number
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Stealth address (hex, ed25519 format)
|
|
61
|
-
*/
|
|
62
|
-
stealthAddress: HexString
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Stealth address in Solana base58 format
|
|
66
|
-
*/
|
|
67
|
-
stealthAddressBase58: string
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Ephemeral public key used (for announcement)
|
|
71
|
-
*/
|
|
72
|
-
ephemeralPublicKey: HexString
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Ephemeral public key in Solana base58 format
|
|
76
|
-
*/
|
|
77
|
-
ephemeralPublicKeyBase58: string
|
|
78
|
-
}
|
|
79
|
-
|
|
80
45
|
/**
|
|
81
46
|
* Managed ephemeral keypair with automatic secure disposal
|
|
82
47
|
*/
|
|
@@ -91,15 +56,6 @@ export interface ManagedEphemeralKeypair extends EphemeralKeypair {
|
|
|
91
56
|
* Called automatically after use, but can be called manually
|
|
92
57
|
*/
|
|
93
58
|
dispose(): void
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Use this keypair to generate a stealth address
|
|
97
|
-
* Automatically disposes the keypair after use
|
|
98
|
-
*/
|
|
99
|
-
useForStealthAddress(
|
|
100
|
-
recipientSpendingKey: HexString,
|
|
101
|
-
recipientViewingKey: HexString
|
|
102
|
-
): EphemeralKeyUsageResult
|
|
103
59
|
}
|
|
104
60
|
|
|
105
61
|
/**
|
|
@@ -162,13 +118,9 @@ export function generateEphemeralKeypair(): EphemeralKeypair {
|
|
|
162
118
|
* ```typescript
|
|
163
119
|
* const managed = generateManagedEphemeralKeypair()
|
|
164
120
|
*
|
|
165
|
-
* // Use
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* recipientViewingKey
|
|
169
|
-
* )
|
|
170
|
-
*
|
|
171
|
-
* console.log('Stealth address:', result.stealthAddressBase58)
|
|
121
|
+
* // Use the ephemeral key, then securely dispose
|
|
122
|
+
* console.log('Public key:', managed.publicKeyBase58)
|
|
123
|
+
* managed.dispose()
|
|
172
124
|
* console.log('Is disposed:', managed.isDisposed) // true
|
|
173
125
|
* ```
|
|
174
126
|
*/
|
|
@@ -203,33 +155,6 @@ export function generateManagedEphemeralKeypair(): ManagedEphemeralKeypair {
|
|
|
203
155
|
disposed = true
|
|
204
156
|
}
|
|
205
157
|
},
|
|
206
|
-
|
|
207
|
-
useForStealthAddress(
|
|
208
|
-
recipientSpendingKey: HexString,
|
|
209
|
-
recipientViewingKey: HexString
|
|
210
|
-
): EphemeralKeyUsageResult {
|
|
211
|
-
if (disposed) {
|
|
212
|
-
throw new Error('Ephemeral keypair has been disposed')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const result = computeStealthAddress(
|
|
217
|
-
privateKeyBytes,
|
|
218
|
-
publicKeyBytes,
|
|
219
|
-
recipientSpendingKey,
|
|
220
|
-
recipientViewingKey
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
...result,
|
|
225
|
-
ephemeralPublicKey: publicKeyHex,
|
|
226
|
-
ephemeralPublicKeyBase58: publicKeyBase58,
|
|
227
|
-
}
|
|
228
|
-
} finally {
|
|
229
|
-
// Always dispose after use
|
|
230
|
-
managed.dispose()
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
158
|
}
|
|
234
159
|
|
|
235
160
|
return managed
|
|
@@ -362,182 +287,3 @@ export function wipeEphemeralPrivateKey(privateKeyHex: HexString): void {
|
|
|
362
287
|
const bytes = hexToBytes(privateKeyHex.slice(2))
|
|
363
288
|
secureWipe(bytes)
|
|
364
289
|
}
|
|
365
|
-
|
|
366
|
-
// ─── Internal Helpers ─────────────────────────────────────────────────────────
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* ed25519 group order (L)
|
|
370
|
-
*/
|
|
371
|
-
const ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Convert bytes to bigint (little-endian for ed25519)
|
|
375
|
-
*/
|
|
376
|
-
function bytesToBigIntLE(bytes: Uint8Array): bigint {
|
|
377
|
-
let result = 0n
|
|
378
|
-
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
379
|
-
result = (result << 8n) | BigInt(bytes[i])
|
|
380
|
-
}
|
|
381
|
-
return result
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Get ed25519 scalar from private key bytes
|
|
386
|
-
* Follows standard ed25519 scalar clamping
|
|
387
|
-
*/
|
|
388
|
-
function getEd25519Scalar(privateKey: Uint8Array): bigint {
|
|
389
|
-
const hash = sha256(privateKey)
|
|
390
|
-
// Clamp to valid scalar
|
|
391
|
-
hash[0] &= 248
|
|
392
|
-
hash[31] &= 127
|
|
393
|
-
hash[31] |= 64
|
|
394
|
-
return bytesToBigIntLE(hash.slice(0, 32))
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Compute stealth address from ephemeral keypair and recipient keys
|
|
399
|
-
*/
|
|
400
|
-
function computeStealthAddress(
|
|
401
|
-
ephemeralPrivateBytes: Uint8Array,
|
|
402
|
-
_ephemeralPublicBytes: Uint8Array, // Reserved for future validation
|
|
403
|
-
recipientSpendingKey: HexString,
|
|
404
|
-
recipientViewingKey: HexString
|
|
405
|
-
): Omit<EphemeralKeyUsageResult, 'ephemeralPublicKey' | 'ephemeralPublicKeyBase58'> {
|
|
406
|
-
// Parse recipient keys
|
|
407
|
-
const spendingKeyBytes = hexToBytes(recipientSpendingKey.slice(2))
|
|
408
|
-
const viewingKeyBytes = hexToBytes(recipientViewingKey.slice(2))
|
|
409
|
-
|
|
410
|
-
// Get ephemeral scalar
|
|
411
|
-
const rawEphemeralScalar = getEd25519Scalar(ephemeralPrivateBytes)
|
|
412
|
-
const ephemeralScalar = rawEphemeralScalar % ED25519_ORDER
|
|
413
|
-
if (ephemeralScalar === 0n) {
|
|
414
|
-
throw new Error('Invalid ephemeral scalar')
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Compute shared secret: S = ephemeral_scalar * P_spend
|
|
418
|
-
const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes)
|
|
419
|
-
const sharedSecretPoint = spendingPoint.multiply(ephemeralScalar)
|
|
420
|
-
const sharedSecretBytes = sharedSecretPoint.toRawBytes()
|
|
421
|
-
|
|
422
|
-
// Hash the shared secret
|
|
423
|
-
const sharedSecretHash = sha256(sharedSecretBytes)
|
|
424
|
-
const viewTag = sharedSecretHash[0]
|
|
425
|
-
|
|
426
|
-
// Derive stealth address: P_stealth = P_view + hash(S)*G
|
|
427
|
-
const hashScalar = bytesToBigIntLE(sharedSecretHash) % ED25519_ORDER
|
|
428
|
-
if (hashScalar === 0n) {
|
|
429
|
-
throw new Error('Invalid hash scalar')
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar)
|
|
433
|
-
const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes)
|
|
434
|
-
const stealthPoint = viewingPoint.add(hashTimesG)
|
|
435
|
-
const stealthAddressBytes = stealthPoint.toRawBytes()
|
|
436
|
-
|
|
437
|
-
const stealthAddress = `0x${bytesToHex(stealthAddressBytes)}` as HexString
|
|
438
|
-
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress)
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
sharedSecret: `0x${bytesToHex(sharedSecretHash)}` as HexString,
|
|
442
|
-
viewTag,
|
|
443
|
-
stealthAddress,
|
|
444
|
-
stealthAddressBase58,
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ─── Announcement Format ──────────────────────────────────────────────────────
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Format ephemeral key data for Solana memo announcement
|
|
452
|
-
*
|
|
453
|
-
* @param ephemeralPublicKeyBase58 - Ephemeral public key in base58
|
|
454
|
-
* @param viewTag - View tag (0-255)
|
|
455
|
-
* @param stealthAddressBase58 - Optional stealth address for verification
|
|
456
|
-
* @returns Formatted announcement string
|
|
457
|
-
*
|
|
458
|
-
* @example
|
|
459
|
-
* ```typescript
|
|
460
|
-
* const memo = formatEphemeralAnnouncement(
|
|
461
|
-
* result.ephemeralPublicKeyBase58,
|
|
462
|
-
* result.viewTag,
|
|
463
|
-
* result.stealthAddressBase58
|
|
464
|
-
* )
|
|
465
|
-
* // "SIP:1:7xK9...:0a:8yL0..."
|
|
466
|
-
* ```
|
|
467
|
-
*/
|
|
468
|
-
export function formatEphemeralAnnouncement(
|
|
469
|
-
ephemeralPublicKeyBase58: string,
|
|
470
|
-
viewTag: number,
|
|
471
|
-
stealthAddressBase58?: string
|
|
472
|
-
): string {
|
|
473
|
-
const viewTagHex = viewTag.toString(16).padStart(2, '0')
|
|
474
|
-
const parts = ['SIP:1', ephemeralPublicKeyBase58, viewTagHex]
|
|
475
|
-
|
|
476
|
-
if (stealthAddressBase58) {
|
|
477
|
-
parts.push(stealthAddressBase58)
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return parts.join(':')
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Parse ephemeral key data from Solana memo announcement
|
|
485
|
-
*
|
|
486
|
-
* @param announcement - Announcement string from memo
|
|
487
|
-
* @returns Parsed ephemeral data or null if invalid
|
|
488
|
-
*
|
|
489
|
-
* @example
|
|
490
|
-
* ```typescript
|
|
491
|
-
* const parsed = parseEphemeralAnnouncement('SIP:1:7xK9...:0a:8yL0...')
|
|
492
|
-
* if (parsed) {
|
|
493
|
-
* console.log('Ephemeral key:', parsed.ephemeralPublicKeyBase58)
|
|
494
|
-
* console.log('View tag:', parsed.viewTag)
|
|
495
|
-
* }
|
|
496
|
-
* ```
|
|
497
|
-
*/
|
|
498
|
-
export function parseEphemeralAnnouncement(
|
|
499
|
-
announcement: string
|
|
500
|
-
): {
|
|
501
|
-
ephemeralPublicKeyBase58: string
|
|
502
|
-
viewTag: number
|
|
503
|
-
stealthAddressBase58?: string
|
|
504
|
-
} | null {
|
|
505
|
-
if (!announcement.startsWith('SIP:1:')) {
|
|
506
|
-
return null
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const parts = announcement.slice(6).split(':')
|
|
510
|
-
if (parts.length < 2) {
|
|
511
|
-
return null
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const ephemeralPublicKeyBase58 = parts[0]
|
|
515
|
-
const viewTagHex = parts[1]
|
|
516
|
-
const stealthAddressBase58 = parts[2]
|
|
517
|
-
|
|
518
|
-
// Validate ephemeral key (base58, 32-44 chars)
|
|
519
|
-
if (!ephemeralPublicKeyBase58 || ephemeralPublicKeyBase58.length < 32 || ephemeralPublicKeyBase58.length > 44) {
|
|
520
|
-
return null
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Validate view tag (1-2 hex chars)
|
|
524
|
-
if (!viewTagHex || viewTagHex.length > 2 || !/^[0-9a-fA-F]+$/.test(viewTagHex)) {
|
|
525
|
-
return null
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const viewTag = parseInt(viewTagHex, 16)
|
|
529
|
-
if (viewTag < 0 || viewTag > 255) {
|
|
530
|
-
return null
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Validate stealth address if present
|
|
534
|
-
if (stealthAddressBase58 && (stealthAddressBase58.length < 32 || stealthAddressBase58.length > 44)) {
|
|
535
|
-
return null
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return {
|
|
539
|
-
ephemeralPublicKeyBase58,
|
|
540
|
-
viewTag,
|
|
541
|
-
stealthAddressBase58,
|
|
542
|
-
}
|
|
543
|
-
}
|
|
@@ -52,6 +52,8 @@ export {
|
|
|
52
52
|
SOLANA_EXPLORER_URLS,
|
|
53
53
|
MEMO_PROGRAM_ID,
|
|
54
54
|
SIP_MEMO_PREFIX,
|
|
55
|
+
SIP_MEMO_PREFIX_V2,
|
|
56
|
+
SIP_MEMO_PREFIX_ANY,
|
|
55
57
|
ESTIMATED_TX_FEE_LAMPORTS,
|
|
56
58
|
ATA_RENT_LAMPORTS,
|
|
57
59
|
getExplorerUrl,
|
|
@@ -150,10 +152,7 @@ export {
|
|
|
150
152
|
batchGenerateManagedEphemeralKeypairs,
|
|
151
153
|
disposeEphemeralKeypairs,
|
|
152
154
|
wipeEphemeralPrivateKey,
|
|
153
|
-
formatEphemeralAnnouncement,
|
|
154
|
-
parseEphemeralAnnouncement,
|
|
155
155
|
type EphemeralKeypair,
|
|
156
|
-
type EphemeralKeyUsageResult,
|
|
157
156
|
type ManagedEphemeralKeypair,
|
|
158
157
|
type BatchGenerationOptions,
|
|
159
158
|
} from './ephemeral-keys'
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
SOLANA_ADDRESS_MAX_LENGTH,
|
|
39
39
|
HELIUS_API_KEY_MIN_LENGTH,
|
|
40
40
|
sanitizeUrl,
|
|
41
|
-
|
|
41
|
+
SIP_MEMO_PREFIX_ANY,
|
|
42
42
|
getExplorerUrl,
|
|
43
43
|
} from '../constants'
|
|
44
44
|
import type {
|
|
@@ -324,7 +324,7 @@ export class HeliusEnhanced {
|
|
|
324
324
|
* Extract SIP metadata from a transaction
|
|
325
325
|
*
|
|
326
326
|
* Parses memo program instructions to find SIP announcements.
|
|
327
|
-
* SIP memo format: SIP
|
|
327
|
+
* SIP memo format: SIP:<version>:<ephemeral_pubkey_base58>:<view_tag_hex>
|
|
328
328
|
*
|
|
329
329
|
* @param tx - Enhanced transaction
|
|
330
330
|
* @returns SIP metadata if found
|
|
@@ -339,13 +339,13 @@ export class HeliusEnhanced {
|
|
|
339
339
|
const description = tx.description || ''
|
|
340
340
|
|
|
341
341
|
// Check if this looks like a SIP transaction
|
|
342
|
-
// SIP transactions have a memo with format: SIP
|
|
343
|
-
if (description.includes(
|
|
342
|
+
// SIP transactions have a memo with format: SIP:<version>:<ephemeral_pubkey>:<view_tag>
|
|
343
|
+
if (description.includes(SIP_MEMO_PREFIX_ANY)) {
|
|
344
344
|
metadata.isSIPTransaction = true
|
|
345
345
|
|
|
346
346
|
// Try to extract SIP memo data
|
|
347
|
-
// The memo format is: SIP
|
|
348
|
-
const sipMemoMatch = description.match(/SIP:
|
|
347
|
+
// The memo format is: SIP:<version>:<ephemeral_pubkey_base58>:<view_tag_hex>
|
|
348
|
+
const sipMemoMatch = description.match(/SIP:[12]:([A-Za-z0-9]{32,44}):([0-9a-fA-F]{1,2})/)
|
|
349
349
|
if (sipMemoMatch) {
|
|
350
350
|
metadata.ephemeralPubKey = sipMemoMatch[1]
|
|
351
351
|
metadata.viewTag = parseInt(sipMemoMatch[2], 16)
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
import type { StealthAddress } from '@sip-protocol/types'
|
|
47
47
|
import { parseAnnouncement } from '../types'
|
|
48
48
|
import type { SolanaScanResult } from '../types'
|
|
49
|
-
import {
|
|
49
|
+
import { SIP_MEMO_PREFIX_ANY } from '../constants'
|
|
50
50
|
import { getTokenSymbol, parseTokenTransferFromBalances } from '../utils'
|
|
51
51
|
import { ValidationError, SecurityError } from '../../../errors'
|
|
52
52
|
import { hmac } from '@noble/hashes/hmac'
|
|
@@ -594,7 +594,7 @@ async function processRawTransaction(
|
|
|
594
594
|
|
|
595
595
|
// Search log messages for SIP announcement
|
|
596
596
|
for (const log of tx.meta.logMessages) {
|
|
597
|
-
if (!log.includes(
|
|
597
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
598
598
|
|
|
599
599
|
// Extract memo content from log
|
|
600
600
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
checkEd25519StealthAddress,
|
|
19
19
|
deriveEd25519StealthPrivateKey,
|
|
20
|
+
deriveEd25519StealthPrivateKeyV1,
|
|
20
21
|
solanaAddressToEd25519PublicKey,
|
|
21
22
|
} from '../../stealth'
|
|
22
23
|
import type { StealthAddress, HexString } from '@sip-protocol/types'
|
|
@@ -28,7 +29,7 @@ import type {
|
|
|
28
29
|
} from './types'
|
|
29
30
|
import { parseAnnouncement } from './types'
|
|
30
31
|
import {
|
|
31
|
-
|
|
32
|
+
SIP_MEMO_PREFIX_ANY,
|
|
32
33
|
MEMO_PROGRAM_ID,
|
|
33
34
|
getExplorerUrl,
|
|
34
35
|
DEFAULT_SCAN_LIMIT,
|
|
@@ -111,7 +112,7 @@ export async function scanForPayments(
|
|
|
111
112
|
|
|
112
113
|
// Look for SIP announcement in logs
|
|
113
114
|
for (const log of tx.meta.logMessages) {
|
|
114
|
-
if (!log.includes(
|
|
115
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
115
116
|
|
|
116
117
|
// Extract memo content from log
|
|
117
118
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
@@ -261,6 +262,7 @@ export async function claimStealthPayment(
|
|
|
261
262
|
spendingPrivateKey,
|
|
262
263
|
destinationAddress,
|
|
263
264
|
mint,
|
|
265
|
+
version = '2',
|
|
264
266
|
} = params
|
|
265
267
|
|
|
266
268
|
// M7 FIX: Check SOL balance for fees before attempting claim
|
|
@@ -285,12 +287,11 @@ export async function claimStealthPayment(
|
|
|
285
287
|
viewTag: 0, // Not needed for derivation
|
|
286
288
|
}
|
|
287
289
|
|
|
288
|
-
// Derive stealth private key
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
spendingPrivateKey,
|
|
292
|
-
viewingPrivateKey
|
|
293
|
-
)
|
|
290
|
+
// Derive stealth private key — route by announcement version:
|
|
291
|
+
// legacy SIP:1 (swapped scheme) vs canonical SIP:2 (EIP-5564).
|
|
292
|
+
const recovery = version === '1'
|
|
293
|
+
? deriveEd25519StealthPrivateKeyV1(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
|
|
294
|
+
: deriveEd25519StealthPrivateKey(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
|
|
294
295
|
|
|
295
296
|
// Create Solana keypair from derived private key
|
|
296
297
|
// Note: ed25519 private keys in Solana are seeds, not raw scalars
|
|
@@ -17,7 +17,7 @@ import type { StealthAddress, HexString } from '@sip-protocol/types'
|
|
|
17
17
|
import { checkEd25519StealthAddress, solanaAddressToEd25519PublicKey } from '../../stealth'
|
|
18
18
|
import { parseAnnouncement, type SolanaAnnouncement } from './types'
|
|
19
19
|
import {
|
|
20
|
-
|
|
20
|
+
SIP_MEMO_PREFIX_ANY,
|
|
21
21
|
MEMO_PROGRAM_ID,
|
|
22
22
|
DEFAULT_SCAN_LIMIT,
|
|
23
23
|
VIEW_TAG_MAX,
|
|
@@ -354,7 +354,7 @@ export class StealthScanner {
|
|
|
354
354
|
|
|
355
355
|
// Look for SIP announcements in logs
|
|
356
356
|
for (const log of tx.meta.logMessages) {
|
|
357
|
-
if (!log.includes(
|
|
357
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
358
358
|
|
|
359
359
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
360
360
|
if (!memoMatch) continue
|
|
@@ -421,7 +421,7 @@ export class StealthScanner {
|
|
|
421
421
|
try {
|
|
422
422
|
// Look for SIP announcements
|
|
423
423
|
for (const log of logs.logs) {
|
|
424
|
-
if (!log.includes(
|
|
424
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
425
425
|
|
|
426
426
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
427
427
|
if (!memoMatch) continue
|
|
@@ -147,6 +147,12 @@ export interface SolanaClaimParams {
|
|
|
147
147
|
destinationAddress: string
|
|
148
148
|
/** SPL token mint address */
|
|
149
149
|
mint: PublicKey
|
|
150
|
+
/**
|
|
151
|
+
* Announcement scheme version of the payment being claimed.
|
|
152
|
+
* '2' (default) = canonical EIP-5564; '1' = legacy swapped (SIP:1) back-compat.
|
|
153
|
+
* Pass the `version` returned by {@link parseAnnouncement}.
|
|
154
|
+
*/
|
|
155
|
+
version?: '1' | '2'
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
/**
|
|
@@ -167,6 +173,8 @@ export interface SolanaClaimResult {
|
|
|
167
173
|
* Announcement data stored in transaction memo
|
|
168
174
|
*/
|
|
169
175
|
export interface SolanaAnnouncement {
|
|
176
|
+
/** Announcement scheme version: '1' = legacy swapped, '2' = canonical EIP-5564 */
|
|
177
|
+
version: string
|
|
170
178
|
/** Ephemeral public key (base58) */
|
|
171
179
|
ephemeralPublicKey: string
|
|
172
180
|
/** View tag for efficient scanning (hex, 1 byte) */
|
|
@@ -177,16 +185,21 @@ export interface SolanaAnnouncement {
|
|
|
177
185
|
|
|
178
186
|
/**
|
|
179
187
|
* Parse announcement from memo string
|
|
180
|
-
* Format: SIP
|
|
188
|
+
* Format: SIP:<version>:<ephemeral_pubkey_base58>:<view_tag_hex>[:<stealth_address_base58>]
|
|
189
|
+
*
|
|
190
|
+
* Accepts SIP:1 (legacy swapped scheme) and SIP:2 (canonical EIP-5564); the
|
|
191
|
+
* detected version is returned so the claim path can route to the matching derivation.
|
|
181
192
|
*
|
|
182
193
|
* M4 FIX: Validates view tag is exactly 1-2 hex characters (1 byte)
|
|
183
194
|
*/
|
|
184
195
|
export function parseAnnouncement(memo: string): SolanaAnnouncement | null {
|
|
185
|
-
|
|
196
|
+
const versionMatch = /^SIP:([12]):/.exec(memo)
|
|
197
|
+
if (!versionMatch) {
|
|
186
198
|
return null
|
|
187
199
|
}
|
|
200
|
+
const version = versionMatch[1]
|
|
188
201
|
|
|
189
|
-
const parts = memo.slice(
|
|
202
|
+
const parts = memo.slice(versionMatch[0].length).split(':')
|
|
190
203
|
if (parts.length < 2) {
|
|
191
204
|
return null
|
|
192
205
|
}
|
|
@@ -217,6 +230,7 @@ export function parseAnnouncement(memo: string): SolanaAnnouncement | null {
|
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
return {
|
|
233
|
+
version,
|
|
220
234
|
ephemeralPublicKey,
|
|
221
235
|
viewTag,
|
|
222
236
|
stealthAddress,
|
|
@@ -231,7 +245,7 @@ export function createAnnouncementMemo(
|
|
|
231
245
|
viewTag: string,
|
|
232
246
|
stealthAddress?: string
|
|
233
247
|
): string {
|
|
234
|
-
const parts = ['SIP:
|
|
248
|
+
const parts = ['SIP:2', ephemeralPublicKey, viewTag]
|
|
235
249
|
if (stealthAddress) {
|
|
236
250
|
parts.push(stealthAddress)
|
|
237
251
|
}
|
|
@@ -497,15 +497,15 @@ export class CosmosIBCStealthService {
|
|
|
497
497
|
const viewingPrivKey = hexToBytes(`0x${bytesToHex(viewingKey)}`.slice(2))
|
|
498
498
|
const spendingPrivKey = hexToBytes(spendingPrivateKey.slice(2))
|
|
499
499
|
|
|
500
|
-
// Compute stealth private key using EIP-5564
|
|
501
|
-
// 1. Compute shared secret: S =
|
|
502
|
-
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
500
|
+
// Compute stealth private key using canonical EIP-5564:
|
|
501
|
+
// 1. Compute shared secret: S = viewingPriv * ephemeralPub (ECDH on the viewing key)
|
|
502
|
+
const sharedSecretPoint = secp256k1.getSharedSecret(viewingPrivKey, ephemeralPubKey)
|
|
503
503
|
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
504
504
|
|
|
505
|
-
// 2. Derive stealth private key: stealthPriv =
|
|
506
|
-
const
|
|
505
|
+
// 2. Derive stealth private key: stealthPriv = spendingPriv + hash(S) mod n
|
|
506
|
+
const spendingPrivBigInt = bytesToBigInt(spendingPrivKey)
|
|
507
507
|
const hashBigInt = bytesToBigInt(sharedSecretHash)
|
|
508
|
-
const stealthPrivBigInt = (
|
|
508
|
+
const stealthPrivBigInt = (spendingPrivBigInt + hashBigInt) % secp256k1.CURVE.n
|
|
509
509
|
|
|
510
510
|
// Convert to bytes and then to compressed public key
|
|
511
511
|
const stealthPrivKey = bigIntToBytes(stealthPrivBigInt, 32)
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,12 @@ export {
|
|
|
88
88
|
generateEd25519StealthAddress,
|
|
89
89
|
deriveEd25519StealthPrivateKey,
|
|
90
90
|
checkEd25519StealthAddress,
|
|
91
|
+
// Legacy SIP:1 back-compat (claim/scan of pre-flip announcements)
|
|
92
|
+
deriveStealthPrivateKeyV1,
|
|
93
|
+
deriveEd25519StealthPrivateKeyV1,
|
|
94
|
+
checkEd25519StealthAddressV1,
|
|
95
|
+
deriveSecp256k1StealthPrivateKeyV1,
|
|
96
|
+
checkSecp256k1StealthAddressV1,
|
|
91
97
|
// Solana address derivation
|
|
92
98
|
ed25519PublicKeyToSolanaAddress,
|
|
93
99
|
solanaAddressToEd25519PublicKey,
|
package/src/move/aptos.ts
CHANGED
|
@@ -269,9 +269,12 @@ export function deriveAptosStealthPrivateKey(
|
|
|
269
269
|
* This is the same as the standard ed25519 check since Aptos stealth
|
|
270
270
|
* addresses use ed25519 stealth public keys.
|
|
271
271
|
*
|
|
272
|
+
* Canonical EIP-5564 view-only check: requires only the recipient's viewing
|
|
273
|
+
* private key plus their spending PUBLIC key (no spending private key needed).
|
|
274
|
+
*
|
|
272
275
|
* @param stealthAddress - Stealth address to check
|
|
273
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
274
276
|
* @param viewingPrivateKey - Recipient's viewing private key
|
|
277
|
+
* @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
|
|
275
278
|
* @returns true if this address belongs to the recipient
|
|
276
279
|
* @throws {ValidationError} If any input is invalid
|
|
277
280
|
*
|
|
@@ -279,21 +282,21 @@ export function deriveAptosStealthPrivateKey(
|
|
|
279
282
|
* ```typescript
|
|
280
283
|
* const isMine = checkAptosStealthAddress(
|
|
281
284
|
* stealthAddress,
|
|
282
|
-
*
|
|
283
|
-
*
|
|
285
|
+
* myViewingPrivKey,
|
|
286
|
+
* mySpendingPubKey
|
|
284
287
|
* )
|
|
285
288
|
* ```
|
|
286
289
|
*/
|
|
287
290
|
export function checkAptosStealthAddress(
|
|
288
291
|
stealthAddress: StealthAddress,
|
|
289
|
-
spendingPrivateKey: HexString,
|
|
290
292
|
viewingPrivateKey: HexString,
|
|
293
|
+
spendingPublicKey: HexString,
|
|
291
294
|
): boolean {
|
|
292
295
|
// Use standard ed25519 check
|
|
293
296
|
return checkEd25519StealthAddress(
|
|
294
297
|
stealthAddress,
|
|
295
|
-
|
|
296
|
-
|
|
298
|
+
viewingPrivateKey,
|
|
299
|
+
spendingPublicKey
|
|
297
300
|
)
|
|
298
301
|
}
|
|
299
302
|
|
|
@@ -344,17 +347,20 @@ export class AptosStealthService {
|
|
|
344
347
|
/**
|
|
345
348
|
* Check if a stealth address belongs to this recipient
|
|
346
349
|
*
|
|
350
|
+
* Canonical EIP-5564 view-only check: requires only the recipient's viewing
|
|
351
|
+
* private key plus their spending PUBLIC key (no spending private key needed).
|
|
352
|
+
*
|
|
347
353
|
* @param stealthAddress - Stealth address to check
|
|
348
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
349
354
|
* @param viewingPrivateKey - Recipient's viewing private key
|
|
355
|
+
* @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
|
|
350
356
|
* @returns true if the address belongs to this recipient
|
|
351
357
|
*/
|
|
352
358
|
checkStealthAddress(
|
|
353
359
|
stealthAddress: StealthAddress,
|
|
354
|
-
spendingPrivateKey: HexString,
|
|
355
360
|
viewingPrivateKey: HexString,
|
|
361
|
+
spendingPublicKey: HexString,
|
|
356
362
|
): boolean {
|
|
357
|
-
return checkAptosStealthAddress(stealthAddress,
|
|
363
|
+
return checkAptosStealthAddress(stealthAddress, viewingPrivateKey, spendingPublicKey)
|
|
358
364
|
}
|
|
359
365
|
|
|
360
366
|
/**
|
package/src/move/sui.ts
CHANGED
|
@@ -267,9 +267,12 @@ export function deriveSuiStealthPrivateKey(
|
|
|
267
267
|
* This is the same as the standard ed25519 check since Sui stealth
|
|
268
268
|
* addresses use ed25519 stealth public keys.
|
|
269
269
|
*
|
|
270
|
+
* Canonical EIP-5564 view-only check: requires only the recipient's viewing
|
|
271
|
+
* private key plus their spending PUBLIC key (no spending private key needed).
|
|
272
|
+
*
|
|
270
273
|
* @param stealthAddress - Stealth address to check
|
|
271
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
272
274
|
* @param viewingPrivateKey - Recipient's viewing private key
|
|
275
|
+
* @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
|
|
273
276
|
* @returns true if this address belongs to the recipient
|
|
274
277
|
* @throws {ValidationError} If any input is invalid
|
|
275
278
|
*
|
|
@@ -277,21 +280,21 @@ export function deriveSuiStealthPrivateKey(
|
|
|
277
280
|
* ```typescript
|
|
278
281
|
* const isMine = checkSuiStealthAddress(
|
|
279
282
|
* stealthAddress,
|
|
280
|
-
*
|
|
281
|
-
*
|
|
283
|
+
* myViewingPrivKey,
|
|
284
|
+
* mySpendingPubKey
|
|
282
285
|
* )
|
|
283
286
|
* ```
|
|
284
287
|
*/
|
|
285
288
|
export function checkSuiStealthAddress(
|
|
286
289
|
stealthAddress: StealthAddress,
|
|
287
|
-
spendingPrivateKey: HexString,
|
|
288
290
|
viewingPrivateKey: HexString,
|
|
291
|
+
spendingPublicKey: HexString,
|
|
289
292
|
): boolean {
|
|
290
293
|
// Use standard ed25519 check
|
|
291
294
|
return checkEd25519StealthAddress(
|
|
292
295
|
stealthAddress,
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
viewingPrivateKey,
|
|
297
|
+
spendingPublicKey
|
|
295
298
|
)
|
|
296
299
|
}
|
|
297
300
|
|
|
@@ -342,17 +345,20 @@ export class SuiStealthService {
|
|
|
342
345
|
/**
|
|
343
346
|
* Check if a stealth address belongs to this recipient
|
|
344
347
|
*
|
|
348
|
+
* Canonical EIP-5564 view-only check: requires only the recipient's viewing
|
|
349
|
+
* private key plus their spending PUBLIC key (no spending private key needed).
|
|
350
|
+
*
|
|
345
351
|
* @param stealthAddress - Stealth address to check
|
|
346
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
347
352
|
* @param viewingPrivateKey - Recipient's viewing private key
|
|
353
|
+
* @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
|
|
348
354
|
* @returns true if the address belongs to this recipient
|
|
349
355
|
*/
|
|
350
356
|
checkStealthAddress(
|
|
351
357
|
stealthAddress: StealthAddress,
|
|
352
|
-
spendingPrivateKey: HexString,
|
|
353
358
|
viewingPrivateKey: HexString,
|
|
359
|
+
spendingPublicKey: HexString,
|
|
354
360
|
): boolean {
|
|
355
|
-
return checkSuiStealthAddress(stealthAddress,
|
|
361
|
+
return checkSuiStealthAddress(stealthAddress, viewingPrivateKey, spendingPublicKey)
|
|
356
362
|
}
|
|
357
363
|
|
|
358
364
|
/**
|