@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
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
  /**
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { sha256 } from '@noble/hashes/sha256'
22
+ import { ed25519 } from '@noble/curves/ed25519'
22
23
  import { secp256k1 } from '@noble/curves/secp256k1'
23
24
  import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
24
25
  import type {
@@ -490,9 +491,12 @@ export class PrivateNFT {
490
491
 
491
492
  const ownedNFTs: OwnedNFT[] = []
492
493
 
493
- // Convert keys to hex for stealth checking
494
- const scanKeyHex = `0x${bytesToHex(scanKey)}` as HexString
494
+ // Convert keys to hex for stealth checking.
495
+ // Canonical EIP-5564 view-only scanning needs the spending PUBLIC key, so
496
+ // derive it from the spending private (scan) key once for both curves.
495
497
  const viewingKeyHex = `0x${bytesToHex(viewingKey)}` as HexString
498
+ const ed25519SpendingPubHex = `0x${bytesToHex(ed25519.getPublicKey(scanKey))}` as HexString
499
+ const secp256k1SpendingPubHex = `0x${bytesToHex(secp256k1.getPublicKey(scanKey, true))}` as HexString
496
500
 
497
501
  // Scan each transfer
498
502
  for (const transfer of transfers) {
@@ -511,14 +515,14 @@ export class PrivateNFT {
511
515
  if (isEd25519Chain(transfer.chain)) {
512
516
  isOwned = checkEd25519StealthAddress(
513
517
  transfer.newOwnerStealth,
514
- scanKeyHex,
515
- viewingKeyHex
518
+ viewingKeyHex,
519
+ ed25519SpendingPubHex
516
520
  )
517
521
  } else {
518
522
  isOwned = checkStealthAddress(
519
523
  transfer.newOwnerStealth,
520
- scanKeyHex,
521
- viewingKeyHex
524
+ viewingKeyHex,
525
+ secp256k1SpendingPubHex
522
526
  )
523
527
  }
524
528
 
@@ -76,6 +76,12 @@ export const SHADOWWIRE_TOKEN_MINTS: Record<TokenSymbol, string> = {
76
76
  USD1: 'USD1exampleaddress1111111111111111111111111', // Placeholder - USD1 stablecoin
77
77
  AOL: 'AOLexampleaddress11111111111111111111111111', // Placeholder
78
78
  IQLABS: 'IQLABSexampleaddress111111111111111111111', // Placeholder - IQ Labs token
79
+ // Added in @radr/shadowwire@1.1.15 — real mints verified against the package's TOKEN_MINTS
80
+ SANA: '5dpN5wMH8j8au29Rp91qn4WfNq6t6xJfcjQNcFeDJ8Ct',
81
+ POKI: '6vK6cL9C66Bsqw7SC2hcCdkgm1UKBDUE6DCYJ4kubonk',
82
+ RAIN: '3iC63FgnB7EhcPaiSaC51UkVweeBDkqu17SaRyy2pump',
83
+ HOSICO: 'Dx2bQe2UPv4k3BmcW8G2KhaL5oKsxduM5XxLSV3Sbonk',
84
+ SKR: 'SKRbvo6Gf7GondiT3BbTfuRDPqLWei4j2Qy2NPGZhW3',
79
85
  }
80
86
 
81
87
  /**
@@ -370,6 +376,13 @@ export class ShadowWireBackend implements PrivacyBackend {
370
376
  amount,
371
377
  token_mint: tokenMint,
372
378
  })
379
+ // @radr/shadowwire@1.1.15 made WithdrawResponse.unsigned_tx_base64 optional
380
+ // (returned only in the unsigned-tx flow). Absence here is an error, not an empty tx.
381
+ if (!response.unsigned_tx_base64) {
382
+ throw new Error(
383
+ `ShadowWire withdraw returned no unsigned transaction${response.error ? `: ${response.error}` : ''}`
384
+ )
385
+ }
373
386
  return {
374
387
  unsignedTx: response.unsigned_tx_base64,
375
388
  amountWithdrawn: response.amount_withdrawn,
@@ -195,11 +195,11 @@ export function generateEd25519StealthMetaAddress(
195
195
  /**
196
196
  * Generate a one-time ed25519 stealth address for a recipient
197
197
  *
198
- * Algorithm (DKSAP for ed25519):
198
+ * Algorithm (DKSAP for ed25519, canonical EIP-5564):
199
199
  * 1. Generate ephemeral keypair (r, R = r*G)
200
- * 2. Compute shared secret: S = r * P_spend (ephemeral scalar * spending public)
200
+ * 2. Compute shared secret: S = r * P_view (ephemeral scalar * viewing public)
201
201
  * 3. Hash shared secret: h = SHA256(S)
202
- * 4. Derive stealth public key: P_stealth = P_view + h*G
202
+ * 4. Derive stealth public key: P_stealth = P_spend + h*G
203
203
  */
