@sip-protocol/sdk 0.8.1 → 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 +1358 -847
- 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-YWGJ77A2.mjs → chunk-PT2DNA7E.mjs} +335 -310
- 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 +1348 -837
- 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/constants.ts +33 -1
- package/src/chains/ethereum/index.ts +2 -1
- package/src/chains/ethereum/privacy-adapter.ts +44 -26
- package/src/chains/ethereum/stealth.ts +84 -30
- package/src/chains/ethereum/types.ts +4 -0
- 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/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
|
|
package/src/stealth/secp256k1.ts
CHANGED
|
@@ -163,21 +163,22 @@ export function generateSecp256k1StealthAddress(
|
|
|
163
163
|
const spendingKeyBytes = hexToBytes(recipientMetaAddress.spendingKey.slice(2))
|
|
164
164
|
const viewingKeyBytes = hexToBytes(recipientMetaAddress.viewingKey.slice(2))
|
|
165
165
|
|
|
166
|
-
// Compute shared secret: S = r *
|
|
166
|
+
// Compute shared secret: S = r * K_view (ephemeral private * viewing public)
|
|
167
|
+
// Canonical EIP-5564: ECDH is on the VIEWING key.
|
|
167
168
|
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
168
169
|
ephemeralPrivateKey,
|
|
169
|
-
|
|
170
|
+
viewingKeyBytes,
|
|
170
171
|
)
|
|
171
172
|
|
|
172
173
|
// Hash the shared secret for use as a scalar
|
|
173
174
|
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
174
175
|
|
|
175
|
-
// Compute stealth address: A =
|
|
176
|
+
// Compute stealth address: A = K_spend + hash(S)*G
|
|
176
177
|
const hashTimesG = secp256k1.getPublicKey(sharedSecretHash, true)
|
|
177
178
|
|
|
178
|
-
const
|
|
179
|
+
const spendingKeyPoint = secp256k1.ProjectivePoint.fromHex(spendingKeyBytes)
|
|
179
180
|
const hashTimesGPoint = secp256k1.ProjectivePoint.fromHex(hashTimesG)
|
|
180
|
-
const stealthPoint =
|
|
181
|
+
const stealthPoint = spendingKeyPoint.add(hashTimesGPoint)
|
|
181
182
|
const stealthAddressBytes = stealthPoint.toRawBytes(true)
|
|
182
183
|
|
|
183
184
|
// Compute view tag (first byte of hash for efficient scanning)
|
|
@@ -199,7 +200,9 @@ export function generateSecp256k1StealthAddress(
|
|
|
199
200
|
// ─── Private Key Derivation ─────────────────────────────────────────────────
|
|
200
201
|
|
|
201
202
|
/**
|
|
202
|
-
* Derive the private key for a secp256k1 stealth address
|
|
203
|
+
* Derive the private key for a secp256k1 stealth address (canonical EIP-5564)
|
|
204
|
+
*
|
|
205
|
+
* Requires BOTH the spending and viewing private keys (spending authority).
|
|
203
206
|
*/
|
|
204
207
|
export function deriveSecp256k1StealthPrivateKey(
|
|
205
208
|
stealthAddress: StealthAddress,
|
|
@@ -226,6 +229,71 @@ export function deriveSecp256k1StealthPrivateKey(
|
|
|
226
229
|
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
227
230
|
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
228
231
|
|
|
232
|
+
try {
|
|
233
|
+
// Compute shared secret: S = k_view * R (viewing private * ephemeral public)
|
|
234
|
+
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
235
|
+
viewingPrivBytes,
|
|
236
|
+
ephemeralPubBytes,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
// Hash the shared secret
|
|
240
|
+
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
241
|
+
|
|
242
|
+
// Derive stealth private key: k_spend + hash(S) mod n (canonical)
|
|
243
|
+
const spendingScalar = bytesToBigInt(spendingPrivBytes)
|
|
244
|
+
const hashScalar = bytesToBigInt(sharedSecretHash)
|
|
245
|
+
const stealthPrivateScalar = (spendingScalar + hashScalar) % secp256k1.CURVE.n
|
|
246
|
+
|
|
247
|
+
// Convert back to bytes
|
|
248
|
+
const stealthPrivateKey = bigIntToBytes(stealthPrivateScalar, 32)
|
|
249
|
+
|
|
250
|
+
const result = {
|
|
251
|
+
stealthAddress: stealthAddress.address,
|
|
252
|
+
ephemeralPublicKey: stealthAddress.ephemeralPublicKey,
|
|
253
|
+
privateKey: `0x${bytesToHex(stealthPrivateKey)}` as HexString,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
secureWipe(stealthPrivateKey)
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
} finally {
|
|
260
|
+
secureWipeAll(spendingPrivBytes, viewingPrivBytes)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @deprecated Legacy SIP:1 swapped-scheme derivation — claim-side back-compat ONLY.
|
|
266
|
+
*
|
|
267
|
+
* Recovers funds sent to secp256k1 stealth addresses generated before the
|
|
268
|
+
* canonical EIP-5564 flip (legacy scheme: `S = k_spend * R`, `p = k_view + H(S)`).
|
|
269
|
+
* Used only when claiming a `SIP:1` announcement. New (SIP:2) sends use
|
|
270
|
+
* {@link deriveSecp256k1StealthPrivateKey}.
|
|
271
|
+
*/
|
|
272
|
+
export function deriveSecp256k1StealthPrivateKeyV1(
|
|
273
|
+
stealthAddress: StealthAddress,
|
|
274
|
+
spendingPrivateKey: HexString,
|
|
275
|
+
viewingPrivateKey: HexString,
|
|
276
|
+
): StealthAddressRecovery {
|
|
277
|
+
validateSecp256k1StealthAddress(stealthAddress)
|
|
278
|
+
|
|
279
|
+
if (!isValidPrivateKey(spendingPrivateKey)) {
|
|
280
|
+
throw new ValidationError(
|
|
281
|
+
'must be a valid 32-byte hex string',
|
|
282
|
+
'spendingPrivateKey'
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
287
|
+
throw new ValidationError(
|
|
288
|
+
'must be a valid 32-byte hex string',
|
|
289
|
+
'viewingPrivateKey'
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
294
|
+
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
295
|
+
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
296
|
+
|
|
229
297
|
try {
|
|
230
298
|
// Compute shared secret: S = p * R (spending private * ephemeral public)
|
|
231
299
|
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
@@ -261,9 +329,78 @@ export function deriveSecp256k1StealthPrivateKey(
|
|
|
261
329
|
// ─── Address Checking ───────────────────────────────────────────────────────
|
|
262
330
|
|
|
263
331
|
/**
|
|
264
|
-
* Check if a secp256k1 stealth address
|
|
332
|
+
* Check if a secp256k1 stealth address is ours — canonical EIP-5564 view-only.
|
|
333
|
+
*
|
|
334
|
+
* Requires only the viewing PRIVATE key + the spending PUBLIC key, so a viewing
|
|
335
|
+
* key can be delegated for scanning without granting spend authority. Never
|
|
336
|
+
* touches the spending private key.
|
|
337
|
+
*
|
|
338
|
+
* @param stealthAddress - Stealth address to check
|
|
339
|
+
* @param viewingPrivateKey - Recipient's viewing private key
|
|
340
|
+
* @param spendingPublicKey - Recipient's compressed spending PUBLIC key (meta-address spendingKey)
|
|
341
|
+
* @returns true if this address belongs to the recipient
|
|
265
342
|
*/
|
|
266
343
|
export function checkSecp256k1StealthAddress(
|
|
344
|
+
stealthAddress: StealthAddress,
|
|
345
|
+
viewingPrivateKey: HexString,
|
|
346
|
+
spendingPublicKey: HexString,
|
|
347
|
+
): boolean {
|
|
348
|
+
validateSecp256k1StealthAddress(stealthAddress)
|
|
349
|
+
|
|
350
|
+
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
351
|
+
throw new ValidationError(
|
|
352
|
+
'must be a valid 32-byte hex string',
|
|
353
|
+
'viewingPrivateKey'
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!isValidCompressedPublicKey(spendingPublicKey)) {
|
|
358
|
+
throw new ValidationError(
|
|
359
|
+
'must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)',
|
|
360
|
+
'spendingPublicKey'
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
365
|
+
const spendingPubBytes = hexToBytes(spendingPublicKey.slice(2))
|
|
366
|
+
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
// Compute shared secret: S = k_view * R (canonical: ECDH on the viewing key)
|
|
370
|
+
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
371
|
+
viewingPrivBytes,
|
|
372
|
+
ephemeralPubBytes,
|
|
373
|
+
)
|
|
374
|
+
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
375
|
+
|
|
376
|
+
// View tag check (fast reject)
|
|
377
|
+
if (sharedSecretHash[0] !== stealthAddress.viewTag) {
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Expected address: A = K_spend + hash(S)*G (no spending private key needed)
|
|
382
|
+
const hashTimesG = secp256k1.getPublicKey(sharedSecretHash, true)
|
|
383
|
+
const expectedPoint = secp256k1.ProjectivePoint.fromHex(spendingPubBytes).add(
|
|
384
|
+
secp256k1.ProjectivePoint.fromHex(hashTimesG),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
// Compare with provided stealth address
|
|
388
|
+
const providedAddress = hexToBytes(stealthAddress.address.slice(2))
|
|
389
|
+
|
|
390
|
+
return bytesToHex(expectedPoint.toRawBytes(true)) === bytesToHex(providedAddress)
|
|
391
|
+
} finally {
|
|
392
|
+
secureWipe(viewingPrivBytes)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @deprecated Legacy SIP:1 full-wallet check — requires BOTH private keys.
|
|
398
|
+
*
|
|
399
|
+
* For detecting/claiming pre-flip (SIP:1) announcements only (legacy swapped
|
|
400
|
+
* scheme: `S = k_spend * R`, address built on the viewing key). New code should
|
|
401
|
+
* use the view-only {@link checkSecp256k1StealthAddress}.
|
|
402
|
+
*/
|
|
403
|
+
export function checkSecp256k1StealthAddressV1(
|
|
267
404
|
stealthAddress: StealthAddress,
|
|
268
405
|
spendingPrivateKey: HexString,
|
|
269
406
|
viewingPrivateKey: HexString,
|
package/src/stealth.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
generateStealthMetaAddress,
|
|
18
18
|
generateStealthAddress,
|
|
19
19
|
deriveStealthPrivateKey,
|
|
20
|
+
deriveStealthPrivateKeyV1,
|
|
20
21
|
checkStealthAddress,
|
|
21
22
|
|
|
22
23
|
// Chain detection
|
|
@@ -27,11 +28,17 @@ export {
|
|
|
27
28
|
generateEd25519StealthMetaAddress,
|
|
28
29
|
generateEd25519StealthAddress,
|
|
29
30
|
deriveEd25519StealthPrivateKey,
|
|
31
|
+
deriveEd25519StealthPrivateKeyV1,
|
|
30
32
|
checkEd25519StealthAddress,
|
|
33
|
+
checkEd25519StealthAddressV1,
|
|
31
34
|
|
|
32
35
|
// secp256k1 (Ethereum, Polygon, etc.)
|
|
33
36
|
publicKeyToEthAddress,
|
|
34
37
|
|
|
38
|
+
// Legacy SIP:1 back-compat (claim/scan of pre-flip announcements)
|
|
39
|
+
deriveSecp256k1StealthPrivateKeyV1,
|
|
40
|
+
checkSecp256k1StealthAddressV1,
|
|
41
|
+
|
|
35
42
|
// Meta-address encoding
|
|
36
43
|
encodeStealthMetaAddress,
|
|
37
44
|
decodeStealthMetaAddress,
|
|
@@ -337,8 +337,8 @@ export class PrivacyEthereumWalletAdapter extends EthereumWalletAdapter {
|
|
|
337
337
|
try {
|
|
338
338
|
const isOwned = checkEthereumStealthAddress(
|
|
339
339
|
announcement,
|
|
340
|
-
this.stealthKeys.metaAddress.spendingKey,
|
|
341
340
|
this.stealthKeys.viewingPrivateKey,
|
|
341
|
+
this.stealthKeys.metaAddress.spendingKey,
|
|
342
342
|
)
|
|
343
343
|
|
|
344
344
|
let ethAddress: HexString
|
|
@@ -247,8 +247,8 @@ export class LedgerPrivacyAdapter extends LedgerWalletAdapter {
|
|
|
247
247
|
// Check if this payment belongs to us using viewing key
|
|
248
248
|
const isOurs = checkEthereumStealthAddress(
|
|
249
249
|
announcement,
|
|
250
|
-
this.stealthKeys!.
|
|
251
|
-
this.stealthKeys!.
|
|
250
|
+
this.stealthKeys!.viewingPrivateKey,
|
|
251
|
+
this.stealthKeys!.metaAddress.spendingKey
|
|
252
252
|
)
|
|
253
253
|
|
|
254
254
|
if (isOurs) {
|
|
@@ -710,8 +710,8 @@ export class MeteorWalletPrivacy {
|
|
|
710
710
|
|
|
711
711
|
return checkNEARStealthAddress(
|
|
712
712
|
stealthAddress,
|
|
713
|
-
this.privacyKeys.
|
|
714
|
-
this.privacyKeys.
|
|
713
|
+
this.privacyKeys.viewingPrivateKey,
|
|
714
|
+
this.privacyKeys.spendingPublicKey
|
|
715
715
|
)
|
|
716
716
|
}
|
|
717
717
|
|
|
@@ -448,8 +448,8 @@ export class MyNearWalletPrivacy {
|
|
|
448
448
|
|
|
449
449
|
return checkNEARStealthAddress(
|
|
450
450
|
stealthAddress,
|
|
451
|
-
this.privacyKeys.
|
|
452
|
-
this.privacyKeys.
|
|
451
|
+
this.privacyKeys.viewingPrivateKey,
|
|
452
|
+
this.privacyKeys.spendingPublicKey
|
|
453
453
|
)
|
|
454
454
|
}
|
|
455
455
|
|