@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.
Files changed (65) 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 +1358 -847
  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-YWGJ77A2.mjs → chunk-PT2DNA7E.mjs} +335 -310
  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 +1348 -837
  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/constants.ts +33 -1
  34. package/src/chains/ethereum/index.ts +2 -1
  35. package/src/chains/ethereum/privacy-adapter.ts +44 -26
  36. package/src/chains/ethereum/stealth.ts +84 -30
  37. package/src/chains/ethereum/types.ts +4 -0
  38. package/src/chains/near/privacy-adapter.ts +8 -5
  39. package/src/chains/near/resolver.ts +22 -8
  40. package/src/chains/near/stealth.ts +9 -9
  41. package/src/chains/solana/constants.ts +13 -1
  42. package/src/chains/solana/ephemeral-keys.ts +3 -257
  43. package/src/chains/solana/index.ts +2 -3
  44. package/src/chains/solana/providers/helius-enhanced.ts +6 -6
  45. package/src/chains/solana/providers/webhook.ts +2 -2
  46. package/src/chains/solana/scan.ts +9 -8
  47. package/src/chains/solana/stealth-scanner.ts +3 -3
  48. package/src/chains/solana/types.ts +18 -4
  49. package/src/cosmos/ibc-stealth.ts +6 -6
  50. package/src/index.ts +6 -0
  51. package/src/move/aptos.ts +15 -9
  52. package/src/move/sui.ts +15 -9
  53. package/src/nft/private-nft.ts +10 -6
  54. package/src/privacy-backends/shadowwire.ts +13 -0
  55. package/src/stealth/ed25519.ts +173 -12
  56. package/src/stealth/index.ts +47 -4
  57. package/src/stealth/secp256k1.ts +144 -7
  58. package/src/stealth.ts +7 -0
  59. package/src/wallet/ethereum/privacy-adapter.ts +1 -1
  60. package/src/wallet/hardware/ledger-privacy.ts +2 -2
  61. package/src/wallet/near/adapter.ts +2 -2
  62. package/src/wallet/near/meteor-wallet.ts +2 -2
  63. package/src/wallet/near/my-near-wallet.ts +2 -2
  64. package/src/wallet/near/wallet-selector.ts +2 -2
  65. 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 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,
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
- * mySpendingPrivKey,
283
- * myViewingPrivKey
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
- spendingPrivateKey,
296
- viewingPrivateKey
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, spendingPrivateKey, viewingPrivateKey)
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
- * mySpendingPrivKey,
281
- * myViewingPrivKey
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
- spendingPrivateKey,
294
- viewingPrivateKey
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, spendingPrivateKey, viewingPrivateKey)
361
+ return checkSuiStealthAddress(stealthAddress, viewingPrivateKey, spendingPublicKey)
356
362
  }
357
363
 
358
364
  /**