@sip-protocol/sdk 0.9.0 → 0.11.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 (70) 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 +1363 -844
  6. package/dist/browser.mjs +13 -3
  7. package/dist/{chunk-64AYA5F5.mjs → chunk-7IUKXWDN.mjs} +229 -148
  8. package/dist/{chunk-4GRJ5MAW.mjs → chunk-KXETSSKP.mjs} +4 -0
  9. package/dist/{chunk-6EU6WQFK.mjs → chunk-L4RKPNIJ.mjs} +266 -239
  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-Cwo3WhxX.d.mts} +128 -37
  13. package/dist/{index-DXh2IGkz.d.ts → index-X8qPQdp6.d.ts} +128 -37
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1356 -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-7QOA3HBZ.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/index.ts +2 -0
  34. package/src/chains/ethereum/privacy-adapter.ts +64 -5
  35. package/src/chains/ethereum/stealth.ts +89 -14
  36. package/src/chains/ethereum/types.ts +18 -2
  37. package/src/chains/near/constants.ts +13 -1
  38. package/src/chains/near/index.ts +2 -0
  39. package/src/chains/near/privacy-adapter.ts +8 -5
  40. package/src/chains/near/resolver.ts +24 -10
  41. package/src/chains/near/stealth.ts +9 -9
  42. package/src/chains/near/types.ts +20 -9
  43. package/src/chains/solana/constants.ts +13 -1
  44. package/src/chains/solana/ephemeral-keys.ts +3 -257
  45. package/src/chains/solana/index.ts +2 -3
  46. package/src/chains/solana/providers/helius-enhanced.ts +6 -6
  47. package/src/chains/solana/providers/webhook.ts +2 -2
  48. package/src/chains/solana/scan.ts +9 -8
  49. package/src/chains/solana/stealth-scanner.ts +3 -3
  50. package/src/chains/solana/transfer.ts +1 -1
  51. package/src/chains/solana/types.ts +18 -4
  52. package/src/cosmos/ibc-stealth.ts +6 -6
  53. package/src/index.ts +6 -0
  54. package/src/move/aptos.ts +15 -9
  55. package/src/move/sui.ts +15 -9
  56. package/src/nft/private-nft.ts +10 -6
  57. package/src/privacy-backends/shadowwire.ts +13 -0
  58. package/src/stealth/ed25519.ts +173 -12
  59. package/src/stealth/index.ts +47 -4
  60. package/src/stealth/secp256k1.ts +157 -9
  61. package/src/stealth.ts +7 -0
  62. package/src/wallet/ethereum/privacy-adapter.ts +1 -1
  63. package/src/wallet/hardware/ledger-privacy.ts +2 -2
  64. package/src/wallet/near/adapter.ts +2 -2
  65. package/src/wallet/near/meteor-wallet.ts +2 -2
  66. package/src/wallet/near/my-near-wallet.ts +2 -2
  67. package/src/wallet/near/wallet-selector.ts +2 -2
  68. package/src/wallet/solana/privacy-adapter.ts +9 -9
  69. package/dist/chunk-5EKF243P.mjs +0 -33809
  70. package/dist/chunk-YWGJ77A2.mjs +0 -33806
