@sip-protocol/sdk 0.9.0 → 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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{TransportWebUSB-YQMAGJAJ.mjs → TransportWebUSB-2KITI5HD.mjs} +24 -12
  3. package/dist/browser.d.mts +4 -4
  4. package/dist/browser.d.ts +4 -4
  5. package/dist/browser.js +1346 -838
  6. package/dist/browser.mjs +13 -3
  7. package/dist/{chunk-64AYA5F5.mjs → chunk-G3TBBG2K.mjs} +221 -146
  8. package/dist/{chunk-4GRJ5MAW.mjs → chunk-KXETSSKP.mjs} +4 -0
  9. package/dist/{chunk-6EU6WQFK.mjs → chunk-PT2DNA7E.mjs} +257 -235
  10. package/dist/{constants-LHAAUC2T.mjs → constants-DCJYTIU3.mjs} +5 -1
  11. package/dist/{dist-2OGQ7FED.mjs → dist-PYEXZNFD.mjs} +609 -221
  12. package/dist/{index-DeE1ZzA4.d.mts → index-B1d8pihL.d.mts} +117 -33
  13. package/dist/{index-DXh2IGkz.d.ts → index-UQhQJZbM.d.ts} +117 -33
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1339 -831
  17. package/dist/index.mjs +13 -3
  18. package/dist/{interface-Bf7w1PLW.d.mts → interface-CQi0-WfS.d.mts} +2 -2
  19. package/dist/{interface-Bf7w1PLW.d.ts → interface-CQi0-WfS.d.ts} +2 -2
  20. package/dist/{noir-kzbLVTei.d.mts → noir-CwPIyBLj.d.mts} +1 -1
  21. package/dist/{noir-kzbLVTei.d.ts → noir-CwPIyBLj.d.ts} +1 -1
  22. package/dist/proofs/halo2.d.mts +1 -1
  23. package/dist/proofs/halo2.d.ts +1 -1
  24. package/dist/proofs/kimchi.d.mts +1 -1
  25. package/dist/proofs/kimchi.d.ts +1 -1
  26. package/dist/proofs/noir.d.mts +1 -1
  27. package/dist/proofs/noir.d.ts +1 -1
  28. package/dist/{solana-U3MEGU7W.mjs → solana-ZWNIQTSU.mjs} +6 -6
  29. package/package.json +32 -32
  30. package/src/adapters/gelato-relay.ts +386 -0
  31. package/src/adapters/index.ts +28 -0
  32. package/src/adapters/oneinch.ts +126 -0
  33. package/src/chains/ethereum/privacy-adapter.ts +8 -5
  34. package/src/chains/ethereum/stealth.ts +17 -14
  35. package/src/chains/near/privacy-adapter.ts +8 -5
  36. package/src/chains/near/resolver.ts +22 -8
  37. package/src/chains/near/stealth.ts +9 -9
  38. package/src/chains/solana/constants.ts +13 -1
  39. package/src/chains/solana/ephemeral-keys.ts +3 -257
  40. package/src/chains/solana/index.ts +2 -3
  41. package/src/chains/solana/providers/helius-enhanced.ts +6 -6
  42. package/src/chains/solana/providers/webhook.ts +2 -2
  43. package/src/chains/solana/scan.ts +9 -8
  44. package/src/chains/solana/stealth-scanner.ts +3 -3
  45. package/src/chains/solana/types.ts +18 -4
  46. package/src/cosmos/ibc-stealth.ts +6 -6
  47. package/src/index.ts +6 -0
  48. package/src/move/aptos.ts +15 -9
  49. package/src/move/sui.ts +15 -9
  50. package/src/nft/private-nft.ts +10 -6
  51. package/src/privacy-backends/shadowwire.ts +13 -0
  52. package/src/stealth/ed25519.ts +173 -12
  53. package/src/stealth/index.ts +47 -4
  54. package/src/stealth/secp256k1.ts +144 -7
  55. package/src/stealth.ts +7 -0
  56. package/src/wallet/ethereum/privacy-adapter.ts +1 -1
  57. package/src/wallet/hardware/ledger-privacy.ts +2 -2
  58. package/src/wallet/near/adapter.ts +2 -2
  59. package/src/wallet/near/meteor-wallet.ts +2 -2
  60. package/src/wallet/near/my-near-wallet.ts +2 -2
  61. package/src/wallet/near/wallet-selector.ts +2 -2
  62. package/src/wallet/solana/privacy-adapter.ts +9 -9
  63. package/dist/chunk-5EKF243P.mjs +0 -33809
  64. package/dist/chunk-YWGJ77A2.mjs +0 -33806
