@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.
- package/LICENSE +21 -0
- package/dist/{TransportWebUSB-YQMAGJAJ.mjs → TransportWebUSB-2KITI5HD.mjs} +24 -12
- package/dist/browser.d.mts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +1346 -838
- package/dist/browser.mjs +13 -3
- package/dist/{chunk-64AYA5F5.mjs → chunk-G3TBBG2K.mjs} +221 -146
- package/dist/{chunk-4GRJ5MAW.mjs → chunk-KXETSSKP.mjs} +4 -0
- package/dist/{chunk-6EU6WQFK.mjs → chunk-PT2DNA7E.mjs} +257 -235
- package/dist/{constants-LHAAUC2T.mjs → constants-DCJYTIU3.mjs} +5 -1
- package/dist/{dist-2OGQ7FED.mjs → dist-PYEXZNFD.mjs} +609 -221
- package/dist/{index-DeE1ZzA4.d.mts → index-B1d8pihL.d.mts} +117 -33
- package/dist/{index-DXh2IGkz.d.ts → index-UQhQJZbM.d.ts} +117 -33
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1339 -831
- package/dist/index.mjs +13 -3
- package/dist/{interface-Bf7w1PLW.d.mts → interface-CQi0-WfS.d.mts} +2 -2
- package/dist/{interface-Bf7w1PLW.d.ts → interface-CQi0-WfS.d.ts} +2 -2
- package/dist/{noir-kzbLVTei.d.mts → noir-CwPIyBLj.d.mts} +1 -1
- package/dist/{noir-kzbLVTei.d.ts → noir-CwPIyBLj.d.ts} +1 -1
- package/dist/proofs/halo2.d.mts +1 -1
- package/dist/proofs/halo2.d.ts +1 -1
- package/dist/proofs/kimchi.d.mts +1 -1
- package/dist/proofs/kimchi.d.ts +1 -1
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/{solana-U3MEGU7W.mjs → solana-ZWNIQTSU.mjs} +6 -6
- package/package.json +32 -32
- package/src/adapters/gelato-relay.ts +386 -0
- package/src/adapters/index.ts +28 -0
- package/src/adapters/oneinch.ts +126 -0
- package/src/chains/ethereum/privacy-adapter.ts +8 -5
- package/src/chains/ethereum/stealth.ts +17 -14
- package/src/chains/near/privacy-adapter.ts +8 -5
- package/src/chains/near/resolver.ts +22 -8
- package/src/chains/near/stealth.ts +9 -9
- package/src/chains/solana/constants.ts +13 -1
- package/src/chains/solana/ephemeral-keys.ts +3 -257
- package/src/chains/solana/index.ts +2 -3
- package/src/chains/solana/providers/helius-enhanced.ts +6 -6
- package/src/chains/solana/providers/webhook.ts +2 -2
- package/src/chains/solana/scan.ts +9 -8
- package/src/chains/solana/stealth-scanner.ts +3 -3
- package/src/chains/solana/types.ts +18 -4
- package/src/cosmos/ibc-stealth.ts +6 -6
- package/src/index.ts +6 -0
- package/src/move/aptos.ts +15 -9
- package/src/move/sui.ts +15 -9
- package/src/nft/private-nft.ts +10 -6
- package/src/privacy-backends/shadowwire.ts +13 -0
- package/src/stealth/ed25519.ts +173 -12
- package/src/stealth/index.ts +47 -4
- package/src/stealth/secp256k1.ts +144 -7
- package/src/stealth.ts +7 -0
- package/src/wallet/ethereum/privacy-adapter.ts +1 -1
- package/src/wallet/hardware/ledger-privacy.ts +2 -2
- package/src/wallet/near/adapter.ts +2 -2
- package/src/wallet/near/meteor-wallet.ts +2 -2
- package/src/wallet/near/my-near-wallet.ts +2 -2
- package/src/wallet/near/wallet-selector.ts +2 -2
- package/src/wallet/solana/privacy-adapter.ts +9 -9
- package/dist/chunk-5EKF243P.mjs +0 -33809
- package/dist/chunk-YWGJ77A2.mjs +0 -33806
|
@@ -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.
|
|
656
|
-
recipient.
|
|
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
|
-
|
|
768
|
-
|
|
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.
|
|
818
|
-
recipient.
|
|
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.
|
|
958
|
-
recipient.
|
|
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
|
-
*
|
|
208
|
-
*
|
|
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
|
-
*
|
|
221
|
-
*
|
|
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
|
-
|
|
232
|
-
|
|
231
|
+
viewingPrivateKey: HexString,
|
|
232
|
+
spendingPublicKey: HexString
|
|
233
233
|
): boolean {
|
|
234
234
|
return checkEd25519StealthAddress(
|
|
235
235
|
stealthAddress,
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
@@ -42,41 +42,6 @@ export interface EphemeralKeypair {
|
|
|
42
42
|
publicKeyBase58: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Result of using an ephemeral keypair for stealth address generation
|
|
47
|
-
*/
|
|
48
|
-
export interface EphemeralKeyUsageResult {
|
|
49
|
-
/**
|
|
50
|
-
* Shared secret derived from ECDH
|
|
51
|
-
*/
|
|
52
|
-
sharedSecret: HexString
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* View tag (first byte of shared secret hash)
|
|
56
|
-
*/
|
|
57
|
-
viewTag: number
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Stealth address (hex, ed25519 format)
|
|
61
|
-
*/
|
|
62
|
-
stealthAddress: HexString
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Stealth address in Solana base58 format
|
|
66
|
-
*/
|
|
67
|
-
stealthAddressBase58: string
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Ephemeral public key used (for announcement)
|
|
71
|
-
*/
|
|
72
|
-
ephemeralPublicKey: HexString
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Ephemeral public key in Solana base58 format
|
|
76
|
-
*/
|
|
77
|
-
ephemeralPublicKeyBase58: string
|
|
78
|
-
}
|
|
79
|
-
|
|
80
45
|
/**
|
|
81
46
|
* Managed ephemeral keypair with automatic secure disposal
|
|
82
47
|
*/
|
|
@@ -91,15 +56,6 @@ export interface ManagedEphemeralKeypair extends EphemeralKeypair {
|
|
|
91
56
|
* Called automatically after use, but can be called manually
|
|
92
57
|
*/
|
|
93
58
|
dispose(): void
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Use this keypair to generate a stealth address
|
|
97
|
-
* Automatically disposes the keypair after use
|
|
98
|
-
*/
|
|
99
|
-
useForStealthAddress(
|
|
100
|
-
recipientSpendingKey: HexString,
|
|
101
|
-
recipientViewingKey: HexString
|
|
102
|
-
): EphemeralKeyUsageResult
|
|
103
59
|
}
|
|
104
60
|
|
|
105
61
|
/**
|
|
@@ -162,13 +118,9 @@ export function generateEphemeralKeypair(): EphemeralKeypair {
|
|
|
162
118
|
* ```typescript
|
|
163
119
|
* const managed = generateManagedEphemeralKeypair()
|
|
164
120
|
*
|
|
165
|
-
* // Use
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* recipientViewingKey
|
|
169
|
-
* )
|
|
170
|
-
*
|
|
171
|
-
* console.log('Stealth address:', result.stealthAddressBase58)
|
|
121
|
+
* // Use the ephemeral key, then securely dispose
|
|
122
|
+
* console.log('Public key:', managed.publicKeyBase58)
|
|
123
|
+
* managed.dispose()
|
|
172
124
|
* console.log('Is disposed:', managed.isDisposed) // true
|
|
173
125
|
* ```
|
|
174
126
|
*/
|
|
@@ -203,33 +155,6 @@ export function generateManagedEphemeralKeypair(): ManagedEphemeralKeypair {
|
|
|
203
155
|
disposed = true
|
|
204
156
|
}
|
|
205
157
|
},
|
|
206
|
-
|
|
207
|
-
useForStealthAddress(
|
|
208
|
-
recipientSpendingKey: HexString,
|
|
209
|
-
recipientViewingKey: HexString
|
|
210
|
-
): EphemeralKeyUsageResult {
|
|
211
|
-
if (disposed) {
|
|
212
|
-
throw new Error('Ephemeral keypair has been disposed')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const result = computeStealthAddress(
|
|
217
|
-
privateKeyBytes,
|
|
218
|
-
publicKeyBytes,
|
|
219
|
-
recipientSpendingKey,
|
|
220
|
-
recipientViewingKey
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
...result,
|
|
225
|
-
ephemeralPublicKey: publicKeyHex,
|
|
226
|
-
ephemeralPublicKeyBase58: publicKeyBase58,
|
|
227
|
-
}
|
|
228
|
-
} finally {
|
|
229
|
-
// Always dispose after use
|
|
230
|
-
managed.dispose()
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
158
|
}
|
|
234
159
|
|
|
235
160
|
return managed
|
|
@@ -362,182 +287,3 @@ export function wipeEphemeralPrivateKey(privateKeyHex: HexString): void {
|
|
|
362
287
|
const bytes = hexToBytes(privateKeyHex.slice(2))
|
|
363
288
|
secureWipe(bytes)
|
|
364
289
|
}
|
|
365
|
-
|
|
366
|
-
// ─── Internal Helpers ─────────────────────────────────────────────────────────
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* ed25519 group order (L)
|
|
370
|
-
*/
|
|
371
|
-
const ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Convert bytes to bigint (little-endian for ed25519)
|
|
375
|
-
*/
|
|
376
|
-
function bytesToBigIntLE(bytes: Uint8Array): bigint {
|
|
377
|
-
let result = 0n
|
|
378
|
-
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
379
|
-
result = (result << 8n) | BigInt(bytes[i])
|
|
380
|
-
}
|
|
381
|
-
return result
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Get ed25519 scalar from private key bytes
|
|
386
|
-
* Follows standard ed25519 scalar clamping
|
|
387
|
-
*/
|
|
388
|
-
function getEd25519Scalar(privateKey: Uint8Array): bigint {
|
|
389
|
-
const hash = sha256(privateKey)
|
|
390
|
-
// Clamp to valid scalar
|
|
391
|
-
hash[0] &= 248
|
|
392
|
-
hash[31] &= 127
|
|
393
|
-
hash[31] |= 64
|
|
394
|
-
return bytesToBigIntLE(hash.slice(0, 32))
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Compute stealth address from ephemeral keypair and recipient keys
|
|
399
|
-
*/
|
|
400
|
-
function computeStealthAddress(
|
|
401
|
-
ephemeralPrivateBytes: Uint8Array,
|
|
402
|
-
_ephemeralPublicBytes: Uint8Array, // Reserved for future validation
|
|
403
|
-
recipientSpendingKey: HexString,
|
|
404
|
-
recipientViewingKey: HexString
|
|
405
|
-
): Omit<EphemeralKeyUsageResult, 'ephemeralPublicKey' | 'ephemeralPublicKeyBase58'> {
|
|
406
|
-
// Parse recipient keys
|
|
407
|
-
const spendingKeyBytes = hexToBytes(recipientSpendingKey.slice(2))
|
|
408
|
-
const viewingKeyBytes = hexToBytes(recipientViewingKey.slice(2))
|
|
409
|
-
|
|
410
|
-
// Get ephemeral scalar
|
|
411
|
-
const rawEphemeralScalar = getEd25519Scalar(ephemeralPrivateBytes)
|
|
412
|
-
const ephemeralScalar = rawEphemeralScalar % ED25519_ORDER
|
|
413
|
-
if (ephemeralScalar === 0n) {
|
|
414
|
-
throw new Error('Invalid ephemeral scalar')
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Compute shared secret: S = ephemeral_scalar * P_spend
|
|
418
|
-
const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes)
|
|
419
|
-
const sharedSecretPoint = spendingPoint.multiply(ephemeralScalar)
|
|
420
|
-
const sharedSecretBytes = sharedSecretPoint.toRawBytes()
|
|
421
|
-
|
|
422
|
-
// Hash the shared secret
|
|
423
|
-
const sharedSecretHash = sha256(sharedSecretBytes)
|
|
424
|
-
const viewTag = sharedSecretHash[0]
|
|
425
|
-
|
|
426
|
-
// Derive stealth address: P_stealth = P_view + hash(S)*G
|
|
427
|
-
const hashScalar = bytesToBigIntLE(sharedSecretHash) % ED25519_ORDER
|
|
428
|
-
if (hashScalar === 0n) {
|
|
429
|
-
throw new Error('Invalid hash scalar')
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar)
|
|
433
|
-
const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes)
|
|
434
|
-
const stealthPoint = viewingPoint.add(hashTimesG)
|
|
435
|
-
const stealthAddressBytes = stealthPoint.toRawBytes()
|
|
436
|
-
|
|
437
|
-
const stealthAddress = `0x${bytesToHex(stealthAddressBytes)}` as HexString
|
|
438
|
-
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress)
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
sharedSecret: `0x${bytesToHex(sharedSecretHash)}` as HexString,
|
|
442
|
-
viewTag,
|
|
443
|
-
stealthAddress,
|
|
444
|
-
stealthAddressBase58,
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ─── Announcement Format ──────────────────────────────────────────────────────
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Format ephemeral key data for Solana memo announcement
|
|
452
|
-
*
|
|
453
|
-
* @param ephemeralPublicKeyBase58 - Ephemeral public key in base58
|
|
454
|
-
* @param viewTag - View tag (0-255)
|
|
455
|
-
* @param stealthAddressBase58 - Optional stealth address for verification
|
|
456
|
-
* @returns Formatted announcement string
|
|
457
|
-
*
|
|
458
|
-
* @example
|
|
459
|
-
* ```typescript
|
|
460
|
-
* const memo = formatEphemeralAnnouncement(
|
|
461
|
-
* result.ephemeralPublicKeyBase58,
|
|
462
|
-
* result.viewTag,
|
|
463
|
-
* result.stealthAddressBase58
|
|
464
|
-
* )
|
|
465
|
-
* // "SIP:1:7xK9...:0a:8yL0..."
|
|
466
|
-
* ```
|
|
467
|
-
*/
|
|
468
|
-
export function formatEphemeralAnnouncement(
|
|
469
|
-
ephemeralPublicKeyBase58: string,
|
|
470
|
-
viewTag: number,
|
|
471
|
-
stealthAddressBase58?: string
|
|
472
|
-
): string {
|
|
473
|
-
const viewTagHex = viewTag.toString(16).padStart(2, '0')
|
|
474
|
-
const parts = ['SIP:1', ephemeralPublicKeyBase58, viewTagHex]
|
|
475
|
-
|
|
476
|
-
if (stealthAddressBase58) {
|
|
477
|
-
parts.push(stealthAddressBase58)
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return parts.join(':')
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Parse ephemeral key data from Solana memo announcement
|
|
485
|
-
*
|
|
486
|
-
* @param announcement - Announcement string from memo
|
|
487
|
-
* @returns Parsed ephemeral data or null if invalid
|
|
488
|
-
*
|
|
489
|
-
* @example
|
|
490
|
-
* ```typescript
|
|
491
|
-
* const parsed = parseEphemeralAnnouncement('SIP:1:7xK9...:0a:8yL0...')
|
|
492
|
-
* if (parsed) {
|
|
493
|
-
* console.log('Ephemeral key:', parsed.ephemeralPublicKeyBase58)
|
|
494
|
-
* console.log('View tag:', parsed.viewTag)
|
|
495
|
-
* }
|
|
496
|
-
* ```
|
|
497
|
-
*/
|
|
498
|
-
export function parseEphemeralAnnouncement(
|
|
499
|
-
announcement: string
|
|
500
|
-
): {
|
|
501
|
-
ephemeralPublicKeyBase58: string
|
|
502
|
-
viewTag: number
|
|
503
|
-
stealthAddressBase58?: string
|
|
504
|
-
} | null {
|
|
505
|
-
if (!announcement.startsWith('SIP:1:')) {
|
|
506
|
-
return null
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const parts = announcement.slice(6).split(':')
|
|
510
|
-
if (parts.length < 2) {
|
|
511
|
-
return null
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const ephemeralPublicKeyBase58 = parts[0]
|
|
515
|
-
const viewTagHex = parts[1]
|
|
516
|
-
const stealthAddressBase58 = parts[2]
|
|
517
|
-
|
|
518
|
-
// Validate ephemeral key (base58, 32-44 chars)
|
|
519
|
-
if (!ephemeralPublicKeyBase58 || ephemeralPublicKeyBase58.length < 32 || ephemeralPublicKeyBase58.length > 44) {
|
|
520
|
-
return null
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Validate view tag (1-2 hex chars)
|
|
524
|
-
if (!viewTagHex || viewTagHex.length > 2 || !/^[0-9a-fA-F]+$/.test(viewTagHex)) {
|
|
525
|
-
return null
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const viewTag = parseInt(viewTagHex, 16)
|
|
529
|
-
if (viewTag < 0 || viewTag > 255) {
|
|
530
|
-
return null
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Validate stealth address if present
|
|
534
|
-
if (stealthAddressBase58 && (stealthAddressBase58.length < 32 || stealthAddressBase58.length > 44)) {
|
|
535
|
-
return null
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return {
|
|
539
|
-
ephemeralPublicKeyBase58,
|
|
540
|
-
viewTag,
|
|
541
|
-
stealthAddressBase58,
|
|
542
|
-
}
|
|
543
|
-
}
|
|
@@ -52,6 +52,8 @@ export {
|
|
|
52
52
|
SOLANA_EXPLORER_URLS,
|
|
53
53
|
MEMO_PROGRAM_ID,
|
|
54
54
|
SIP_MEMO_PREFIX,
|
|
55
|
+
SIP_MEMO_PREFIX_V2,
|
|
56
|
+
SIP_MEMO_PREFIX_ANY,
|
|
55
57
|
ESTIMATED_TX_FEE_LAMPORTS,
|
|
56
58
|
ATA_RENT_LAMPORTS,
|
|
57
59
|
getExplorerUrl,
|
|
@@ -150,10 +152,7 @@ export {
|
|
|
150
152
|
batchGenerateManagedEphemeralKeypairs,
|
|
151
153
|
disposeEphemeralKeypairs,
|
|
152
154
|
wipeEphemeralPrivateKey,
|
|
153
|
-
formatEphemeralAnnouncement,
|
|
154
|
-
parseEphemeralAnnouncement,
|
|
155
155
|
type EphemeralKeypair,
|
|
156
|
-
type EphemeralKeyUsageResult,
|
|
157
156
|
type ManagedEphemeralKeypair,
|
|
158
157
|
type BatchGenerationOptions,
|
|
159
158
|
} from './ephemeral-keys'
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
SOLANA_ADDRESS_MAX_LENGTH,
|
|
39
39
|
HELIUS_API_KEY_MIN_LENGTH,
|
|
40
40
|
sanitizeUrl,
|
|
41
|
-
|
|
41
|
+
SIP_MEMO_PREFIX_ANY,
|
|
42
42
|
getExplorerUrl,
|
|
43
43
|
} from '../constants'
|
|
44
44
|
import type {
|
|
@@ -324,7 +324,7 @@ export class HeliusEnhanced {
|
|
|
324
324
|
* Extract SIP metadata from a transaction
|
|
325
325
|
*
|
|
326
326
|
* Parses memo program instructions to find SIP announcements.
|
|
327
|
-
* SIP memo format: SIP
|
|
327
|
+
* SIP memo format: SIP:<version>:<ephemeral_pubkey_base58>:<view_tag_hex>
|
|
328
328
|
*
|
|
329
329
|
* @param tx - Enhanced transaction
|
|
330
330
|
* @returns SIP metadata if found
|
|
@@ -339,13 +339,13 @@ export class HeliusEnhanced {
|
|
|
339
339
|
const description = tx.description || ''
|
|
340
340
|
|
|
341
341
|
// Check if this looks like a SIP transaction
|
|
342
|
-
// SIP transactions have a memo with format: SIP
|
|
343
|
-
if (description.includes(
|
|
342
|
+
// SIP transactions have a memo with format: SIP:<version>:<ephemeral_pubkey>:<view_tag>
|
|
343
|
+
if (description.includes(SIP_MEMO_PREFIX_ANY)) {
|
|
344
344
|
metadata.isSIPTransaction = true
|
|
345
345
|
|
|
346
346
|
// Try to extract SIP memo data
|
|
347
|
-
// The memo format is: SIP
|
|
348
|
-
const sipMemoMatch = description.match(/SIP:
|
|
347
|
+
// The memo format is: SIP:<version>:<ephemeral_pubkey_base58>:<view_tag_hex>
|
|
348
|
+
const sipMemoMatch = description.match(/SIP:[12]:([A-Za-z0-9]{32,44}):([0-9a-fA-F]{1,2})/)
|
|
349
349
|
if (sipMemoMatch) {
|
|
350
350
|
metadata.ephemeralPubKey = sipMemoMatch[1]
|
|
351
351
|
metadata.viewTag = parseInt(sipMemoMatch[2], 16)
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
import type { StealthAddress } from '@sip-protocol/types'
|
|
47
47
|
import { parseAnnouncement } from '../types'
|
|
48
48
|
import type { SolanaScanResult } from '../types'
|
|
49
|
-
import {
|
|
49
|
+
import { SIP_MEMO_PREFIX_ANY } from '../constants'
|
|
50
50
|
import { getTokenSymbol, parseTokenTransferFromBalances } from '../utils'
|
|
51
51
|
import { ValidationError, SecurityError } from '../../../errors'
|
|
52
52
|
import { hmac } from '@noble/hashes/hmac'
|
|
@@ -594,7 +594,7 @@ async function processRawTransaction(
|
|
|
594
594
|
|
|
595
595
|
// Search log messages for SIP announcement
|
|
596
596
|
for (const log of tx.meta.logMessages) {
|
|
597
|
-
if (!log.includes(
|
|
597
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
598
598
|
|
|
599
599
|
// Extract memo content from log
|
|
600
600
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
checkEd25519StealthAddress,
|
|
19
19
|
deriveEd25519StealthPrivateKey,
|
|
20
|
+
deriveEd25519StealthPrivateKeyV1,
|
|
20
21
|
solanaAddressToEd25519PublicKey,
|
|
21
22
|
} from '../../stealth'
|
|
22
23
|
import type { StealthAddress, HexString } from '@sip-protocol/types'
|
|
@@ -28,7 +29,7 @@ import type {
|
|
|
28
29
|
} from './types'
|
|
29
30
|
import { parseAnnouncement } from './types'
|
|
30
31
|
import {
|
|
31
|
-
|
|
32
|
+
SIP_MEMO_PREFIX_ANY,
|
|
32
33
|
MEMO_PROGRAM_ID,
|
|
33
34
|
getExplorerUrl,
|
|
34
35
|
DEFAULT_SCAN_LIMIT,
|
|
@@ -111,7 +112,7 @@ export async function scanForPayments(
|
|
|
111
112
|
|
|
112
113
|
// Look for SIP announcement in logs
|
|
113
114
|
for (const log of tx.meta.logMessages) {
|
|
114
|
-
if (!log.includes(
|
|
115
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
115
116
|
|
|
116
117
|
// Extract memo content from log
|
|
117
118
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
@@ -261,6 +262,7 @@ export async function claimStealthPayment(
|
|
|
261
262
|
spendingPrivateKey,
|
|
262
263
|
destinationAddress,
|
|
263
264
|
mint,
|
|
265
|
+
version = '2',
|
|
264
266
|
} = params
|
|
265
267
|
|
|
266
268
|
// M7 FIX: Check SOL balance for fees before attempting claim
|
|
@@ -285,12 +287,11 @@ export async function claimStealthPayment(
|
|
|
285
287
|
viewTag: 0, // Not needed for derivation
|
|
286
288
|
}
|
|
287
289
|
|
|
288
|
-
// Derive stealth private key
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
spendingPrivateKey,
|
|
292
|
-
viewingPrivateKey
|
|
293
|
-
)
|
|
290
|
+
// Derive stealth private key — route by announcement version:
|
|
291
|
+
// legacy SIP:1 (swapped scheme) vs canonical SIP:2 (EIP-5564).
|
|
292
|
+
const recovery = version === '1'
|
|
293
|
+
? deriveEd25519StealthPrivateKeyV1(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
|
|
294
|
+
: deriveEd25519StealthPrivateKey(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
|
|
294
295
|
|
|
295
296
|
// Create Solana keypair from derived private key
|
|
296
297
|
// Note: ed25519 private keys in Solana are seeds, not raw scalars
|
|
@@ -17,7 +17,7 @@ import type { StealthAddress, HexString } from '@sip-protocol/types'
|
|
|
17
17
|
import { checkEd25519StealthAddress, solanaAddressToEd25519PublicKey } from '../../stealth'
|
|
18
18
|
import { parseAnnouncement, type SolanaAnnouncement } from './types'
|
|
19
19
|
import {
|
|
20
|
-
|
|
20
|
+
SIP_MEMO_PREFIX_ANY,
|
|
21
21
|
MEMO_PROGRAM_ID,
|
|
22
22
|
DEFAULT_SCAN_LIMIT,
|
|
23
23
|
VIEW_TAG_MAX,
|
|
@@ -354,7 +354,7 @@ export class StealthScanner {
|
|
|
354
354
|
|
|
355
355
|
// Look for SIP announcements in logs
|
|
356
356
|
for (const log of tx.meta.logMessages) {
|
|
357
|
-
if (!log.includes(
|
|
357
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
358
358
|
|
|
359
359
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
360
360
|
if (!memoMatch) continue
|
|
@@ -421,7 +421,7 @@ export class StealthScanner {
|
|
|
421
421
|
try {
|
|
422
422
|
// Look for SIP announcements
|
|
423
423
|
for (const log of logs.logs) {
|
|
424
|
-
if (!log.includes(
|
|
424
|
+
if (!log.includes(SIP_MEMO_PREFIX_ANY)) continue
|
|
425
425
|
|
|
426
426
|
const memoMatch = log.match(/Program log: (.+)/)
|
|
427
427
|
if (!memoMatch) continue
|
|
@@ -147,6 +147,12 @@ export interface SolanaClaimParams {
|
|
|
147
147
|
destinationAddress: string
|
|
148
148
|
/** SPL token mint address */
|
|
149
149
|
mint: PublicKey
|
|
150
|
+
/**
|
|
151
|
+
* Announcement scheme version of the payment being claimed.
|
|
152
|
+
* '2' (default) = canonical EIP-5564; '1' = legacy swapped (SIP:1) back-compat.
|
|
153
|
+
* Pass the `version` returned by {@link parseAnnouncement}.
|
|
154
|
+
*/
|
|
155
|
+
version?: '1' | '2'
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
/**
|
|
@@ -167,6 +173,8 @@ export interface SolanaClaimResult {
|
|
|
167
173
|
* Announcement data stored in transaction memo
|
|
168
174
|
*/
|
|
169
175
|
export interface SolanaAnnouncement {
|
|
176
|
+
/** Announcement scheme version: '1' = legacy swapped, '2' = canonical EIP-5564 */
|
|
177
|
+
version: string
|
|
170
178
|
/** Ephemeral public key (base58) */
|
|
171
179
|
ephemeralPublicKey: string
|
|
172
180
|
/** View tag for efficient scanning (hex, 1 byte) */
|
|
@@ -177,16 +185,21 @@ export interface SolanaAnnouncement {
|
|
|
177
185
|
|
|
178
186
|
/**
|
|
179
187
|
* Parse announcement from memo string
|
|
180
|
-
* Format: SIP
|
|
188
|
+
* Format: SIP:<version>:<ephemeral_pubkey_base58>:<view_tag_hex>[:<stealth_address_base58>]
|
|
189
|
+
*
|
|
190
|
+
* Accepts SIP:1 (legacy swapped scheme) and SIP:2 (canonical EIP-5564); the
|
|
191
|
+
* detected version is returned so the claim path can route to the matching derivation.
|
|
181
192
|
*
|
|
182
193
|
* M4 FIX: Validates view tag is exactly 1-2 hex characters (1 byte)
|
|
183
194
|
*/
|
|
184
195
|
export function parseAnnouncement(memo: string): SolanaAnnouncement | null {
|
|
185
|
-
|
|
196
|
+
const versionMatch = /^SIP:([12]):/.exec(memo)
|
|
197
|
+
if (!versionMatch) {
|
|
186
198
|
return null
|
|
187
199
|
}
|
|
200
|
+
const version = versionMatch[1]
|
|
188
201
|
|
|
189
|
-
const parts = memo.slice(
|
|
202
|
+
const parts = memo.slice(versionMatch[0].length).split(':')
|
|
190
203
|
if (parts.length < 2) {
|
|
191
204
|
return null
|
|
192
205
|
}
|
|
@@ -217,6 +230,7 @@ export function parseAnnouncement(memo: string): SolanaAnnouncement | null {
|
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
return {
|
|
233
|
+
version,
|
|
220
234
|
ephemeralPublicKey,
|
|
221
235
|
viewTag,
|
|
222
236
|
stealthAddress,
|
|
@@ -231,7 +245,7 @@ export function createAnnouncementMemo(
|
|
|
231
245
|
viewTag: string,
|
|
232
246
|
stealthAddress?: string
|
|
233
247
|
): string {
|
|
234
|
-
const parts = ['SIP:
|
|
248
|
+
const parts = ['SIP:2', ephemeralPublicKey, viewTag]
|
|
235
249
|
if (stealthAddress) {
|
|
236
250
|
parts.push(stealthAddress)
|
|
237
251
|
}
|
|
@@ -497,15 +497,15 @@ export class CosmosIBCStealthService {
|
|
|
497
497
|
const viewingPrivKey = hexToBytes(`0x${bytesToHex(viewingKey)}`.slice(2))
|
|
498
498
|
const spendingPrivKey = hexToBytes(spendingPrivateKey.slice(2))
|
|
499
499
|
|
|
500
|
-
// Compute stealth private key using EIP-5564
|
|
501
|
-
// 1. Compute shared secret: S =
|
|
502
|
-
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
500
|
+
// Compute stealth private key using canonical EIP-5564:
|
|
501
|
+
// 1. Compute shared secret: S = viewingPriv * ephemeralPub (ECDH on the viewing key)
|
|
502
|
+
const sharedSecretPoint = secp256k1.getSharedSecret(viewingPrivKey, ephemeralPubKey)
|
|
503
503
|
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
504
504
|
|
|
505
|
-
// 2. Derive stealth private key: stealthPriv =
|
|
506
|
-
const
|
|
505
|
+
// 2. Derive stealth private key: stealthPriv = spendingPriv + hash(S) mod n
|
|
506
|
+
const spendingPrivBigInt = bytesToBigInt(spendingPrivKey)
|
|
507
507
|
const hashBigInt = bytesToBigInt(sharedSecretHash)
|
|
508
|
-
const stealthPrivBigInt = (
|
|
508
|
+
const stealthPrivBigInt = (spendingPrivBigInt + hashBigInt) % secp256k1.CURVE.n
|
|
509
509
|
|
|
510
510
|
// Convert to bytes and then to compressed public key
|
|
511
511
|
const stealthPrivKey = bigIntToBytes(stealthPrivBigInt, 32)
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,12 @@ export {
|
|
|
88
88
|
generateEd25519StealthAddress,
|
|
89
89
|
deriveEd25519StealthPrivateKey,
|
|
90
90
|
checkEd25519StealthAddress,
|
|
91
|
+
// Legacy SIP:1 back-compat (claim/scan of pre-flip announcements)
|
|
92
|
+
deriveStealthPrivateKeyV1,
|
|
93
|
+
deriveEd25519StealthPrivateKeyV1,
|
|
94
|
+
checkEd25519StealthAddressV1,
|
|
95
|
+
deriveSecp256k1StealthPrivateKeyV1,
|
|
96
|
+
checkSecp256k1StealthAddressV1,
|
|
91
97
|
// Solana address derivation
|
|
92
98
|
ed25519PublicKeyToSolanaAddress,
|
|
93
99
|
solanaAddressToEd25519PublicKey,
|