@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
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { StealthMetaAddress, HexString, StealthAddress } from '@sip-protocol/types'
11
+ import { secp256k1 } from '@noble/curves/secp256k1'
12
+ import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
11
13
  import {
12
14
  generateEthereumStealthMetaAddress,
13
15
  generateEthereumStealthAddress,
@@ -15,6 +17,8 @@ import {
15
17
  encodeEthereumStealthMetaAddress,
16
18
  deriveEthereumStealthPrivateKey,
17
19
  checkEthereumStealthAddress,
20
+ checkEthereumStealthByEthAddress,
21
+ stealthPublicKeyToEthAddress,
18
22
  type EthereumStealthMetaAddress,
19
23
  type EthereumStealthAddress,
20
24
  } from './stealth'
@@ -359,20 +363,23 @@ export class EthereumPrivacyAdapter {
359
363
  /**
360
364
  * Check if a stealth address belongs to a recipient
361
365
  *
366
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
367
+ * private key plus their spending PUBLIC key (no spending private key needed).
368
+ *
362
369
  * @param stealthAddress - Stealth address object
363
- * @param spendingPrivateKey - Spending private key (hex)
364
370
  * @param viewingPrivateKey - Viewing private key (hex)
371
+ * @param spendingPublicKey - Spending public key (hex, meta-address spendingKey)
365
372
  * @returns True if the address belongs to the recipient
366
373
  */
367
374
  checkStealthAddress(
368
375
  stealthAddress: StealthAddress,
369
- spendingPrivateKey: HexString,
370
- viewingPrivateKey: HexString
376
+ viewingPrivateKey: HexString,
377
+ spendingPublicKey: HexString
371
378
  ): boolean {
372
379
  return checkEthereumStealthAddress(
373
380
  stealthAddress,
374
- spendingPrivateKey,
375
- viewingPrivateKey
381
+ viewingPrivateKey,
382
+ spendingPublicKey
376
383
  )
377
384
  }
378
385
 
@@ -582,20 +589,17 @@ export class EthereumPrivacyAdapter {
582
589
 
583
590
  // Check each recipient
584
591
  for (const recipient of this.scanRecipients.values()) {
585
- const isOwner = checkEthereumStealthAddress(
586
- stealthAddress,
587
- recipient.spendingPublicKey, // Note: need spending PRIVATE key for full check
588
- recipient.viewingPrivateKey
592
+ // Use ETH address comparison since announcements store 20-byte addresses
593
+ // Returns the stealth private key if match found, null otherwise
594
+ const stealthPrivateKey = checkEthereumStealthByEthAddress(
595
+ announcement.stealthAddress,
596
+ announcement.ephemeralPublicKey,
597
+ announcement.viewTag,
598
+ recipient.spendingPrivateKey,
599
+ recipient.viewingPrivateKey,
589
600
  )
590
601
 
591
- if (isOwner) {
592
- // Derive stealth private key for claiming
593
- const recovery = deriveEthereumStealthPrivateKey(
594
- stealthAddress,
595
- recipient.spendingPublicKey, // This should be spending PRIVATE key
596
- recipient.viewingPrivateKey
597
- )
598
-
602
+ if (stealthPrivateKey) {
599
603
  results.push({
600
604
  payment: {
601
605
  stealthAddress,
@@ -606,7 +610,7 @@ export class EthereumPrivacyAdapter {
606
610
  timestamp: announcement.timestamp,
607
611
  },
608
612
  recipient,
609
- stealthPrivateKey: recovery.privateKey,
613
+ stealthPrivateKey,
610
614
  })
611
615
  break // Found owner, no need to check other recipients
612
616
  }
@@ -641,12 +645,26 @@ export class EthereumPrivacyAdapter {
641
645
  * @returns Built claim transaction
642
646
  */
643
647
  buildClaimTransaction(params: EthereumClaimParams): EthereumClaimBuild {
644
- // Derive stealth private key
645
- const recovery = deriveEthereumStealthPrivateKey(
646
- params.stealthAddress,
647
- params.spendingPrivateKey,
648
- params.viewingPrivateKey
649
- )
648
+ // Use pre-derived key if available (e.g. from scanning flow), otherwise derive
649
+ let stealthPrivateKey: HexString
650
+ let stealthEthAddress: HexString
651
+
652
+ if (params.stealthPrivateKey) {
653
+ stealthPrivateKey = params.stealthPrivateKey
654
+ stealthEthAddress = stealthPublicKeyToEthAddress(
655
+ ('0x' + bytesToHex(
656
+ secp256k1.getPublicKey(hexToBytes(params.stealthPrivateKey.slice(2)), true)
657
+ )) as HexString
658
+ )
659
+ } else {
660
+ const recovery = deriveEthereumStealthPrivateKey(
661
+ params.stealthAddress,
662
+ params.spendingPrivateKey,
663
+ params.viewingPrivateKey
664
+ )
665
+ stealthPrivateKey = recovery.privateKey
666
+ stealthEthAddress = recovery.ethAddress
667
+ }
650
668
 
651
669
  // Build transaction
652
670
  const amount = params.amount ?? 0n // Full balance if not specified
@@ -673,8 +691,8 @@ export class EthereumPrivacyAdapter {
673
691
  }
674
692
 
675
693
  return {
676
- stealthEthAddress: recovery.ethAddress,
677
- stealthPrivateKey: recovery.privateKey,
694
+ stealthEthAddress,
695
+ stealthPrivateKey,
678
696
  destinationAddress: params.destinationAddress,
679
697
  amount,
680
698
  tx,
@@ -21,6 +21,7 @@ import type {
21
21
  StealthAddressRecovery,
22
22
  HexString,
23
23
  } from '@sip-protocol/types'
24
+ import { secp256k1 } from '@noble/curves/secp256k1'
24
25
  import {
25
26
  generateSecp256k1StealthMetaAddress,
26
27
  generateSecp256k1StealthAddress,
@@ -28,6 +29,8 @@ import {
28
29
  checkSecp256k1StealthAddress,
29
30
  publicKeyToEthAddress,
30
31
  } from '../../stealth/secp256k1'
32
+ import { sha256, hexToBytes, bytesToHex } from '../../stealth/utils'
33
+ import { isValidPrivateKey } from '../../validation'
31
34
  import { ValidationError } from '../../errors'
32
35
  import { SECP256K1_SCHEME_ID } from './constants'
33
36
 
@@ -345,9 +348,12 @@ export function deriveEthereumStealthPrivateKey(
345
348
  *
346
349
  * Used during scanning to quickly filter announcements.
347
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
+ *
348
354
  * @param stealthAddress - The stealth address to check
349
- * @param spendingPrivateKey - Recipient's spending private key
350
355
  * @param viewingPrivateKey - Recipient's viewing private key
356
+ * @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
351
357
  * @returns True if the address belongs to this recipient
352
358
  *
353
359
  * @example
@@ -356,8 +362,8 @@ export function deriveEthereumStealthPrivateKey(
356
362
  * for (const announcement of announcements) {
357
363
  * const isMine = checkEthereumStealthAddress(
358
364
  * announcement.stealthAddress,
359
- * mySpendingPrivateKey,
360
- * myViewingPrivateKey
365
+ * myViewingPrivateKey,
366
+ * mySpendingPublicKey
361
367
  * )
362
368
  * if (isMine) {
363
369
  * console.log('Found incoming payment!')
@@ -367,43 +373,91 @@ export function deriveEthereumStealthPrivateKey(
367
373
  */
368
374
  export function checkEthereumStealthAddress(
369
375
  stealthAddress: StealthAddress,
370
- spendingPrivateKey: HexString,
371
- viewingPrivateKey: HexString
376
+ viewingPrivateKey: HexString,
377
+ spendingPublicKey: HexString
372
378
  ): boolean {
373
379
  return checkSecp256k1StealthAddress(
374
380
  stealthAddress,
375
- spendingPrivateKey,
376
- viewingPrivateKey
381
+ viewingPrivateKey,
382
+ spendingPublicKey
377
383
  )
378
384
  }
379
385
 
380
386
  /**
381
- * Quick view tag check for efficient scanning
382
- *
383
- * Before doing the full elliptic curve check, verify the view tag matches.
384
- * This is ~256x faster for non-matching addresses.
387
+ * Check if an Ethereum stealth address matches by ETH address comparison
385
388
  *
386
- * @param _announcement - The announcement to check (unused, for API compatibility)
387
- * @param _viewingPrivateKey - Recipient's viewing private key (unused)
388
- * @param _spendingPublicKey - Sender's spending public key (unused)
389
- * @returns True if view tag matches (address *might* be ours)
389
+ * Used when the announcement only contains the 20-byte ETH address (not the
390
+ * full 33-byte compressed public key). Derives the expected stealth public key,
391
+ * converts it to an ETH address, and compares.
390
392
  *
391
- * @deprecated Use checkEthereumStealthAddress instead which handles everything
393
+ * @param ethAddress - The ETH address from the announcement (20 bytes)
394
+ * @param ephemeralPublicKey - Ephemeral public key from the announcement
395
+ * @param viewTag - View tag from the announcement
396
+ * @param spendingPublicKey - Recipient's spending public key
397
+ * @param viewingPrivateKey - Recipient's viewing private key
398
+ * @returns True if the address belongs to this recipient
392
399
  */
393
- export function checkViewTag(
394
- _announcement: { ephemeralPublicKey: HexString; viewTag: number },
395
- _viewingPrivateKey: HexString,
396
- _spendingPublicKey: HexString
397
- ): boolean {
398
- // This is a simplified check - the full implementation would compute
399
- // the shared secret and compare the first byte with the view tag.
400
- // For efficiency, the secp256k1.checkSecp256k1StealthAddress already
401
- // does the view tag check first, so we delegate to it with a minimal
402
- // StealthAddress object.
403
-
404
- // Note: This function is provided for API completeness but in practice,
405
- // users should call checkEthereumStealthAddress which handles everything.
406
- return true // Placeholder - actual implementation in checkEthereumStealthAddress
400
+ export function checkEthereumStealthByEthAddress(
401
+ ethAddress: HexString,
402
+ ephemeralPublicKey: HexString,
403
+ viewTag: number,
404
+ spendingPrivateKey: HexString,
405
+ viewingPrivateKey: HexString,
406
+ ): HexString | null {
407
+ if (!isValidPrivateKey(spendingPrivateKey)) {
408
+ throw new ValidationError(
409
+ 'must be a valid 32-byte hex string',
410
+ 'spendingPrivateKey'
411
+ )
412
+ }
413
+ if (!isValidPrivateKey(viewingPrivateKey)) {
414
+ throw new ValidationError(
415
+ 'must be a valid 32-byte hex string',
416
+ 'viewingPrivateKey'
417
+ )
418
+ }
419
+
420
+ const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
421
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
422
+ const ephemeralPubBytes = hexToBytes(ephemeralPublicKey.slice(2))
423
+
424
+ try {
425
+ // Compute shared secret: S = viewingPrivateKey * ephemeralPublicKey
426
+ // Canonical EIP-5564: ECDH on the viewing key (mirrors generation S = r * K_view)
427
+ const sharedSecretPoint = secp256k1.getSharedSecret(
428
+ viewingPrivBytes,
429
+ ephemeralPubBytes,
430
+ )
431
+ const sharedSecretHash = sha256(sharedSecretPoint)
432
+
433
+ // Quick view tag check
434
+ if (sharedSecretHash[0] !== viewTag) {
435
+ return null
436
+ }
437
+
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))
441
+ const hashScalar = BigInt('0x' + bytesToHex(sharedSecretHash))
442
+ const stealthPrivScalar = (spendingScalar + hashScalar) % secp256k1.CURVE.n
443
+
444
+ // Compute expected public key from derived private key
445
+ const stealthPrivHex = stealthPrivScalar.toString(16).padStart(64, '0')
446
+ const stealthPrivKeyBytes = hexToBytes(stealthPrivHex)
447
+ const expectedPubKey = secp256k1.getPublicKey(stealthPrivKeyBytes, true)
448
+
449
+ // Convert to ETH address
450
+ const expectedPubKeyHex = ('0x' + bytesToHex(expectedPubKey)) as HexString
451
+ const expectedEthAddress = publicKeyToEthAddress(expectedPubKeyHex)
452
+
453
+ // Compare addresses (case-insensitive)
454
+ if (expectedEthAddress.toLowerCase() === ethAddress.toLowerCase()) {
455
+ return ('0x' + stealthPrivHex) as HexString
456
+ }
457
+ return null
458
+ } catch {
459
+ return null
460
+ }
407
461
  }
408
462
 
409
463
  // ─── Address Conversion ─────────────────────────────────────────────────────
@@ -224,6 +224,8 @@ export interface EthereumClaimParams {
224
224
  spendingPrivateKey: HexString
225
225
  /** Destination address to receive funds */
226
226
  destinationAddress: HexString
227
+ /** Pre-derived stealth private key (skips derivation if provided, e.g. from scanning) */
228
+ stealthPrivateKey?: HexString
227
229
  /** Network */
228
230
  network?: EthereumNetwork
229
231
  /** RPC URL (overrides default) */
@@ -439,6 +441,8 @@ export interface EthereumScanRecipient {
439
441
  viewingPrivateKey: HexString
440
442
  /** Spending public key */
441
443
  spendingPublicKey: HexString
444
+ /** Spending private key (required for scanning and key derivation) */
445
+ spendingPrivateKey: HexString
442
446
  /** Optional label */
443
447
  label?: string
444
448
  }
@@ -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'
@@ -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