@@ -30,6 +30,7 @@
30
30
  * @module chains/near/resolver
31
31
  */
32
32
 
33
+ import { ed25519 } from '@noble/curves/ed25519'
33
34
  import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
34
35
  import type { HexString, StealthAddress } from '@sip-protocol/types'
35
36
  import { ValidationError } from '../../errors'
@@ -44,6 +45,19 @@ import {
44
45
  } from './constants'
45
46
  import type { NEARViewingKey } from './viewing-key'
46
47
 
48
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Derive the ed25519 spending PUBLIC key from a spending private key.
52
+ *
53
+ * Canonical EIP-5564 view-only scanning needs the spending public key, but
54
+ * scan recipients in this module hold the spending private key. This converts
55
+ * one to the other on the fly.
56
+ */
57
+ function nearSpendingPublicFromPrivate(spendingPrivateKey: HexString): HexString {
58
+ return `0x${bytesToHex(ed25519.getPublicKey(hexToBytes(spendingPrivateKey.slice(2))))}` as HexString
59
+ }
60
+
47
61
  // ─── Types ────────────────────────────────────────────────────────────────────
48
62
 
49
63
  /**
@@ -652,8 +666,8 @@ export class NEARStealthScanner {
652
666
  try {
653
667
  const isMatch = checkNEARStealthAddress(
654
668
  stealthAddressToCheck,
655
- recipient.spendingPrivateKey,
656
- recipient.viewingPrivateKey
669
+ recipient.viewingPrivateKey,
670
+ nearSpendingPublicFromPrivate(recipient.spendingPrivateKey)
657
671
  )
658
672
 
659
673
  if (isMatch) {
@@ -764,8 +778,8 @@ export class NEARStealthScanner {
764
778
  try {
765
779
  return checkNEARStealthAddress(
766
780
  stealthAddressToCheck,
767
- spendingPrivateKey,
768
- viewingPrivateKey
781
+ viewingPrivateKey,
782
+ nearSpendingPublicFromPrivate(spendingPrivateKey)
769
783
  )
770
784
  } catch {
771
785
  return false
@@ -814,8 +828,8 @@ export class NEARStealthScanner {
814
828
  try {
815
829
  const isMatch = checkNEARStealthAddress(
816
830
  stealthAddressToCheck,
817
- recipient.spendingPrivateKey,
818
- recipient.viewingPrivateKey
831
+ recipient.viewingPrivateKey,
832
+ nearSpendingPublicFromPrivate(recipient.spendingPrivateKey)
819
833
  )
820
834
 
821
835
  if (isMatch) {
@@ -954,8 +968,8 @@ export function hasNEARAnnouncementMatch(
954
968
  try {
955
969
  const isMatch = checkNEARStealthAddress(
956
970
  stealthAddressToCheck,
957
- recipient.spendingPrivateKey,
958
- recipient.viewingPrivateKey
971
+ recipient.viewingPrivateKey,
972
+ nearSpendingPublicFromPrivate(recipient.spendingPrivateKey)
959
973
  )
960
974
 
961
975
  if (isMatch) {
@@ -204,12 +204,12 @@ export function deriveNEARStealthPrivateKey(
204
204
  /**
205
205
  * Check if a NEAR stealth address was intended for this recipient
206
206
  *
207
- * Efficiently checks if a stealth address belongs to the owner of
208
- * the given spending/viewing keys.
207
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
208
+ * private key plus their spending PUBLIC key (no spending private key needed).
209
209
  *
210
210
  * @param stealthAddress - The stealth address to check
211
- * @param spendingPrivateKey - Recipient's spending private key
212
211
  * @param viewingPrivateKey - Recipient's viewing private key
212
+ * @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
213
213
  * @returns True if the address belongs to this recipient
214
214
  *
215
215
  * @example
@@ -217,8 +217,8 @@ export function deriveNEARStealthPrivateKey(
217
217
  * // Check if a detected address is for us
218
218
  * const isForMe = checkNEARStealthAddress(
219
219
  * announcement.stealthAddress,
220
- * mySpendingPrivateKey,
221
- * myViewingPrivateKey
220
+ * myViewingPrivateKey,
221
+ * mySpendingPublicKey
222
222
  * )
223
223
  *
224
224
  * if (isForMe) {
@@ -228,13 +228,13 @@ export function deriveNEARStealthPrivateKey(
228
228
  */