204
204
  export function generateEd25519StealthAddress(
205
205
  recipientMetaAddress: StealthMetaAddress,
@@ -226,14 +226,14 @@ export function generateEd25519StealthAddress(
226
226
  throw new Error('CRITICAL: Zero ephemeral scalar after reduction - investigate RNG')
227
227
  }
228
228
 
229
- // S = ephemeral_scalar * P_spend
230
- const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes)
231
- const sharedSecretPoint = spendingPoint.multiply(ephemeralScalar)
229
+ // S = ephemeral_scalar * P_view (canonical EIP-5564: ECDH on the VIEWING key)
230
+ const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes)
231
+ const sharedSecretPoint = viewingPoint.multiply(ephemeralScalar)
232
232
 
233
233
  // Hash the shared secret point
234
234
  const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
235
235
 
236
- // Derive stealth public key: P_stealth = P_view + hash(S)*G
236
+ // Derive stealth public key: P_stealth = P_spend + hash(S)*G
237
237
  const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
238
238
  if (hashScalar === 0n) {
239
239
  throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
@@ -242,9 +242,9 @@ export function generateEd25519StealthAddress(
242
242
  // Compute hash(S) * G
243
243
  const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar)
244
244
 
245
- // Add to viewing key: P_stealth = P_view + hash(S)*G
246
- const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes)
247
- const stealthPoint = viewingPoint.add(hashTimesG)
245
+ // Add to spending key: P_stealth = P_spend + hash(S)*G
246
+ const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes)
247
+ const stealthPoint = spendingPoint.add(hashTimesG)
248
248
  const stealthAddressBytes = stealthPoint.toRawBytes()
249
249
 
250
250
  // Compute view tag
