@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/dist/browser.js +105 -102
- package/dist/browser.mjs +1 -1
- package/dist/chunk-5EKF243P.mjs +33809 -0
- package/dist/chunk-6EU6WQFK.mjs +33809 -0
- package/dist/index.js +102 -99
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/chains/ethereum/constants.ts +33 -1
- package/src/chains/ethereum/index.ts +2 -1
- package/src/chains/ethereum/privacy-adapter.ts +36 -21
- package/src/chains/ethereum/stealth.ts +74 -23
- package/src/chains/ethereum/types.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sip-protocol/sdk",
|
|
3
|
-
"version": "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": "
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
677
|
-
stealthPrivateKey
|
|
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
|
-
*
|
|
384
|
+
* Check if an Ethereum stealth address matches by ETH address comparison
|
|
382
385
|
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
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
|
|
387
|
-
* @param
|
|
388
|
-
* @param
|
|
389
|
-
* @
|
|
390
|
-
*
|
|
391
|
-
* @
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
}
|