@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
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
|
-
*
|
|
283
|
-
*
|
|
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
|
-
|
|
296
|
-
|
|
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,
|
|
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
|
-
*
|
|
281
|
-
*
|
|
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
|
-
|
|
294
|
-
|
|
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,
|
|
361
|
+
return checkSuiStealthAddress(stealthAddress, viewingPrivateKey, spendingPublicKey)
|
|
356
362
|
}
|
|
357
363
|
|
|
358
364
|
/**
|
package/src/nft/private-nft.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
518
|
+
viewingKeyHex,
|
|
519
|
+
ed25519SpendingPubHex
|
|
516
520
|
)
|
|
517
521
|
} else {
|
|
518
522
|
isOwned = checkStealthAddress(
|
|
519
523
|
transfer.newOwnerStealth,
|
|
520
|
-
|
|
521
|
-
|
|
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,
|
package/src/stealth/ed25519.ts
CHANGED
|
@@ -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 *
|
|
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 =
|
|
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 *
|
|
230
|
-
const
|
|
231
|
-
const sharedSecretPoint =
|
|
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 =
|
|
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
|
|
246
|
-
const
|
|
247
|
-
const stealthPoint =
|
|
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
|
|
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,
|
package/src/stealth/index.ts
CHANGED
|
@@ -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,
|
|
225
|
+
return checkEd25519StealthAddress(stealthAddress, viewingPrivateKey, spendingPublicKey)
|
|
191
226
|
}
|
|
192
227
|
|
|
193
228
|
// Default to secp256k1
|
|
194
|
-
return checkSecp256k1StealthAddress(stealthAddress,
|
|
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
|
|