@@ -266,7 +266,9 @@ export function generateEd25519StealthAddress(
266
266
  // ─── Private Key Derivation ─────────────────────────────────────────────────
267
267
 
268
268
  /**
269
- * Derive the private key for an ed25519 stealth address
269
+ * Derive the private key for an ed25519 stealth address (canonical EIP-5564)
270
+ *
271
+ * Requires BOTH the spending and viewing private keys (spending authority).
270
272
  *
271
273
  * **IMPORTANT: Derived Key Format**
272
274
  *
@@ -302,6 +304,86 @@ export function deriveEd25519StealthPrivateKey(
302
304
  const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
303
305
  const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
304
306
 
307
+ try {
308
+ // Compute shared secret: S = viewing_scalar * R (canonical: ECDH on the viewing key)
309
+ const rawViewingScalar = getEd25519Scalar(viewingPrivBytes)
310
+ const viewingScalar = rawViewingScalar % ED25519_ORDER
311
+ if (viewingScalar === 0n) {
312
+ throw new Error('CRITICAL: Zero viewing scalar after reduction - investigate key derivation')
313
+ }
314
+ const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes)
315
+ const sharedSecretPoint = ephemeralPoint.multiply(viewingScalar)
316
+
317
+ // Hash the shared secret
318
+ const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
319
+
320
+ // Get spending scalar and reduce mod L
321
+ const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes)
322
+ const spendingScalar = rawSpendingScalar % ED25519_ORDER
323
+ if (spendingScalar === 0n) {
324
+ throw new Error('CRITICAL: Zero spending scalar after reduction - investigate key derivation')
325
+ }
326
+
327
+ // Derive stealth private key: s_stealth = s_spend + hash(S) mod L (canonical)
328
+ const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
329
+ if (hashScalar === 0n) {
330
+ throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
331
+ }
332
+ const stealthPrivateScalar = (spendingScalar + hashScalar) % ED25519_ORDER
333
+ if (stealthPrivateScalar === 0n) {
334
+ throw new Error('CRITICAL: Zero stealth scalar after reduction - investigate key derivation')
335
+ }
336
+
337
+ // Convert to bytes (little-endian for ed25519)
338
+ const stealthPrivateKey = bigIntToBytesLE(stealthPrivateScalar, 32)
339
+
340
+ const result = {
341
+ stealthAddress: stealthAddress.address,
342
+ ephemeralPublicKey: stealthAddress.ephemeralPublicKey,
343
+ privateKey: `0x${bytesToHex(stealthPrivateKey)}` as HexString,
344
+ }
345
+
346
+ secureWipe(stealthPrivateKey)
347
+
348
+ return result
349
+ } finally {
350
+ secureWipeAll(spendingPrivBytes, viewingPrivBytes)
351
+ }
352
+ }
353
+
354
+ /**
355
+ * @deprecated Legacy SIP:1 swapped-scheme derivation — claim-side back-compat ONLY.
356
+ *
357
+ * Recovers funds sent to stealth addresses generated before the canonical
358
+ * EIP-5564 flip (legacy scheme: ECDH used the spending key, `S = s_spend * R`,
359
+ * and the private key was `p = s_view + H(S)`). Used only when claiming a
360
+ * `SIP:1` announcement. New (SIP:2) sends use {@link deriveEd25519StealthPrivateKey}.
361
+ */
362
+ export function deriveEd25519StealthPrivateKeyV1(
363
+ stealthAddress: StealthAddress,
364
+ spendingPrivateKey: HexString,
365
+ viewingPrivateKey: HexString,
366
+ ): StealthAddressRecovery {
367
+ validateEd25519StealthAddress(stealthAddress)
368
+
369
+ if (!isValidPrivateKey(spendingPrivateKey)) {
370
+ throw new ValidationError(
371
+ 'must be a valid 32-byte hex string',
372
+ 'spendingPrivateKey'
373
+ )
374
+ }
375
+
376
+ if (!isValidPrivateKey(viewingPrivateKey)) {
377
+ throw new ValidationError(
378
+ 'must be a valid 32-byte hex string',
379
+ 'viewingPrivateKey'
380
+ )
381
+ }
382
+
383
+ const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
384
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
385
+ const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
386
+
305
387
  try {
306
388
  // Get spending scalar and reduce mod L
307
389
  const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes)
@@ -354,9 +436,88 @@ export function deriveEd25519StealthPrivateKey(
354
436
  // ─── Address Checking ───────────────────────────────────────────────────────
355
437
 
356
438
  /**
357
- * Check if an ed25519 stealth address was intended for this recipient
439
+ * Check if an ed25519 stealth address is ours canonical EIP-5564 view-only.
440
+ *
441
+ * Requires only the viewing PRIVATE key + the spending PUBLIC key, so a viewing
442
+ * key can be delegated for scanning without granting spend authority. Never
443
+ * touches the spending private key.
444
+ *
445
+ * @param stealthAddress - Stealth address to check
446
+ * @param viewingPrivateKey - Recipient's viewing private key
447
+ * @param spendingPublicKey - Recipient's spending PUBLIC key (meta-address spendingKey)
448
+ * @returns true if this address belongs to the recipient
358
449
  */
359
450
  export function checkEd25519StealthAddress(
451
+ stealthAddress: StealthAddress,
452
+ viewingPrivateKey: HexString,
453
+ spendingPublicKey: HexString,
454
+ ): boolean {
455
+ validateEd25519StealthAddress(stealthAddress)
456
+
457
+ if (!isValidPrivateKey(viewingPrivateKey)) {
458
+ throw new ValidationError(
459
+ 'must be a valid 32-byte hex string',
460
+ 'viewingPrivateKey'
461
+ )
462
+ }
463
+
464
+ if (!isValidEd25519PublicKey(spendingPublicKey)) {
465
+ throw new ValidationError(
466
+ 'must be a valid ed25519 public key (32 bytes)',
467
+ 'spendingPublicKey'
468
+ )
469
+ }
470
+
471
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
472
+ const spendingPubBytes = hexToBytes(spendingPublicKey.slice(2))
473
+ const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
474
+
475
+ try {
476
+ // Compute shared secret: S = viewing_scalar * R (canonical: ECDH on the viewing key)
477
+ const rawViewingScalar = getEd25519Scalar(viewingPrivBytes)
478
+ const viewingScalar = rawViewingScalar % ED25519_ORDER
479
+ if (viewingScalar === 0n) {
480
+ throw new Error('CRITICAL: Zero viewing scalar after reduction - investigate key derivation')
481
+ }
482
+ const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes)
483
+ const sharedSecretPoint = ephemeralPoint.multiply(viewingScalar)
484
+
485
+ // Hash the shared secret
486
+ const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
487
+
488
+ // View tag check (fast reject)
489
+ if (sharedSecretHash[0] !== stealthAddress.viewTag) {
490
+ return false
491
+ }
492
+
493
+ const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
494
+ if (hashScalar === 0n) {
495
+ throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
496
+ }
497
+
498
+ // Expected address: P_stealth = P_spend + hash(S)*G (no spending private key needed)
499
+ const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar)
500
+ const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingPubBytes)
501
+ const expectedPoint = spendingPoint.add(hashTimesG)
502
+ const expectedPubKeyBytes = expectedPoint.toRawBytes()
503
+
504
+ // Compare with provided stealth address
505
+ const providedAddress = hexToBytes(stealthAddress.address.slice(2))
506
+
507
+ return bytesToHex(expectedPubKeyBytes) === bytesToHex(providedAddress)
508
+ } finally {
509
+ secureWipe(viewingPrivBytes)
510
+ }
511
+ }
512
+
513
+ /**
514
+ * @deprecated Legacy SIP:1 full-wallet check — requires BOTH private keys.
515
+ *
516
+ * For detecting/claiming pre-flip (SIP:1) announcements only (legacy swapped
517
+ * scheme: `S = s_spend * R`, address built on the viewing key). New code should
518
+ * use the view-only {@link checkEd25519StealthAddress}.
519
+ */
520
+ export function checkEd25519StealthAddressV1(
360
521
  stealthAddress: StealthAddress,
361
522
  spendingPrivateKey: HexString,
362
523
  viewingPrivateKey: HexString,
@@ -30,14 +30,18 @@ import {
30
30
  generateEd25519StealthMetaAddress,
31
31
  generateEd25519StealthAddress,
32
32
  deriveEd25519StealthPrivateKey,
33
+ deriveEd25519StealthPrivateKeyV1,
33
34
  checkEd25519StealthAddress,
35
+ checkEd25519StealthAddressV1,
34
36
  } from './ed25519'
35
37
 
36
38
  import {
37
39
  generateSecp256k1StealthMetaAddress,
38
40
  generateSecp256k1StealthAddress,
39
41
  deriveSecp256k1StealthPrivateKey,
42
+ deriveSecp256k1StealthPrivateKeyV1,
40
43
  checkSecp256k1StealthAddress,
44
+ checkSecp256k1StealthAddressV1,
41
45
  publicKeyToEthAddress,
42
46
  validateSecp256k1StealthMetaAddress,
43
47
  validateSecp256k1StealthAddress,
@@ -166,32 +170,63 @@ export function deriveStealthPrivateKey(
166
170
  return deriveSecp256k1StealthPrivateKey(stealthAddress, spendingPrivateKey, viewingPrivateKey)
167
171
  }
168
172
 
173
+ /**
174
+ * Derive the stealth private key for a LEGACY SIP:1 announcement (back-compat)
175
+ *
176
+ * Routes to the pre-flip swapped-scheme derivation. Use only when claiming funds
177
+ * announced before the canonical EIP-5564 flip; new (SIP:2) payments use
178
+ * {@link deriveStealthPrivateKey}.
179
+ *
180
+ * @param stealthAddress - The legacy stealth address to recover
181
+ * @param spendingPrivateKey - Recipient's spending private key
182
+ * @param viewingPrivateKey - Recipient's viewing private key
183
+ * @returns Recovery data including the derived private key
184
+ */
185
+ export function deriveStealthPrivateKeyV1(
186
+ stealthAddress: StealthAddress,
187
+ spendingPrivateKey: HexString,
188
+ viewingPrivateKey: HexString,
189
+ ): StealthAddressRecovery {
190
+ const addressHex = stealthAddress.address.slice(2)
191
+
192
+ if (addressHex.length === 64) {
193
+ // 32 bytes = ed25519
194
+ return deriveEd25519StealthPrivateKeyV1(stealthAddress, spendingPrivateKey, viewingPrivateKey)
195
+ }
196
+
197
+ // Default to secp256k1
198
+ return deriveSecp256k1StealthPrivateKeyV1(stealthAddress, spendingPrivateKey, viewingPrivateKey)
199
+ }
200
+
169
201
  /**
170
202
  * Check if a stealth address was intended for this recipient
171
203
  *
172
204
  * Automatically dispatches to the correct curve implementation.
173
205
  *
206
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
207
+ * private key plus their spending PUBLIC key (no spending private key needed).
208
+ *
174
209
  * @param stealthAddress - Stealth address to check
175
- * @param spendingPrivateKey - Recipient's spending private key
176
210
  * @param viewingPrivateKey - Recipient's viewing private key
211
+ * @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
177
212
  * @returns true if this address belongs to the recipient
178
213
  * @throws {ValidationError} If any input is invalid
179
214
  */
180
215
  export function checkStealthAddress(
181
216
  stealthAddress: StealthAddress,
182
- spendingPrivateKey: HexString,
183
217
  viewingPrivateKey: HexString,
218
+ spendingPublicKey: HexString,
184
219
  ): boolean {
185
220
  // Try to detect curve from address length
186
221
  const addressHex = stealthAddress.address.slice(2)
187
222
 
188
223
  if (addressHex.length === 64) {
189
224
  // 32 bytes = ed25519
190
- return checkEd25519StealthAddress(stealthAddress, spendingPrivateKey, viewingPrivateKey)
225
+ return checkEd25519StealthAddress(stealthAddress, viewingPrivateKey, spendingPublicKey)
191
226
  }
192
227
 
193
228
  // Default to secp256k1
194
- return checkSecp256k1StealthAddress(stealthAddress, spendingPrivateKey, viewingPrivateKey)
229
+ return checkSecp256k1StealthAddress(stealthAddress, viewingPrivateKey, spendingPublicKey)
195
230
  }
196
231
 
197
232
  // ─── Re-exports ─────────────────────────────────────────────────────────────
@@ -207,6 +242,14 @@ export {
207
242
  checkEd25519StealthAddress,
208
243
  }
209
244
 
245
+ // Legacy SIP:1 back-compat (claim/scan of pre-flip announcements)
246
+ export {
247
+ deriveEd25519StealthPrivateKeyV1,
248
+ checkEd25519StealthAddressV1,
249
+ deriveSecp256k1StealthPrivateKeyV1,
250
+ checkSecp256k1StealthAddressV1,
251
+ }
252
+
210
253
  // secp256k1 (Ethereum, Polygon, etc.)
211
254
  export { publicKeyToEthAddress }
212
255