229
229
  export function checkNEARStealthAddress(
230
230
  stealthAddress: StealthAddress,
231
- spendingPrivateKey: HexString,
232
- viewingPrivateKey: HexString
231
+ viewingPrivateKey: HexString,
232
+ spendingPublicKey: HexString
233
233
  ): boolean {
234
234
  return checkEd25519StealthAddress(
235
235
  stealthAddress,
236
- spendingPrivateKey,
237
- viewingPrivateKey
236
+ viewingPrivateKey,
237
+ spendingPublicKey
238
238
  )
239
239
  }
240
240
 
@@ -66,11 +66,23 @@ export const SOLANA_EXPLORER_URLS = {
66
66
  export const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'
67
67
 
68
68
  /**
69
- * SIP announcement memo prefix
69
+ * SIP announcement memo prefix (legacy SIP:1, swapped scheme — read-only back-compat)
70
70
  * Format: SIP:1:<ephemeral_pubkey_base58>:<view_tag_hex>
71
71
  */
72
72
  export const SIP_MEMO_PREFIX = 'SIP:1:'
73
73
 
74
+ /**
75
+ * Canonical EIP-5564 (SIP:2) announcement memo prefix — emitted by new sends.
76
+ * Format: SIP:2:<ephemeral_pubkey_base58>:<view_tag_hex>[:<stealth_address_base58>]
77
+ */
78
+ export const SIP_MEMO_PREFIX_V2 = 'SIP:2:'
79
+
80
+ /**
81
+ * Version-agnostic prefix for filtering announcement logs during scanning.
82
+ * Matches both SIP:1 (legacy) and SIP:2 (canonical) announcements.
83
+ */
84
+ export const SIP_MEMO_PREFIX_ANY = 'SIP:'
85
+
74
86
  /**
75
87
  * Estimated transaction fee in lamports
76
88
  * Includes base fee + rent for ATA creation
@@ -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 for stealth address generation (auto-disposes)
166
- * const result = managed.useForStealthAddress(
167
- * recipientSpendingKey,
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
- SIP_MEMO_PREFIX,
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:1:<ephemeral_pubkey_base58>:<view_tag_hex>
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:1:<ephemeral_pubkey>:<view_tag>
343
- if (description.includes(SIP_MEMO_PREFIX) || description.includes('SIP:')) {
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:1:<ephemeral_pubkey_base58>:<view_tag_hex>
348
- const sipMemoMatch = description.match(/SIP:1:([A-Za-z0-9]{32,44}):([0-9a-fA-F]{2})/)
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 { SIP_MEMO_PREFIX } from '../constants'
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(SIP_MEMO_PREFIX)) continue
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
- SIP_MEMO_PREFIX,
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(SIP_MEMO_PREFIX)) continue
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
- const recovery = deriveEd25519StealthPrivateKey(
290
- stealthAddressObj,
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
- SIP_MEMO_PREFIX,
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(SIP_MEMO_PREFIX)) continue
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(SIP_MEMO_PREFIX)) continue
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:1:<ephemeral_pubkey_base58>:<view_tag_hex>
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
- if (!memo.startsWith('SIP:1:')) {
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(6).split(':')
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:1', ephemeralPublicKey, viewTag]
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 algorithm:
501
- // 1. Compute shared secret: S = spendingPriv * ephemeralPub
502
- const sharedSecretPoint = secp256k1.getSharedSecret(spendingPrivKey, ephemeralPubKey)
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 = viewingPriv + hash(S) mod n
506
- const viewingPrivBigInt = bytesToBigInt(viewingPrivKey)
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 = (viewingPrivBigInt + hashBigInt) % secp256k1.CURVE.n
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,