@sip-protocol/sdk 0.8.0 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sip-protocol/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Core SDK for Shielded Intents Protocol - Privacy layer for cross-chain transactions",
5
5
  "author": "SIP Protocol <hello@sip-protocol.org>",
6
6
  "homepage": "https://sip-protocol.org",
@@ -78,7 +78,7 @@
78
78
  "@scure/base": "^2.0.0",
79
79
  "@scure/bip32": "^2.0.1",
80
80
  "@scure/bip39": "^2.0.1",
81
- "@sip-protocol/types": "workspace:*",
81
+ "@sip-protocol/types": "^0.2.2",
82
82
  "@solana-program/compute-budget": "^0.11.0",
83
83
  "@solana-program/system": "^0.10.0",
84
84
  "@solana/compat": "^5.4.0",
@@ -129,7 +129,7 @@ export const ETHEREUM_TOKEN_CONTRACTS = {
129
129
  /** Tether USD */
130
130
  USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
131
131
  /** Dai Stablecoin */
132
- DAI: '0x6B175474E89094C44Da98b954EescdeCB5bE3d830',
132
+ DAI: '0x6B175474E89094C44Da98b954EeDeAC495271d0F',
133
133
  /** Chainlink */
134
134
  LINK: '0x514910771AF9Ca656af840dff83E8264EcF986CA',
135
135
  /** Uniswap */
@@ -237,6 +237,38 @@ export const EIP5564_ANNOUNCER_ADDRESS = '0x55649E01B5Df198D18D95b5cc5051630cfD4
237
237
  */
238
238
  export const EIP5564_REGISTRY_ADDRESS = '0x6538E6bf4B0eBd30A8Ea10e318b7AEb51A8E4b5c'
239
239
 
240
+ // ─── SIP Deployed Contract Addresses ──────────────────────────────────────────
241
+
242
+ /**
243
+ * SIP Privacy contract addresses per network
244
+ * Deployed via Foundry Deploy.s.sol
245
+ */
246
+ export const SIP_CONTRACT_ADDRESSES: Partial<Record<EthereumNetwork, {
247
+ sipPrivacy: string
248
+ pedersenVerifier: string
249
+ zkVerifier: string
250
+ stealthAddressRegistry: string
251
+ }>> = {
252
+ sepolia: {
253
+ sipPrivacy: '0x0B0d06D6B5136d63Bd0817414E2D318999e50339',
254
+ pedersenVerifier: '0xEB14E9022A4c3DEED072DeC6b3858c19a00C87Db',
255
+ zkVerifier: '0x26988D988684627084e6ae113e0354f6bc56b126',
256
+ stealthAddressRegistry: '0x1f7f3edD264Cf255dD99Fd433eD9FADE427dEF99',
257
+ },
258
+ 'optimism-sepolia': {
259
+ sipPrivacy: '0x0B0d06D6B5136d63Bd0817414E2D318999e50339',
260
+ pedersenVerifier: '0xEB14E9022A4c3DEED072DeC6b3858c19a00C87Db',
261
+ zkVerifier: '0x26988D988684627084e6ae113e0354f6bc56b126',
262
+ stealthAddressRegistry: '0x1f7f3edD264Cf255dD99Fd433eD9FADE427dEF99',
263
+ },
264
+ 'base-sepolia': {
265
+ sipPrivacy: '0x0B0d06D6B5136d63Bd0817414E2D318999e50339',
266
+ pedersenVerifier: '0xEB14E9022A4c3DEED072DeC6b3858c19a00C87Db',
267
+ zkVerifier: '0x26988D988684627084e6ae113e0354f6bc56b126',
268
+ stealthAddressRegistry: '0x1f7f3edD264Cf255dD99Fd433eD9FADE427dEF99',
269
+ },
270
+ }
271
+
240
272
  /**
241
273
  * SIP announcement event signature (for log filtering)
242
274
  * Announcement(uint256 indexed schemeId, address indexed stealthAddress, address indexed caller, bytes ephemeralPubKey, bytes metadata)
@@ -39,6 +39,7 @@ export {
39
39
  isL2Network,
40
40
  isValidEthAddress,
41
41
  sanitizeUrl,
42
+ SIP_CONTRACT_ADDRESSES,
42
43
  } from './constants'
43
44
 
44
45
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -83,7 +84,7 @@ export {
83
84
  generateEthereumStealthAddress,
84
85
  deriveEthereumStealthPrivateKey,
85
86
  checkEthereumStealthAddress,
86
- checkViewTag,
87
+ checkEthereumStealthByEthAddress,
87
88
  stealthPublicKeyToEthAddress,
88
89
  extractPublicKeys,
89
90
  createMetaAddressFromPublicKeys,
@@ -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'
@@ -582,20 +586,17 @@ export class EthereumPrivacyAdapter {
582
586
 
583
587
  // Check each recipient
584
588
  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
589
+ // Use ETH address comparison since announcements store 20-byte addresses
590
+ // Returns the stealth private key if match found, null otherwise
591
+ const stealthPrivateKey = checkEthereumStealthByEthAddress(
592
+ announcement.stealthAddress,
593
+ announcement.ephemeralPublicKey,
594
+ announcement.viewTag,
595
+ recipient.spendingPrivateKey,
596
+ recipient.viewingPrivateKey,
589
597
  )
590
598
 
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
-
599
+ if (stealthPrivateKey) {
599
600
  results.push({
600
601
  payment: {
601
602
  stealthAddress,
@@ -606,7 +607,7 @@ export class EthereumPrivacyAdapter {
606
607
  timestamp: announcement.timestamp,
607
608
  },
608
609
  recipient,
609
- stealthPrivateKey: recovery.privateKey,
610
+ stealthPrivateKey,
610
611
  })
611
612
  break // Found owner, no need to check other recipients
612
613
  }
@@ -641,12 +642,26 @@ export class EthereumPrivacyAdapter {
641
642
  * @returns Built claim transaction
642
643
  */
643
644
  buildClaimTransaction(params: EthereumClaimParams): EthereumClaimBuild {
644
- // Derive stealth private key
645
- const recovery = deriveEthereumStealthPrivateKey(
646
- params.stealthAddress,
647
- params.spendingPrivateKey,
648
- params.viewingPrivateKey
649
- )
645
+ // Use pre-derived key if available (e.g. from scanning flow), otherwise derive
646
+ let stealthPrivateKey: HexString
647
+ let stealthEthAddress: HexString
648
+
649
+ if (params.stealthPrivateKey) {
650
+ stealthPrivateKey = params.stealthPrivateKey
651
+ stealthEthAddress = stealthPublicKeyToEthAddress(
652
+ ('0x' + bytesToHex(
653
+ secp256k1.getPublicKey(hexToBytes(params.stealthPrivateKey.slice(2)), true)
654
+ )) as HexString
655
+ )
656
+ } else {
657
+ const recovery = deriveEthereumStealthPrivateKey(
658
+ params.stealthAddress,
659
+ params.spendingPrivateKey,
660
+ params.viewingPrivateKey
661
+ )
662
+ stealthPrivateKey = recovery.privateKey
663
+ stealthEthAddress = recovery.ethAddress
664
+ }
650
665
 
651
666
  // Build transaction
652
667
  const amount = params.amount ?? 0n // Full balance if not specified
@@ -673,8 +688,8 @@ export class EthereumPrivacyAdapter {
673
688
  }
674
689
 
675
690
  return {
676
- stealthEthAddress: recovery.ethAddress,
677
- stealthPrivateKey: recovery.privateKey,
691
+ stealthEthAddress,
692
+ stealthPrivateKey,
678
693
  destinationAddress: params.destinationAddress,
679
694
  amount,
680
695
  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
 
@@ -378,32 +381,80 @@ export function checkEthereumStealthAddress(
378
381
  }
379
382
 
380
383
  /**
381
- * Quick view tag check for efficient scanning
384
+ * Check if an Ethereum stealth address matches by ETH address comparison
382
385
  *
383
- * Before doing the full elliptic curve check, verify the view tag matches.
384
- * This is ~256x faster for non-matching addresses.
386
+ * Used when the announcement only contains the 20-byte ETH address (not the
387
+ * full 33-byte compressed public key). Derives the expected stealth public key,
388
+ * converts it to an ETH address, and compares.
385
389
  *
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)
390
- *
391
- * @deprecated Use checkEthereumStealthAddress instead which handles everything
390
+ * @param ethAddress - The ETH address from the announcement (20 bytes)
391
+ * @param ephemeralPublicKey - Ephemeral public key from the announcement
392
+ * @param viewTag - View tag from the announcement
393
+ * @param spendingPublicKey - Recipient's spending public key
394
+ * @param viewingPrivateKey - Recipient's viewing private key
395
+ * @returns True if the address belongs to this recipient
392
396
  */
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
397
+ export function checkEthereumStealthByEthAddress(
398
+ ethAddress: HexString,
399
+ ephemeralPublicKey: HexString,
400
+ viewTag: number,
401
+ spendingPrivateKey: HexString,
402
+ viewingPrivateKey: HexString,
403
+ ): HexString | null {
404
+ if (!isValidPrivateKey(spendingPrivateKey)) {
405
+ throw new ValidationError(
406
+ 'must be a valid 32-byte hex string',
407
+ 'spendingPrivateKey'
408
+ )
409
+ }
410
+ if (!isValidPrivateKey(viewingPrivateKey)) {
411
+ throw new ValidationError(
412
+ 'must be a valid 32-byte hex string',
413
+ 'viewingPrivateKey'
414
+ )
415
+ }
416
+
417
+ const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
418
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
419
+ const ephemeralPubBytes = hexToBytes(ephemeralPublicKey.slice(2))
420
+
421
+ try {
422
+ // Compute shared secret: S = spendingPrivateKey * ephemeralPublicKey
423
+ // Mirrors generation: S = ephemeralPrivate * spendingPublic
424
+ const sharedSecretPoint = secp256k1.getSharedSecret(
425
+ spendingPrivBytes,
426
+ ephemeralPubBytes,
427
+ )
428
+ const sharedSecretHash = sha256(sharedSecretPoint)
429
+
430
+ // Quick view tag check
431
+ if (sharedSecretHash[0] !== viewTag) {
432
+ return null
433
+ }
434
+
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
+ const hashScalar = BigInt('0x' + bytesToHex(sharedSecretHash))
439
+ const stealthPrivScalar = (viewingScalar + hashScalar) % secp256k1.CURVE.n
440
+
441
+ // Compute expected public key from derived private key
442
+ const stealthPrivHex = stealthPrivScalar.toString(16).padStart(64, '0')
443
+ const stealthPrivKeyBytes = hexToBytes(stealthPrivHex)
444
+ const expectedPubKey = secp256k1.getPublicKey(stealthPrivKeyBytes, true)
445
+
446
+ // Convert to ETH address
447
+ const expectedPubKeyHex = ('0x' + bytesToHex(expectedPubKey)) as HexString
448
+ const expectedEthAddress = publicKeyToEthAddress(expectedPubKeyHex)
449
+
450
+ // Compare addresses (case-insensitive)
451
+ if (expectedEthAddress.toLowerCase() === ethAddress.toLowerCase()) {
452
+ return ('0x' + stealthPrivHex) as HexString
453
+ }
454
+ return null
455
+ } catch {
456
+ return null
457
+ }
407
458
  }
408
459
 
409
460
  // ─── 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
  }