@@ -348,9 +348,12 @@ export function deriveEthereumStealthPrivateKey(
348
348
  *
349
349
  * Used during scanning to quickly filter announcements.
350
350
  *
351
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
352
+ * private key plus their spending PUBLIC key (no spending private key needed).
353
+ *
351
354
  * @param stealthAddress - The stealth address to check
352
- * @param spendingPrivateKey - Recipient's spending private key
353
355
  * @param viewingPrivateKey - Recipient's viewing private key
356
+ * @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
354
357
  * @returns True if the address belongs to this recipient
355
358
  *
356
359
  * @example
@@ -359,8 +362,8 @@ export function deriveEthereumStealthPrivateKey(
359
362
  * for (const announcement of announcements) {
360
363
  * const isMine = checkEthereumStealthAddress(
361
364
  * announcement.stealthAddress,
362
- * mySpendingPrivateKey,
363
- * myViewingPrivateKey
365
+ * myViewingPrivateKey,
366
+ * mySpendingPublicKey
364
367
  * )
365
368
  * if (isMine) {
366
369
  * console.log('Found incoming payment!')
@@ -370,13 +373,13 @@ export function deriveEthereumStealthPrivateKey(
370
373
  */
371
374
  export function checkEthereumStealthAddress(
372
375
  stealthAddress: StealthAddress,
373
- spendingPrivateKey: HexString,
374
- viewingPrivateKey: HexString
376
+ viewingPrivateKey: HexString,
377
+ spendingPublicKey: HexString
375
378
  ): boolean {
376
379
  return checkSecp256k1StealthAddress(
377
380
  stealthAddress,
378
- spendingPrivateKey,
379
- viewingPrivateKey
381
+ viewingPrivateKey,
382
+ spendingPublicKey
380
383
  )
381
384
  }
382
385
 
@@ -419,10 +422,10 @@ export function checkEthereumStealthByEthAddress(
419
422
  const ephemeralPubBytes = hexToBytes(ephemeralPublicKey.slice(2))
420
423
 
421
424
  try {
422
- // Compute shared secret: S = spendingPrivateKey * ephemeralPublicKey
423
- // Mirrors generation: S = ephemeralPrivate * spendingPublic
425
+ // Compute shared secret: S = viewingPrivateKey * ephemeralPublicKey
426
+ // Canonical EIP-5564: ECDH on the viewing key (mirrors generation S = r * K_view)
424
427
  const sharedSecretPoint = secp256k1.getSharedSecret(
425
- spendingPrivBytes,
428
+ viewingPrivBytes,
426
429
  ephemeralPubBytes,
427
430
  )
428
431
  const sharedSecretHash = sha256(sharedSecretPoint)
@@ -432,11 +435,11 @@ export function checkEthereumStealthByEthAddress(
432
435
  return null
433
436
  }
434
437
 
435
- // Derive stealth private key: viewingPriv + hash(S) mod n
436
- // Mirrors generation: stealth = viewingPub + hash(S)*G
437
- const viewingScalar = BigInt('0x' + bytesToHex(viewingPrivBytes))
438
+ // Derive stealth private key: spendingPriv + hash(S) mod n
439
+ // Mirrors generation: stealth = spendingPub + hash(S)*G
440
+ const spendingScalar = BigInt('0x' + bytesToHex(spendingPrivBytes))
438
441
  const hashScalar = BigInt('0x' + bytesToHex(sharedSecretHash))
439
- const stealthPrivScalar = (viewingScalar + hashScalar) % secp256k1.CURVE.n
442
+ const stealthPrivScalar = (spendingScalar + hashScalar) % secp256k1.CURVE.n
440
443
 
441
444
  // Compute expected public key from derived private key
442
445
  const stealthPrivHex = stealthPrivScalar.toString(16).padStart(64, '0')
@@ -457,6 +460,78 @@ export function checkEthereumStealthByEthAddress(
457
460
  }
458
461
  }
459
462
 
463
+ /**
464
+ * View-only check of an Ethereum stealth announcement by ETH address.
465
+ *
466
+ * Detects whether a payment was intended for this recipient using only the viewing
467
+ * PRIVATE key and the spending PUBLIC key — never the spending private key (which is
468
+ * required to claim, not to detect). Recomputes the expected stealth public key as
469
+ * `A = K_spend + H(S)*G` (point arithmetic on the spending public key), converts it to an
470
+ * ETH address, and compares. Mirrors {@link checkEthereumStealthByEthAddress} but without
471
+ * the spending private key and returning a boolean rather than the derived key.
472
+ *
473
+ * @param ethAddress - The ETH address from the announcement (20 bytes)
474
+ * @param ephemeralPublicKey - Ephemeral public key from the announcement
475
+ * @param viewTag - View tag from the announcement
476
+ * @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
477
+ * @param viewingPrivateKey - Recipient's viewing private key
478
+ * @returns True if the address belongs to this recipient
479
+ */
480
+ export function checkEthereumStealthByEthAddressViewOnly(
481
+ ethAddress: HexString,
482
+ ephemeralPublicKey: HexString,
483
+ viewTag: number,
484
+ spendingPublicKey: HexString,
485
+ viewingPrivateKey: HexString,
486
+ ): boolean {
487
+ if (!isValidPrivateKey(viewingPrivateKey)) {
488
+ throw new ValidationError(
489
+ 'must be a valid 32-byte hex string',
490
+ 'viewingPrivateKey'
491
+ )
492
+ }
493
+
494
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
495
+ const spendingPubBytes = hexToBytes(spendingPublicKey.slice(2))
496
+ const ephemeralPubBytes = hexToBytes(ephemeralPublicKey.slice(2))
497
+
498
+ try {
499
+ // Compute shared secret: S = viewingPrivateKey * ephemeralPublicKey
500
+ // Canonical EIP-5564: ECDH on the viewing key (mirrors generation S = r * K_view)
501
+ const sharedSecretPoint = secp256k1.getSharedSecret(
502
+ viewingPrivBytes,
503
+ ephemeralPubBytes,
504
+ )
505
+ const sharedSecretHash = sha256(sharedSecretPoint)
506
+
507
+ // Quick view tag check
508
+ if (sharedSecretHash[0] !== viewTag) {
509
+ return false
510
+ }
511
+
512
+ // Expected stealth address: A = K_spend + hash(S)*G — point arithmetic on the spending
513
+ // PUBLIC key, so no spending private key is needed. Reduce hash(S) into [1, n-1] (a zero
514
+ // offset would be degenerate) before scaling the generator.
515
+ const hashScalar = BigInt('0x' + bytesToHex(sharedSecretHash)) % secp256k1.CURVE.n
516
+ if (hashScalar === 0n) {
517
+ return false
518
+ }
519
+ const spendingPubPoint = secp256k1.ProjectivePoint.fromHex(spendingPubBytes)
520
+ const expectedPoint = spendingPubPoint.add(
521
+ secp256k1.ProjectivePoint.BASE.multiply(hashScalar),
522
+ )
523
+ const expectedPubKey = expectedPoint.toRawBytes(true)
524
+
525
+ // Convert to ETH address and compare (case-insensitive)
526
+ const expectedPubKeyHex = ('0x' + bytesToHex(expectedPubKey)) as HexString
527
+ const expectedEthAddress = publicKeyToEthAddress(expectedPubKeyHex)
528
+
529
+ return expectedEthAddress.toLowerCase() === ethAddress.toLowerCase()
530
+ } catch {
531
+ return false
532
+ }
533
+ }
534
+
460
535
  // ─── Address Conversion ─────────────────────────────────────────────────────
461
536
 
462
537
  /**
@@ -441,8 +441,12 @@ export interface EthereumScanRecipient {
441
441
  viewingPrivateKey: HexString
442
442
  /** Spending public key */
443
443
  spendingPublicKey: HexString
444
- /** Spending private key (required for scanning and key derivation) */
445
- spendingPrivateKey: HexString
444
+ /**
445
+ * Spending private key — required to derive claimable keys during a full scan
446
+ * (`scanAnnouncements`). Optional for view-only registration: a recipient with only
447
+ * `viewingPrivateKey` + `spendingPublicKey` detects payments via `scanAnnouncementsViewOnly`.
448
+ */
449
+ spendingPrivateKey?: HexString
446
450
  /** Optional label */
447
451
  label?: string
448
452
  }
@@ -458,3 +462,15 @@ export interface EthereumDetectedPaymentResult {
458
462
  /** Derived stealth private key (for claiming) */
459
463
  stealthPrivateKey: HexString
460
464
  }
465
+
466
+ /**
467
+ * View-only detected payment — carries no derived private key (detection used the
468
+ * spending PUBLIC key only). To claim, derive the stealth private key separately with
469
+ * both private keys at claim time.
470
+ */
471
+ export interface EthereumViewOnlyDetectionResult {
472
+ /** The detected payment */
473
+ payment: EthereumDetectedPayment
474
+ /** The recipient that matched */
475
+ recipient: EthereumScanRecipient
476
+ }
@@ -77,11 +77,23 @@ export const NEAR_TOKEN_DECIMALS: Record<string, number> = {
77
77
  }
78
78
 
79
79
  /**
80
- * SIP announcement prefix for NEAR memos
80
+ * SIP announcement memo prefix (legacy SIP:1, swapped scheme — read-only back-compat)
81
81
  * Format: SIP:1:<ephemeral_pubkey_hex>:<view_tag_hex>
82
82
  */
83
83
  export const SIP_MEMO_PREFIX = 'SIP:1:'
84
84
 
85
+ /**
86
+ * Canonical EIP-5564 (SIP:2) announcement memo prefix — emitted by new sends.
87
+ * Format: SIP:2:<ephemeral_pubkey_hex>:<view_tag_hex>
88
+ */
89
+ export const SIP_MEMO_PREFIX_V2 = 'SIP:2:'
90
+
91
+ /**
92
+ * Version-agnostic announcement memo prefix for scanning.
93
+ * Matches both SIP:1 (legacy) and SIP:2 (canonical) announcements.
94
+ */
95
+ export const SIP_MEMO_PREFIX_ANY = 'SIP:'
96
+
85
97
  /**
86
98
  * NEAR implicit account length (64 hex characters = 32 bytes)
87
99
  */
@@ -49,6 +49,8 @@ export {
49
49
  NEAR_TOKEN_CONTRACTS,
50
50
  NEAR_TOKEN_DECIMALS,
51
51
  SIP_MEMO_PREFIX,
52
+ SIP_MEMO_PREFIX_V2,
53
+ SIP_MEMO_PREFIX_ANY,
52
54
  NEAR_IMPLICIT_ACCOUNT_LENGTH,
53
55
  NEAR_ACCOUNT_ID_MIN_LENGTH,
54
56
  NEAR_ACCOUNT_ID_MAX_LENGTH,
@@ -426,20 +426,23 @@ export class NEARPrivacyAdapter {
426
426
  /**
427
427
  * Check if a stealth address belongs to a recipient
428
428
  *
429
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
430
+ * private key plus their spending PUBLIC key (no spending private key needed).
431
+ *
429
432
  * @param stealthAddress - Stealth address object
430
- * @param spendingPrivateKey - Spending private key (hex)
431
433
  * @param viewingPrivateKey - Viewing private key (hex)
434
+ * @param spendingPublicKey - Spending public key (hex, meta-address spendingKey)
432
435
  * @returns True if the address belongs to the recipient
433
436
  */
434
437
  checkStealthAddress(
435
438
  stealthAddress: StealthAddress,
436
- spendingPrivateKey: HexString,
437
- viewingPrivateKey: HexString
439
+ viewingPrivateKey: HexString,
440
+ spendingPublicKey: HexString
438
441
  ): boolean {
439
442
  return checkNEARStealthAddress(
440
443
  stealthAddress,
441
- spendingPrivateKey,
442
- viewingPrivateKey
444
+ viewingPrivateKey,
445
+ spendingPublicKey
443
446
  )
444
447
  }
445
448
 
@@ -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'
@@ -37,13 +38,26 @@ import { isValidHex } from '../../validation'
37
38
  import { checkNEARStealthAddress, implicitAccountToEd25519PublicKey } from './stealth'
38
39
  import { parseAnnouncement, type NEARAnnouncement } from './types'
39
40
  import {
40
- SIP_MEMO_PREFIX,
41
+ SIP_MEMO_PREFIX_ANY,
41
42
  VIEW_TAG_MIN,
42
43
  VIEW_TAG_MAX,
43
44
  isImplicitAccount,
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) {
@@ -708,7 +722,7 @@ export class NEARStealthScanner {
708
722
  const announcements: NEARAnnouncement[] = []
709
723
 
710
724
  for (const log of logs) {
711
- if (!log.includes(SIP_MEMO_PREFIX)) {
725
+ if (!log.includes(SIP_MEMO_PREFIX_ANY)) {
712
726
  continue
713
727
  }
714
728
 
@@ -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
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { HexString, StealthAddress } from '@sip-protocol/types'
11
- import { SIP_MEMO_PREFIX, VIEW_TAG_MIN, VIEW_TAG_MAX } from './constants'
11
+ import { SIP_MEMO_PREFIX_V2, VIEW_TAG_MIN, VIEW_TAG_MAX } from './constants'
12
12
 
13
13
  // ─── Announcement Types ──────────────────────────────────────────────────────
14
14
 
@@ -18,6 +18,8 @@ import { SIP_MEMO_PREFIX, VIEW_TAG_MIN, VIEW_TAG_MAX } from './constants'
18
18
  * Contains the information needed for recipients to scan for payments.
19
19
  */
20
20
  export interface NEARAnnouncement {
21
+ /** Announcement scheme version: '1' = legacy swapped, '2' = canonical EIP-5564 */
22
+ version?: string
21
23
  /** Ephemeral public key (ed25519, 0x-prefixed hex) */
22
24
  ephemeralPublicKey: HexString
23
25
  /** View tag for efficient filtering (0-255) */
@@ -35,16 +37,21 @@ export interface NEARAnnouncement {
35
37
  /**
36
38
  * Parse an announcement from a NEAR memo string
37
39
  *
38
- * Format: SIP:1:<ephemeral_pubkey_hex>:<view_tag_hex>
40
+ * Accepts SIP:1 (legacy swapped scheme) and SIP:2 (canonical EIP-5564); the detected
41
+ * version is returned. NEAR derives canonically regardless, so the version is recorded
42
+ * for consistency rather than to route the claim.
43
+ *
44
+ * Format: SIP:<version>:<ephemeral_pubkey_hex>:<view_tag_hex>
39
45
  *
40
46
  * @param memo - The memo string to parse
41
47
  * @returns Parsed announcement or null if invalid
42
48
  *
43
49
  * @example
44
50
  * ```typescript
45
- * const memo = 'SIP:1:1234...abcd:0f'
51
+ * const memo = 'SIP:2:1234...abcd:0f'
46
52
  * const announcement = parseAnnouncement(memo)
47
53
  * if (announcement) {
54
+ * console.log(announcement.version) // '2'
48
55
  * console.log(announcement.ephemeralPublicKey)
49
56
  * console.log(announcement.viewTag) // 15
50
57
  * }
@@ -55,13 +62,15 @@ export function parseAnnouncement(memo: string): Partial<NEARAnnouncement> | nul
55
62
  return null
56
63
  }
57
64
 
58
- // Check prefix
59
- if (!memo.startsWith(SIP_MEMO_PREFIX)) {
65
+ // Accept SIP:1 (legacy) and SIP:2 (canonical); capture the version.
66
+ const versionMatch = /^SIP:([12]):/.exec(memo)
67
+ if (!versionMatch) {
60
68
  return null
61
69
  }
70
+ const version = versionMatch[1]
62
71
 
63
- // Parse parts: SIP:1:<ephemeral_pubkey>:<view_tag>
64
- const content = memo.slice(SIP_MEMO_PREFIX.length)
72
+ // Parse parts: <ephemeral_pubkey_hex>:<view_tag_hex>
73
+ const content = memo.slice(versionMatch[0].length)
65
74
  const parts = content.split(':')
66
75
 
67
76
  if (parts.length < 2) {
@@ -86,6 +95,7 @@ export function parseAnnouncement(memo: string): Partial<NEARAnnouncement> | nul
86
95
  }
87
96
 
88
97
  return {
98
+ version,
89
99
  ephemeralPublicKey: `0x${ephemeralKeyHex.toLowerCase()}` as HexString,
90
100
  viewTag,
91
101
  }
@@ -104,7 +114,7 @@ export function parseAnnouncement(memo: string): Partial<NEARAnnouncement> | nul
104
114
  * stealthAddress.ephemeralPublicKey,
105
115
  * stealthAddress.viewTag
106
116
  * )
107
- * // => 'SIP:1:1234...abcd:0f'
117
+ * // => 'SIP:2:1234...abcd:0f'
108
118
  * ```
109
119
  */
110
120
  export function createAnnouncementMemo(
@@ -117,7 +127,8 @@ export function createAnnouncementMemo(
117
127
  // Convert view tag to 2-char hex
118
128
  const viewTagHex = viewTag.toString(16).padStart(2, '0')
119
129
 
120
- return `${SIP_MEMO_PREFIX}${ephemeralKeyHex}:${viewTagHex}`
130
+ // Emit the canonical SIP:2 announcement (EIP-5564). Legacy SIP:1 remains parseable.
131
+ return `${SIP_MEMO_PREFIX_V2}${ephemeralKeyHex}:${viewTagHex}`
121
132
  }
122
133
 
123
134
  // ─── Transfer Types ──────────────────────────────────────────────────────────
@@ -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