@sip-protocol/sdk 0.7.3 → 0.8.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/README.md +267 -0
- package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
- package/dist/browser.d.mts +10 -4
- package/dist/browser.d.ts +10 -4
- package/dist/browser.js +47556 -19603
- package/dist/browser.mjs +628 -48
- package/dist/chunk-4GRJ5MAW.mjs +152 -0
- package/dist/chunk-5D7A3L3W.mjs +717 -0
- package/dist/chunk-64AYA5F5.mjs +7834 -0
- package/dist/chunk-GMDGB22A.mjs +379 -0
- package/dist/chunk-I534WKN7.mjs +328 -0
- package/dist/chunk-IBZVA5Y7.mjs +1003 -0
- package/dist/chunk-PRRZAWJE.mjs +223 -0
- package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
- package/dist/{chunk-3M3HNQCW.mjs → chunk-YWGJ77A2.mjs} +28656 -13103
- package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
- package/dist/constants-LHAAUC2T.mjs +51 -0
- package/dist/dist-2OGQ7FED.mjs +3957 -0
- package/dist/dist-IFHPYLDX.mjs +254 -0
- package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
- package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
- package/dist/{index-DIBZHOOQ.d.ts → index-DXh2IGkz.d.ts} +21239 -10304
- package/dist/{index-8MQz13eJ.d.mts → index-DeE1ZzA4.d.mts} +21239 -10304
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48396 -19623
- package/dist/index.mjs +537 -19
- package/dist/interface-Bf7w1PLW.d.mts +679 -0
- package/dist/interface-Bf7w1PLW.d.ts +679 -0
- package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
- package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
- package/dist/proofs/halo2.d.mts +151 -0
- package/dist/proofs/halo2.d.ts +151 -0
- package/dist/proofs/halo2.js +350 -0
- package/dist/proofs/halo2.mjs +11 -0
- package/dist/proofs/kimchi.d.mts +160 -0
- package/dist/proofs/kimchi.d.ts +160 -0
- package/dist/proofs/kimchi.js +431 -0
- package/dist/proofs/kimchi.mjs +13 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +74 -18
- package/dist/proofs/noir.mjs +84 -24
- package/dist/solana-U3MEGU7W.mjs +280 -0
- package/dist/validity_proof-3POXLPNY.mjs +21 -0
- package/package.json +44 -11
- package/src/adapters/index.ts +41 -0
- package/src/adapters/jupiter.ts +571 -0
- package/src/adapters/near-intents.ts +135 -0
- package/src/advisor/advisor.ts +653 -0
- package/src/advisor/index.ts +54 -0
- package/src/advisor/tools.ts +303 -0
- package/src/advisor/types.ts +164 -0
- package/src/chains/ethereum/announcement.ts +536 -0
- package/src/chains/ethereum/bnb-optimizations.ts +474 -0
- package/src/chains/ethereum/commitment.ts +522 -0
- package/src/chains/ethereum/constants.ts +462 -0
- package/src/chains/ethereum/deployment.ts +596 -0
- package/src/chains/ethereum/gas-estimation.ts +538 -0
- package/src/chains/ethereum/index.ts +268 -0
- package/src/chains/ethereum/optimizations.ts +614 -0
- package/src/chains/ethereum/privacy-adapter.ts +855 -0
- package/src/chains/ethereum/registry.ts +584 -0
- package/src/chains/ethereum/rpc.ts +905 -0
- package/src/chains/ethereum/stealth.ts +491 -0
- package/src/chains/ethereum/token.ts +790 -0
- package/src/chains/ethereum/transfer.ts +637 -0
- package/src/chains/ethereum/types.ts +456 -0
- package/src/chains/ethereum/viewing-key.ts +455 -0
- package/src/chains/near/commitment.ts +608 -0
- package/src/chains/near/constants.ts +284 -0
- package/src/chains/near/function-call.ts +871 -0
- package/src/chains/near/history.ts +654 -0
- package/src/chains/near/implicit-account.ts +840 -0
- package/src/chains/near/index.ts +393 -0
- package/src/chains/near/native-transfer.ts +658 -0
- package/src/chains/near/nep141.ts +775 -0
- package/src/chains/near/privacy-adapter.ts +889 -0
- package/src/chains/near/resolver.ts +971 -0
- package/src/chains/near/rpc.ts +1016 -0
- package/src/chains/near/stealth.ts +419 -0
- package/src/chains/near/types.ts +317 -0
- package/src/chains/near/viewing-key.ts +876 -0
- package/src/chains/solana/anchor-transfer.ts +386 -0
- package/src/chains/solana/commitment.ts +577 -0
- package/src/chains/solana/constants.ts +126 -12
- package/src/chains/solana/ephemeral-keys.ts +543 -0
- package/src/chains/solana/index.ts +252 -1
- package/src/chains/solana/key-derivation.ts +418 -0
- package/src/chains/solana/kit-compat.ts +334 -0
- package/src/chains/solana/optimizations.ts +560 -0
- package/src/chains/solana/privacy-adapter.ts +605 -0
- package/src/chains/solana/providers/generic.ts +47 -6
- package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
- package/src/chains/solana/providers/helius-enhanced.ts +623 -0
- package/src/chains/solana/providers/helius.ts +186 -33
- package/src/chains/solana/providers/index.ts +31 -0
- package/src/chains/solana/providers/interface.ts +61 -18
- package/src/chains/solana/providers/quicknode.ts +409 -0
- package/src/chains/solana/providers/triton.ts +426 -0
- package/src/chains/solana/providers/webhook.ts +338 -67
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +83 -66
- package/src/chains/solana/sol-transfer.ts +732 -0
- package/src/chains/solana/spl-transfer.ts +886 -0
- package/src/chains/solana/stealth-scanner.ts +703 -0
- package/src/chains/solana/sunspot-verifier.ts +453 -0
- package/src/chains/solana/transaction-builder.ts +755 -0
- package/src/chains/solana/transfer.ts +74 -5
- package/src/chains/solana/types.ts +57 -6
- package/src/chains/solana/utils.ts +110 -0
- package/src/chains/solana/viewing-key.ts +807 -0
- package/src/compliance/fireblocks.ts +921 -0
- package/src/compliance/index.ts +23 -0
- package/src/compliance/range-sas.ts +398 -33
- package/src/config/endpoints.ts +100 -0
- package/src/crypto.ts +11 -8
- package/src/errors.ts +82 -0
- package/src/evm/erc4337-relayer.ts +830 -0
- package/src/evm/index.ts +47 -0
- package/src/fees/calculator.ts +396 -0
- package/src/fees/index.ts +87 -0
- package/src/fees/near-contract.ts +429 -0
- package/src/fees/types.ts +268 -0
- package/src/index.ts +686 -1
- package/src/intent.ts +6 -3
- package/src/logger.ts +324 -0
- package/src/network/index.ts +80 -0
- package/src/network/proxy.ts +691 -0
- package/src/optimizations/index.ts +541 -0
- package/src/oracle/types.ts +1 -0
- package/src/privacy-backends/arcium-types.ts +727 -0
- package/src/privacy-backends/arcium.ts +719 -0
- package/src/privacy-backends/combined-privacy.ts +866 -0
- package/src/privacy-backends/cspl-token.ts +595 -0
- package/src/privacy-backends/cspl-types.ts +512 -0
- package/src/privacy-backends/cspl.ts +907 -0
- package/src/privacy-backends/health.ts +488 -0
- package/src/privacy-backends/inco-types.ts +323 -0
- package/src/privacy-backends/inco.ts +616 -0
- package/src/privacy-backends/index.ts +254 -4
- package/src/privacy-backends/interface.ts +649 -6
- package/src/privacy-backends/lru-cache.ts +343 -0
- package/src/privacy-backends/magicblock.ts +458 -0
- package/src/privacy-backends/mock.ts +258 -0
- package/src/privacy-backends/privacycash.ts +13 -17
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +414 -2
- package/src/privacy-backends/router.ts +283 -3
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +3 -0
- package/src/privacy-logger.ts +191 -0
- package/src/production-safety.ts +373 -0
- package/src/proofs/aggregator.ts +1029 -0
- package/src/proofs/browser-composer.ts +1150 -0
- package/src/proofs/browser.ts +113 -25
- package/src/proofs/cache/index.ts +127 -0
- package/src/proofs/cache/interface.ts +545 -0
- package/src/proofs/cache/key-generator.ts +188 -0
- package/src/proofs/cache/lru-cache.ts +481 -0
- package/src/proofs/cache/multi-tier-cache.ts +575 -0
- package/src/proofs/cache/persistent-cache.ts +788 -0
- package/src/proofs/compliance-proof.ts +872 -0
- package/src/proofs/composer/base.ts +923 -0
- package/src/proofs/composer/index.ts +25 -0
- package/src/proofs/composer/interface.ts +518 -0
- package/src/proofs/composer/types.ts +383 -0
- package/src/proofs/converters/halo2.ts +452 -0
- package/src/proofs/converters/index.ts +208 -0
- package/src/proofs/converters/interface.ts +363 -0
- package/src/proofs/converters/kimchi.ts +462 -0
- package/src/proofs/converters/noir.ts +451 -0
- package/src/proofs/fallback.ts +888 -0
- package/src/proofs/halo2.ts +42 -0
- package/src/proofs/index.ts +471 -0
- package/src/proofs/interface.ts +13 -0
- package/src/proofs/kimchi.ts +42 -0
- package/src/proofs/lazy.ts +1004 -0
- package/src/proofs/mock.ts +25 -1
- package/src/proofs/noir.ts +110 -29
- package/src/proofs/orchestrator.ts +960 -0
- package/src/proofs/parallel/concurrency.ts +297 -0
- package/src/proofs/parallel/dependency-graph.ts +602 -0
- package/src/proofs/parallel/executor.ts +420 -0
- package/src/proofs/parallel/index.ts +131 -0
- package/src/proofs/parallel/interface.ts +685 -0
- package/src/proofs/parallel/worker-pool.ts +644 -0
- package/src/proofs/providers/halo2.ts +560 -0
- package/src/proofs/providers/index.ts +34 -0
- package/src/proofs/providers/kimchi.ts +641 -0
- package/src/proofs/validator.ts +881 -0
- package/src/proofs/verifier.ts +867 -0
- package/src/quantum/index.ts +112 -0
- package/src/quantum/winternitz-vault.ts +639 -0
- package/src/quantum/wots.ts +611 -0
- package/src/settlement/backends/direct-chain.ts +1 -0
- package/src/settlement/index.ts +9 -0
- package/src/settlement/router.ts +732 -46
- package/src/solana/index.ts +72 -0
- package/src/solana/jito-relayer.ts +687 -0
- package/src/solana/noir-verifier-types.ts +430 -0
- package/src/solana/noir-verifier.ts +816 -0
- package/src/stealth/address-derivation.ts +193 -0
- package/src/stealth/ed25519.ts +431 -0
- package/src/stealth/index.ts +233 -0
- package/src/stealth/meta-address.ts +221 -0
- package/src/stealth/secp256k1.ts +368 -0
- package/src/stealth/utils.ts +194 -0
- package/src/stealth.ts +50 -1504
- package/src/sync/index.ts +106 -0
- package/src/sync/manager.ts +504 -0
- package/src/sync/mock-provider.ts +318 -0
- package/src/sync/oblivious.ts +625 -0
- package/src/tokens/index.ts +15 -0
- package/src/tokens/registry.ts +301 -0
- package/src/utils/deprecation.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/wallet/ethereum/index.ts +68 -0
- package/src/wallet/ethereum/metamask-privacy.ts +420 -0
- package/src/wallet/ethereum/multi-wallet.ts +646 -0
- package/src/wallet/ethereum/privacy-adapter.ts +700 -0
- package/src/wallet/ethereum/types.ts +3 -1
- package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
- package/src/wallet/hardware/index.ts +10 -0
- package/src/wallet/hardware/ledger-privacy.ts +414 -0
- package/src/wallet/index.ts +71 -0
- package/src/wallet/near/adapter.ts +626 -0
- package/src/wallet/near/index.ts +86 -0
- package/src/wallet/near/meteor-wallet.ts +1153 -0
- package/src/wallet/near/my-near-wallet.ts +790 -0
- package/src/wallet/near/wallet-selector.ts +702 -0
- package/src/wallet/solana/adapter.ts +6 -4
- package/src/wallet/solana/index.ts +13 -0
- package/src/wallet/solana/privacy-adapter.ts +567 -0
- package/src/wallet/sui/types.ts +6 -4
- package/src/zcash/rpc-client.ts +13 -6
- package/dist/chunk-2XIVXWHA.mjs +0 -1930
- package/dist/chunk-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-7RFRWDCW.mjs +0 -1504
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-F6F73W35.mjs +0 -16166
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-OFDBEIEK.mjs +0 -16166
- package/dist/chunk-SF7YSLF5.mjs +0 -1515
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/chunk-WWUSGOXE.mjs +0 -17129
- package/dist/constants-VOI7BSLK.mjs +0 -27
- package/dist/index-B71aXVzk.d.ts +0 -13264
- package/dist/index-BYZbDjal.d.ts +0 -11390
- package/dist/index-CHB3KuOB.d.mts +0 -11859
- package/dist/index-CzWPI6Le.d.ts +0 -11859
- package/dist/index-pOIIuwfV.d.mts +0 -13264
- package/dist/index-xbWjohNq.d.mts +0 -11390
- package/dist/solana-4O4K45VU.mjs +0 -46
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-NDABAZ6P.mjs +0 -56
- package/dist/solana-Q4NAVBTS.mjs +0 -46
- package/dist/solana-ZYO63LY5.mjs +0 -46
package/src/stealth.ts
CHANGED
|
@@ -1,1507 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stealth Address Generation for SIP Protocol
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* @param label - Optional human-readable label for identification
|
|
55
|
-
* @returns Object containing:
|
|
56
|
-
* - `metaAddress`: Public keys to share with senders
|
|
57
|
-
* - `spendingPrivateKey`: Secret key for claiming funds (keep secure!)
|
|
58
|
-
* - `viewingPrivateKey`: Secret key for scanning incoming payments (keep secure!)
|
|
59
|
-
*
|
|
60
|
-
* @throws {ValidationError} If chain is invalid or not supported
|
|
61
|
-
*
|
|
62
|
-
* @example Generate stealth keys for Ethereum
|
|
63
|
-
* ```typescript
|
|
64
|
-
* import { generateStealthMetaAddress, encodeStealthMetaAddress } from '@sip-protocol/sdk'
|
|
65
|
-
*
|
|
66
|
-
* // Generate keys
|
|
67
|
-
* const { metaAddress, spendingPrivateKey, viewingPrivateKey } =
|
|
68
|
-
* generateStealthMetaAddress('ethereum', 'My Privacy Wallet')
|
|
69
|
-
*
|
|
70
|
-
* // Encode for sharing (QR code, website, etc.)
|
|
71
|
-
* const encoded = encodeStealthMetaAddress(metaAddress)
|
|
72
|
-
* console.log('Share this:', encoded)
|
|
73
|
-
* // Output: "sip:ethereum:0x02abc...123:0x03def...456"
|
|
74
|
-
*
|
|
75
|
-
* // Store private keys securely (e.g., encrypted keystore)
|
|
76
|
-
* secureStorage.save({
|
|
77
|
-
* spendingPrivateKey,
|
|
78
|
-
* viewingPrivateKey,
|
|
79
|
-
* })
|
|
80
|
-
* ```
|
|
81
|
-
*
|
|
82
|
-
* @example Multi-chain setup
|
|
83
|
-
* ```typescript
|
|
84
|
-
* // Generate different stealth keys for each chain
|
|
85
|
-
* const ethKeys = generateStealthMetaAddress('ethereum', 'ETH Privacy')
|
|
86
|
-
* const zkKeys = generateStealthMetaAddress('zcash', 'ZEC Privacy')
|
|
87
|
-
*
|
|
88
|
-
* // Publish meta-addresses
|
|
89
|
-
* publishToProfile({
|
|
90
|
-
* ethereum: encodeStealthMetaAddress(ethKeys.metaAddress),
|
|
91
|
-
* zcash: encodeStealthMetaAddress(zkKeys.metaAddress),
|
|
92
|
-
* })
|
|
93
|
-
* ```
|
|
94
|
-
*
|
|
95
|
-
* @see {@link generateStealthAddress} to generate payment addresses as a sender
|
|
96
|
-
* @see {@link encodeStealthMetaAddress} to encode for sharing
|
|
97
|
-
* @see {@link deriveStealthPrivateKey} to claim funds as a recipient
|
|
98
|
-
* @see {@link generateEd25519StealthMetaAddress} for Solana/NEAR chains
|
|
99
|
-
*/
|
|
100
|
-
export function generateStealthMetaAddress(
|
|
101
|
-
chain: ChainId,
|
|
102
|
-
label?: string,
|
|
103
|
-
): {
|
|
104
|
-
metaAddress: StealthMetaAddress
|
|
105
|
-
spendingPrivateKey: HexString
|
|
106
|
-
viewingPrivateKey: HexString
|
|
107
|
-
} {
|
|
108
|
-
// Validate chain
|
|
109
|
-
if (!isValidChainId(chain)) {
|
|
110
|
-
throw new ValidationError(
|
|
111
|
-
`invalid chain '${chain}', must be one of: solana, ethereum, near, zcash, polygon, arbitrum, optimism, base, bitcoin, aptos, sui, cosmos, osmosis, injective, celestia, sei, dydx`,
|
|
112
|
-
'chain'
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Dispatch to curve-specific implementation
|
|
117
|
-
if (isEd25519Chain(chain)) {
|
|
118
|
-
return generateEd25519StealthMetaAddress(chain, label)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// secp256k1 implementation for EVM chains
|
|
122
|
-
const spendingPrivateKey = randomBytes(32)
|
|
123
|
-
const viewingPrivateKey = randomBytes(32)
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
// Derive public keys
|
|
127
|
-
const spendingKey = secp256k1.getPublicKey(spendingPrivateKey, true)
|
|
128
|
-
const viewingKey = secp256k1.getPublicKey(viewingPrivateKey, true)
|
|
129
|
-
|
|
130
|
-
// Convert to hex strings before wiping buffers
|
|
131
|
-
const result = {
|
|
132
|
-
metaAddress: {
|
|
133
|
-
spendingKey: `0x${bytesToHex(spendingKey)}` as HexString,
|
|
134
|
-
viewingKey: `0x${bytesToHex(viewingKey)}` as HexString,
|
|
135
|
-
chain,
|
|
136
|
-
label,
|
|
137
|
-
},
|
|
138
|
-
spendingPrivateKey: `0x${bytesToHex(spendingPrivateKey)}` as HexString,
|
|
139
|
-
viewingPrivateKey: `0x${bytesToHex(viewingPrivateKey)}` as HexString,
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return result
|
|
143
|
-
} finally {
|
|
144
|
-
// Securely wipe private key buffers
|
|
145
|
-
// Note: The hex strings returned to caller must be handled securely by them
|
|
146
|
-
secureWipeAll(spendingPrivateKey, viewingPrivateKey)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Validate a StealthMetaAddress object
|
|
152
|
-
* Supports both secp256k1 (EVM chains) and ed25519 (Solana, NEAR, etc.) key formats
|
|
153
|
-
*/
|
|
154
|
-
function validateStealthMetaAddress(
|
|
155
|
-
metaAddress: StealthMetaAddress,
|
|
156
|
-
field: string = 'recipientMetaAddress'
|
|
157
|
-
): void {
|
|
158
|
-
if (!metaAddress || typeof metaAddress !== 'object') {
|
|
159
|
-
throw new ValidationError('must be an object', field)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Validate chain
|
|
163
|
-
if (!isValidChainId(metaAddress.chain)) {
|
|
164
|
-
throw new ValidationError(
|
|
165
|
-
`invalid chain '${metaAddress.chain}'`,
|
|
166
|
-
`${field}.chain`
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Determine key type based on chain (ed25519 vs secp256k1)
|
|
171
|
-
const isEd25519 = isEd25519Chain(metaAddress.chain)
|
|
172
|
-
|
|
173
|
-
if (isEd25519) {
|
|
174
|
-
// Ed25519 chains (Solana, NEAR, Aptos, Sui) use 32-byte public keys
|
|
175
|
-
if (!isValidEd25519PublicKey(metaAddress.spendingKey)) {
|
|
176
|
-
throw new ValidationError(
|
|
177
|
-
'spendingKey must be a valid ed25519 public key (32 bytes)',
|
|
178
|
-
`${field}.spendingKey`
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
if (!isValidEd25519PublicKey(metaAddress.viewingKey)) {
|
|
182
|
-
throw new ValidationError(
|
|
183
|
-
'viewingKey must be a valid ed25519 public key (32 bytes)',
|
|
184
|
-
`${field}.viewingKey`
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
// Secp256k1 chains (Ethereum, etc.) use 33-byte compressed public keys
|
|
189
|
-
if (!isValidCompressedPublicKey(metaAddress.spendingKey)) {
|
|
190
|
-
throw new ValidationError(
|
|
191
|
-
'spendingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)',
|
|
192
|
-
`${field}.spendingKey`
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
if (!isValidCompressedPublicKey(metaAddress.viewingKey)) {
|
|
196
|
-
throw new ValidationError(
|
|
197
|
-
'viewingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)',
|
|
198
|
-
`${field}.viewingKey`
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Generate a one-time stealth address for sending funds to a recipient
|
|
206
|
-
*
|
|
207
|
-
* As a sender, use this function to create a unique, unlinkable payment address
|
|
208
|
-
* from the recipient's public meta-address. Each call generates a new address
|
|
209
|
-
* that only the recipient can link to their identity.
|
|
210
|
-
*
|
|
211
|
-
* **Privacy Properties:**
|
|
212
|
-
* - Address is unique per transaction (prevents on-chain linkability)
|
|
213
|
-
* - Only recipient can detect and claim payments
|
|
214
|
-
* - Third-party observers cannot link payments to the same recipient
|
|
215
|
-
* - View tag enables efficient payment scanning
|
|
216
|
-
*
|
|
217
|
-
* **Algorithm (EIP-5564 DKSAP):**
|
|
218
|
-
* 1. Generate ephemeral keypair (r, R = r*G)
|
|
219
|
-
* 2. Compute shared secret: S = r * P_spend
|
|
220
|
-
* 3. Derive stealth address: A = P_view + hash(S)*G
|
|
221
|
-
* 4. Publish (R, A) on-chain; keep r secret
|
|
222
|
-
*
|
|
223
|
-
* @param recipientMetaAddress - Recipient's public stealth meta-address
|
|
224
|
-
* @returns Object containing:
|
|
225
|
-
* - `stealthAddress`: One-time payment address to publish on-chain
|
|
226
|
-
* - `sharedSecret`: Secret for sender's records (optional, don't publish!)
|
|
227
|
-
*
|
|
228
|
-
* @throws {ValidationError} If meta-address is invalid or malformed
|
|
229
|
-
*
|
|
230
|
-
* @example Send shielded payment
|
|
231
|
-
* ```typescript
|
|
232
|
-
* import { generateStealthAddress, decodeStealthMetaAddress } from '@sip-protocol/sdk'
|
|
233
|
-
*
|
|
234
|
-
* // Recipient shares their meta-address (e.g., on website, profile)
|
|
235
|
-
* const recipientMetaAddr = 'sip:ethereum:0x02abc...123:0x03def...456'
|
|
236
|
-
*
|
|
237
|
-
* // Decode the meta-address
|
|
238
|
-
* const metaAddress = decodeStealthMetaAddress(recipientMetaAddr)
|
|
239
|
-
*
|
|
240
|
-
* // Generate one-time payment address
|
|
241
|
-
* const { stealthAddress } = generateStealthAddress(metaAddress)
|
|
242
|
-
*
|
|
243
|
-
* // Use the stealth address in your transaction
|
|
244
|
-
* await sendPayment({
|
|
245
|
-
* to: stealthAddress.address, // One-time address
|
|
246
|
-
* amount: '1000000000000000000', // 1 ETH
|
|
247
|
-
* ephemeralKey: stealthAddress.ephemeralPublicKey, // Publish for recipient
|
|
248
|
-
* viewTag: stealthAddress.viewTag, // For efficient scanning
|
|
249
|
-
* })
|
|
250
|
-
* ```
|
|
251
|
-
*
|
|
252
|
-
* @example Integrate with SIP intent
|
|
253
|
-
* ```typescript
|
|
254
|
-
* // In a shielded intent, the recipient stealth address is generated automatically
|
|
255
|
-
* const intent = await sip.createIntent({
|
|
256
|
-
* input: { asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, amount: 10n },
|
|
257
|
-
* output: { asset: { chain: 'ethereum', symbol: 'ETH', address: null, decimals: 18 }, minAmount: 0n, maxSlippage: 0.01 },
|
|
258
|
-
* privacy: PrivacyLevel.SHIELDED,
|
|
259
|
-
* recipientMetaAddress: 'sip:ethereum:0x02abc...123:0x03def...456',
|
|
260
|
-
* })
|
|
261
|
-
* // intent.recipientStealth contains the generated stealth address
|
|
262
|
-
* ```
|
|
263
|
-
*
|
|
264
|
-
* @see {@link generateStealthMetaAddress} to create meta-address as recipient
|
|
265
|
-
* @see {@link deriveStealthPrivateKey} for recipient to claim funds
|
|
266
|
-
* @see {@link checkStealthAddress} to scan for incoming payments
|
|
267
|
-
*/
|
|
268
|
-
export function generateStealthAddress(
|
|
269
|
-
recipientMetaAddress: StealthMetaAddress,
|
|
270
|
-
): {
|
|
271
|
-
stealthAddress: StealthAddress
|
|
272
|
-
sharedSecret: HexString
|
|
273
|
-
} {
|
|
274
|
-
// Validate input
|
|
275
|
-
validateStealthMetaAddress(recipientMetaAddress)
|
|
276
|
-
|
|
277
|
-
// Dispatch to curve-specific implementation based on chain
|
|
278
|
-
if (isEd25519Chain(recipientMetaAddress.chain)) {
|
|
279
|
-
return generateEd25519StealthAddress(recipientMetaAddress)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// secp256k1 implementation for EVM chains
|
|
283
|
-
const ephemeralPrivateKey = randomBytes(32)
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const ephemeralPublicKey = secp256k1.getPublicKey(ephemeralPrivateKey, true)
|
|
287
|
-
|
|
288
|
-
// Parse recipient's keys (remove 0x prefix)
|
|
289
|
-
const spendingKeyBytes = hexToBytes(recipientMetaAddress.spendingKey.slice(2))
|
|
290
|
-
const viewingKeyBytes = hexToBytes(recipientMetaAddress.viewingKey.slice(2))
|
|
291
|
-
|
|
292
|
-
// Compute shared secret: S = r * P (ephemeral private * spending public)
|
|
293
|
-
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
294
|
-
ephemeralPrivateKey,
|
|
295
|
-
spendingKeyBytes,
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
// Hash the shared secret for use as a scalar
|
|
299
|
-
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
300
|
-
|
|
301
|
-
// Compute stealth address: A = Q + hash(S)*G
|
|
302
|
-
// First get hash(S)*G
|
|
303
|
-
const hashTimesG = secp256k1.getPublicKey(sharedSecretHash, true)
|
|
304
|
-
|
|
305
|
-
// Then add to viewing key Q
|
|
306
|
-
const viewingKeyPoint = secp256k1.ProjectivePoint.fromHex(viewingKeyBytes)
|
|
307
|
-
const hashTimesGPoint = secp256k1.ProjectivePoint.fromHex(hashTimesG)
|
|
308
|
-
const stealthPoint = viewingKeyPoint.add(hashTimesGPoint)
|
|
309
|
-
const stealthAddressBytes = stealthPoint.toRawBytes(true)
|
|
310
|
-
|
|
311
|
-
// Compute view tag (first byte of hash for efficient scanning)
|
|
312
|
-
const viewTag = sharedSecretHash[0]
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
stealthAddress: {
|
|
316
|
-
address: `0x${bytesToHex(stealthAddressBytes)}` as HexString,
|
|
317
|
-
ephemeralPublicKey: `0x${bytesToHex(ephemeralPublicKey)}` as HexString,
|
|
318
|
-
viewTag,
|
|
319
|
-
},
|
|
320
|
-
sharedSecret: `0x${bytesToHex(sharedSecretHash)}` as HexString,
|
|
321
|
-
}
|
|
322
|
-
} finally {
|
|
323
|
-
// Securely wipe ephemeral private key
|
|
324
|
-
secureWipe(ephemeralPrivateKey)
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Validate a StealthAddress object
|
|
330
|
-
*/
|
|
331
|
-
function validateStealthAddress(
|
|
332
|
-
stealthAddress: StealthAddress,
|
|
333
|
-
field: string = 'stealthAddress'
|
|
334
|
-
): void {
|
|
335
|
-
if (!stealthAddress || typeof stealthAddress !== 'object') {
|
|
336
|
-
throw new ValidationError('must be an object', field)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Validate address (compressed public key)
|
|
340
|
-
if (!isValidCompressedPublicKey(stealthAddress.address)) {
|
|
341
|
-
throw new ValidationError(
|
|
342
|
-
'address must be a valid compressed secp256k1 public key',
|
|
343
|
-
`${field}.address`
|
|
344
|
-
)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Validate ephemeral public key
|
|
348
|
-
if (!isValidCompressedPublicKey(stealthAddress.ephemeralPublicKey)) {
|
|
349
|
-
throw new ValidationError(
|
|
350
|
-
'ephemeralPublicKey must be a valid compressed secp256k1 public key',
|
|
351
|
-
`${field}.ephemeralPublicKey`
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Validate view tag (0-255)
|
|
356
|
-
if (typeof stealthAddress.viewTag !== 'number' ||
|
|
357
|
-
!Number.isInteger(stealthAddress.viewTag) ||
|
|
358
|
-
stealthAddress.viewTag < 0 ||
|
|
359
|
-
stealthAddress.viewTag > 255) {
|
|
360
|
-
throw new ValidationError(
|
|
361
|
-
'viewTag must be an integer between 0 and 255',
|
|
362
|
-
`${field}.viewTag`
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Derive the private key for a stealth address (for recipient to claim funds)
|
|
369
|
-
*
|
|
370
|
-
* @param stealthAddress - The stealth address to recover
|
|
371
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
372
|
-
* @param viewingPrivateKey - Recipient's viewing private key
|
|
373
|
-
* @returns Recovery data including derived private key
|
|
374
|
-
* @throws {ValidationError} If any input is invalid
|
|
375
|
-
*/
|
|
376
|
-
export function deriveStealthPrivateKey(
|
|
377
|
-
stealthAddress: StealthAddress,
|
|
378
|
-
spendingPrivateKey: HexString,
|
|
379
|
-
viewingPrivateKey: HexString,
|
|
380
|
-
): StealthAddressRecovery {
|
|
381
|
-
// Validate stealth address
|
|
382
|
-
validateStealthAddress(stealthAddress)
|
|
383
|
-
|
|
384
|
-
// Validate private keys
|
|
385
|
-
if (!isValidPrivateKey(spendingPrivateKey)) {
|
|
386
|
-
throw new ValidationError(
|
|
387
|
-
'must be a valid 32-byte hex string',
|
|
388
|
-
'spendingPrivateKey'
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
393
|
-
throw new ValidationError(
|
|
394
|
-
'must be a valid 32-byte hex string',
|
|
395
|
-
'viewingPrivateKey'
|
|
396
|
-
)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Parse keys
|
|
400
|
-
const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
401
|
-
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
402
|
-
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
// Compute shared secret: S = p * R (spending private * ephemeral public)
|
|
406
|
-
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
407
|
-
spendingPrivBytes,
|
|
408
|
-
ephemeralPubBytes,
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
// Hash the shared secret
|
|
412
|
-
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
413
|
-
|
|
414
|
-
// Derive stealth private key: q + hash(S) mod n
|
|
415
|
-
// Where q is the viewing private key
|
|
416
|
-
const viewingScalar = bytesToBigInt(viewingPrivBytes)
|
|
417
|
-
const hashScalar = bytesToBigInt(sharedSecretHash)
|
|
418
|
-
const stealthPrivateScalar = (viewingScalar + hashScalar) % secp256k1.CURVE.n
|
|
419
|
-
|
|
420
|
-
// Convert back to bytes
|
|
421
|
-
const stealthPrivateKey = bigIntToBytes(stealthPrivateScalar, 32)
|
|
422
|
-
|
|
423
|
-
const result = {
|
|
424
|
-
stealthAddress: stealthAddress.address,
|
|
425
|
-
ephemeralPublicKey: stealthAddress.ephemeralPublicKey,
|
|
426
|
-
privateKey: `0x${bytesToHex(stealthPrivateKey)}` as HexString,
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Wipe derived key buffer after converting to hex
|
|
430
|
-
secureWipe(stealthPrivateKey)
|
|
431
|
-
|
|
432
|
-
return result
|
|
433
|
-
} finally {
|
|
434
|
-
// Securely wipe input private key buffers
|
|
435
|
-
secureWipeAll(spendingPrivBytes, viewingPrivBytes)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Check if a stealth address was intended for this recipient
|
|
441
|
-
* Uses view tag for efficient filtering before full computation
|
|
442
|
-
*
|
|
443
|
-
* @param stealthAddress - Stealth address to check
|
|
444
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
445
|
-
* @param viewingPrivateKey - Recipient's viewing private key
|
|
446
|
-
* @returns true if this address belongs to the recipient
|
|
447
|
-
* @throws {ValidationError} If any input is invalid
|
|
448
|
-
*/
|
|
449
|
-
export function checkStealthAddress(
|
|
450
|
-
stealthAddress: StealthAddress,
|
|
451
|
-
spendingPrivateKey: HexString,
|
|
452
|
-
viewingPrivateKey: HexString,
|
|
453
|
-
): boolean {
|
|
454
|
-
// Validate stealth address
|
|
455
|
-
validateStealthAddress(stealthAddress)
|
|
456
|
-
|
|
457
|
-
// Validate private keys
|
|
458
|
-
if (!isValidPrivateKey(spendingPrivateKey)) {
|
|
459
|
-
throw new ValidationError(
|
|
460
|
-
'must be a valid 32-byte hex string',
|
|
461
|
-
'spendingPrivateKey'
|
|
462
|
-
)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
466
|
-
throw new ValidationError(
|
|
467
|
-
'must be a valid 32-byte hex string',
|
|
468
|
-
'viewingPrivateKey'
|
|
469
|
-
)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Parse keys
|
|
473
|
-
const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
474
|
-
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
475
|
-
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
476
|
-
|
|
477
|
-
try {
|
|
478
|
-
// Quick check: compute shared secret and verify view tag first
|
|
479
|
-
const sharedSecretPoint = secp256k1.getSharedSecret(
|
|
480
|
-
spendingPrivBytes,
|
|
481
|
-
ephemeralPubBytes,
|
|
482
|
-
)
|
|
483
|
-
const sharedSecretHash = sha256(sharedSecretPoint)
|
|
484
|
-
|
|
485
|
-
// View tag check (optimization - reject quickly if doesn't match)
|
|
486
|
-
if (sharedSecretHash[0] !== stealthAddress.viewTag) {
|
|
487
|
-
return false
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Full check: derive the expected stealth address
|
|
491
|
-
const viewingScalar = bytesToBigInt(viewingPrivBytes)
|
|
492
|
-
const hashScalar = bytesToBigInt(sharedSecretHash)
|
|
493
|
-
const stealthPrivateScalar = (viewingScalar + hashScalar) % secp256k1.CURVE.n
|
|
494
|
-
|
|
495
|
-
// Compute expected public key from derived private key
|
|
496
|
-
const derivedKeyBytes = bigIntToBytes(stealthPrivateScalar, 32)
|
|
497
|
-
const expectedPubKey = secp256k1.getPublicKey(derivedKeyBytes, true)
|
|
498
|
-
|
|
499
|
-
// Wipe derived key immediately after use
|
|
500
|
-
secureWipe(derivedKeyBytes)
|
|
501
|
-
|
|
502
|
-
// Compare with provided stealth address
|
|
503
|
-
const providedAddress = hexToBytes(stealthAddress.address.slice(2))
|
|
504
|
-
|
|
505
|
-
return bytesToHex(expectedPubKey) === bytesToHex(providedAddress)
|
|
506
|
-
} finally {
|
|
507
|
-
// Securely wipe input private key buffers
|
|
508
|
-
secureWipeAll(spendingPrivBytes, viewingPrivBytes)
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Encode a stealth meta-address as a string
|
|
514
|
-
* Format: sip:{chain}:{spendingKey}:{viewingKey}
|
|
515
|
-
*/
|
|
516
|
-
export function encodeStealthMetaAddress(metaAddress: StealthMetaAddress): string {
|
|
517
|
-
return `sip:${metaAddress.chain}:${metaAddress.spendingKey}:${metaAddress.viewingKey}`
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Decode a stealth meta-address from a string
|
|
522
|
-
*
|
|
523
|
-
* @param encoded - Encoded stealth meta-address (format: sip:<chain>:<spendingKey>:<viewingKey>)
|
|
524
|
-
* @returns Decoded StealthMetaAddress
|
|
525
|
-
* @throws {ValidationError} If format is invalid or keys are malformed
|
|
526
|
-
*/
|
|
527
|
-
export function decodeStealthMetaAddress(encoded: string): StealthMetaAddress {
|
|
528
|
-
if (typeof encoded !== 'string') {
|
|
529
|
-
throw new ValidationError('must be a string', 'encoded')
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const parts = encoded.split(':')
|
|
533
|
-
if (parts.length < 4 || parts[0] !== 'sip') {
|
|
534
|
-
throw new ValidationError(
|
|
535
|
-
'invalid format, expected: sip:<chain>:<spendingKey>:<viewingKey>',
|
|
536
|
-
'encoded'
|
|
537
|
-
)
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const [, chain, spendingKey, viewingKey] = parts
|
|
541
|
-
|
|
542
|
-
// Validate chain
|
|
543
|
-
if (!isValidChainId(chain)) {
|
|
544
|
-
throw new ValidationError(
|
|
545
|
-
`invalid chain '${chain}'`,
|
|
546
|
-
'encoded.chain'
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Validate keys based on chain's curve type
|
|
551
|
-
const chainId = chain as ChainId
|
|
552
|
-
if (isEd25519Chain(chainId)) {
|
|
553
|
-
// Ed25519 chains (Solana, NEAR) use 32-byte public keys
|
|
554
|
-
if (!isValidEd25519PublicKey(spendingKey)) {
|
|
555
|
-
throw new ValidationError(
|
|
556
|
-
'spendingKey must be a valid 32-byte ed25519 public key',
|
|
557
|
-
'encoded.spendingKey'
|
|
558
|
-
)
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (!isValidEd25519PublicKey(viewingKey)) {
|
|
562
|
-
throw new ValidationError(
|
|
563
|
-
'viewingKey must be a valid 32-byte ed25519 public key',
|
|
564
|
-
'encoded.viewingKey'
|
|
565
|
-
)
|
|
566
|
-
}
|
|
567
|
-
} else {
|
|
568
|
-
// secp256k1 chains (Ethereum, etc.) use 33-byte compressed public keys
|
|
569
|
-
if (!isValidCompressedPublicKey(spendingKey)) {
|
|
570
|
-
throw new ValidationError(
|
|
571
|
-
'spendingKey must be a valid compressed secp256k1 public key',
|
|
572
|
-
'encoded.spendingKey'
|
|
573
|
-
)
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (!isValidCompressedPublicKey(viewingKey)) {
|
|
577
|
-
throw new ValidationError(
|
|
578
|
-
'viewingKey must be a valid compressed secp256k1 public key',
|
|
579
|
-
'encoded.viewingKey'
|
|
580
|
-
)
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return {
|
|
585
|
-
chain: chain as ChainId,
|
|
586
|
-
spendingKey: spendingKey as HexString,
|
|
587
|
-
viewingKey: viewingKey as HexString,
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ─── Utility Functions ──────────────────────────────────────────────────────
|
|
592
|
-
|
|
593
|
-
function bytesToBigInt(bytes: Uint8Array): bigint {
|
|
594
|
-
let result = 0n
|
|
595
|
-
for (const byte of bytes) {
|
|
596
|
-
result = (result << 8n) + BigInt(byte)
|
|
597
|
-
}
|
|
598
|
-
return result
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function bigIntToBytes(value: bigint, length: number): Uint8Array {
|
|
602
|
-
const bytes = new Uint8Array(length)
|
|
603
|
-
for (let i = length - 1; i >= 0; i--) {
|
|
604
|
-
bytes[i] = Number(value & 0xffn)
|
|
605
|
-
value >>= 8n
|
|
606
|
-
}
|
|
607
|
-
return bytes
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Convert a secp256k1 public key to an Ethereum address
|
|
612
|
-
*
|
|
613
|
-
* Algorithm (EIP-5564 style):
|
|
614
|
-
* 1. Decompress the public key to uncompressed form (65 bytes)
|
|
615
|
-
* 2. Remove the 0x04 prefix (take last 64 bytes)
|
|
616
|
-
* 3. keccak256 hash of the 64 bytes
|
|
617
|
-
* 4. Take the last 20 bytes as the address
|
|
618
|
-
*
|
|
619
|
-
* @param publicKey - Compressed (33 bytes) or uncompressed (65 bytes) public key
|
|
620
|
-
* @returns Ethereum address (20 bytes, checksummed)
|
|
621
|
-
*/
|
|
622
|
-
export function publicKeyToEthAddress(publicKey: HexString): HexString {
|
|
623
|
-
// Remove 0x prefix if present
|
|
624
|
-
const keyHex = publicKey.startsWith('0x') ? publicKey.slice(2) : publicKey
|
|
625
|
-
const keyBytes = hexToBytes(keyHex)
|
|
626
|
-
|
|
627
|
-
let uncompressedBytes: Uint8Array
|
|
628
|
-
|
|
629
|
-
// Check if compressed (33 bytes) or uncompressed (65 bytes)
|
|
630
|
-
if (keyBytes.length === 33) {
|
|
631
|
-
// Decompress using secp256k1
|
|
632
|
-
const point = secp256k1.ProjectivePoint.fromHex(keyBytes)
|
|
633
|
-
uncompressedBytes = point.toRawBytes(false) // false = uncompressed
|
|
634
|
-
} else if (keyBytes.length === 65) {
|
|
635
|
-
uncompressedBytes = keyBytes
|
|
636
|
-
} else {
|
|
637
|
-
throw new ValidationError(
|
|
638
|
-
`invalid public key length: ${keyBytes.length}, expected 33 (compressed) or 65 (uncompressed)`,
|
|
639
|
-
'publicKey'
|
|
640
|
-
)
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Remove the 0x04 prefix (first byte of uncompressed key)
|
|
644
|
-
const pubKeyWithoutPrefix = uncompressedBytes.slice(1)
|
|
645
|
-
|
|
646
|
-
// keccak256 hash
|
|
647
|
-
const hash = keccak_256(pubKeyWithoutPrefix)
|
|
648
|
-
|
|
649
|
-
// Take last 20 bytes
|
|
650
|
-
const addressBytes = hash.slice(-20)
|
|
651
|
-
|
|
652
|
-
// Convert to checksummed address
|
|
653
|
-
return toChecksumAddress(`0x${bytesToHex(addressBytes)}`)
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Convert address to EIP-55 checksummed format
|
|
658
|
-
*/
|
|
659
|
-
function toChecksumAddress(address: string): HexString {
|
|
660
|
-
const addr = address.toLowerCase().replace('0x', '')
|
|
661
|
-
const hash = bytesToHex(keccak_256(new TextEncoder().encode(addr)))
|
|
662
|
-
|
|
663
|
-
let checksummed = '0x'
|
|
664
|
-
for (let i = 0; i < addr.length; i++) {
|
|
665
|
-
if (parseInt(hash[i], 16) >= 8) {
|
|
666
|
-
checksummed += addr[i].toUpperCase()
|
|
667
|
-
} else {
|
|
668
|
-
checksummed += addr[i]
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return checksummed as HexString
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
676
|
-
// ED25519 STEALTH ADDRESSES
|
|
677
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
678
|
-
//
|
|
679
|
-
// ed25519 stealth address implementation for Solana and NEAR chains.
|
|
680
|
-
// Uses DKSAP (Dual-Key Stealth Address Protocol) pattern adapted for ed25519.
|
|
681
|
-
//
|
|
682
|
-
// Key differences from secp256k1:
|
|
683
|
-
// - Public keys are 32 bytes (not 33 compressed)
|
|
684
|
-
// - Uses SHA-512 for key derivation (matches ed25519 spec)
|
|
685
|
-
// - Scalar arithmetic modulo ed25519 curve order (L)
|
|
686
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* ed25519 curve order (L) - the order of the base point
|
|
690
|
-
*/
|
|
691
|
-
const ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Chains that use ed25519 for stealth addresses
|
|
695
|
-
*/
|
|
696
|
-
const ED25519_CHAINS: ChainId[] = ['solana', 'near', 'aptos', 'sui']
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Check if a chain uses ed25519 for stealth addresses
|
|
700
|
-
*/
|
|
701
|
-
export function isEd25519Chain(chain: ChainId): boolean {
|
|
702
|
-
return ED25519_CHAINS.includes(chain)
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Curve type used for stealth addresses
|
|
707
|
-
*/
|
|
708
|
-
export type StealthCurve = 'secp256k1' | 'ed25519'
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Get the curve type used by a chain for stealth addresses
|
|
712
|
-
*
|
|
713
|
-
* @param chain - Chain identifier
|
|
714
|
-
* @returns 'ed25519' for Solana/NEAR, 'secp256k1' for EVM chains
|
|
715
|
-
*/
|
|
716
|
-
export function getCurveForChain(chain: ChainId): StealthCurve {
|
|
717
|
-
return isEd25519Chain(chain) ? 'ed25519' : 'secp256k1'
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Validate an ed25519 StealthMetaAddress object
|
|
722
|
-
*/
|
|
723
|
-
function validateEd25519StealthMetaAddress(
|
|
724
|
-
metaAddress: StealthMetaAddress,
|
|
725
|
-
field: string = 'recipientMetaAddress'
|
|
726
|
-
): void {
|
|
727
|
-
if (!metaAddress || typeof metaAddress !== 'object') {
|
|
728
|
-
throw new ValidationError('must be an object', field)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Validate chain is ed25519-compatible
|
|
732
|
-
if (!isValidChainId(metaAddress.chain)) {
|
|
733
|
-
throw new ValidationError(
|
|
734
|
-
`invalid chain '${metaAddress.chain}'`,
|
|
735
|
-
`${field}.chain`
|
|
736
|
-
)
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (!isEd25519Chain(metaAddress.chain)) {
|
|
740
|
-
throw new ValidationError(
|
|
741
|
-
`chain '${metaAddress.chain}' does not use ed25519, use secp256k1 functions instead`,
|
|
742
|
-
`${field}.chain`
|
|
743
|
-
)
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Validate spending key (32 bytes for ed25519)
|
|
747
|
-
if (!isValidEd25519PublicKey(metaAddress.spendingKey)) {
|
|
748
|
-
throw new ValidationError(
|
|
749
|
-
'spendingKey must be a valid ed25519 public key (32 bytes)',
|
|
750
|
-
`${field}.spendingKey`
|
|
751
|
-
)
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// Validate viewing key (32 bytes for ed25519)
|
|
755
|
-
if (!isValidEd25519PublicKey(metaAddress.viewingKey)) {
|
|
756
|
-
throw new ValidationError(
|
|
757
|
-
'viewingKey must be a valid ed25519 public key (32 bytes)',
|
|
758
|
-
`${field}.viewingKey`
|
|
759
|
-
)
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/**
|
|
764
|
-
* Validate an ed25519 StealthAddress object
|
|
765
|
-
*/
|
|
766
|
-
function validateEd25519StealthAddress(
|
|
767
|
-
stealthAddress: StealthAddress,
|
|
768
|
-
field: string = 'stealthAddress'
|
|
769
|
-
): void {
|
|
770
|
-
if (!stealthAddress || typeof stealthAddress !== 'object') {
|
|
771
|
-
throw new ValidationError('must be an object', field)
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Validate address (32-byte ed25519 public key)
|
|
775
|
-
if (!isValidEd25519PublicKey(stealthAddress.address)) {
|
|
776
|
-
throw new ValidationError(
|
|
777
|
-
'address must be a valid ed25519 public key (32 bytes)',
|
|
778
|
-
`${field}.address`
|
|
779
|
-
)
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Validate ephemeral public key (32 bytes for ed25519)
|
|
783
|
-
if (!isValidEd25519PublicKey(stealthAddress.ephemeralPublicKey)) {
|
|
784
|
-
throw new ValidationError(
|
|
785
|
-
'ephemeralPublicKey must be a valid ed25519 public key (32 bytes)',
|
|
786
|
-
`${field}.ephemeralPublicKey`
|
|
787
|
-
)
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Validate view tag (0-255)
|
|
791
|
-
if (typeof stealthAddress.viewTag !== 'number' ||
|
|
792
|
-
!Number.isInteger(stealthAddress.viewTag) ||
|
|
793
|
-
stealthAddress.viewTag < 0 ||
|
|
794
|
-
stealthAddress.viewTag > 255) {
|
|
795
|
-
throw new ValidationError(
|
|
796
|
-
'viewTag must be an integer between 0 and 255',
|
|
797
|
-
`${field}.viewTag`
|
|
798
|
-
)
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Get the scalar from an ed25519 private key
|
|
804
|
-
*
|
|
805
|
-
* ed25519 key derivation:
|
|
806
|
-
* 1. Hash the 32-byte seed with SHA-512 to get 64 bytes
|
|
807
|
-
* 2. First 32 bytes are the scalar (after clamping)
|
|
808
|
-
* 3. Last 32 bytes are used for nonce generation (not needed here)
|
|
809
|
-
*/
|
|
810
|
-
function getEd25519Scalar(privateKey: Uint8Array): bigint {
|
|
811
|
-
// Hash the private key seed with SHA-512
|
|
812
|
-
const hash = sha512(privateKey)
|
|
813
|
-
|
|
814
|
-
// Take first 32 bytes and clamp as per ed25519 spec
|
|
815
|
-
const scalar = hash.slice(0, 32)
|
|
816
|
-
|
|
817
|
-
// Clamp: clear lowest 3 bits, clear highest bit, set second highest bit
|
|
818
|
-
scalar[0] &= 248
|
|
819
|
-
scalar[31] &= 127
|
|
820
|
-
scalar[31] |= 64
|
|
821
|
-
|
|
822
|
-
// Convert to bigint (little-endian for ed25519)
|
|
823
|
-
return bytesToBigIntLE(scalar)
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* Convert bytes to bigint (little-endian, used by ed25519)
|
|
828
|
-
*/
|
|
829
|
-
function bytesToBigIntLE(bytes: Uint8Array): bigint {
|
|
830
|
-
let result = 0n
|
|
831
|
-
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
832
|
-
result = (result << 8n) + BigInt(bytes[i])
|
|
833
|
-
}
|
|
834
|
-
return result
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* Convert bigint to bytes (little-endian, used by ed25519)
|
|
839
|
-
*/
|
|
840
|
-
function bigIntToBytesLE(value: bigint, length: number): Uint8Array {
|
|
841
|
-
const bytes = new Uint8Array(length)
|
|
842
|
-
for (let i = 0; i < length; i++) {
|
|
843
|
-
bytes[i] = Number(value & 0xffn)
|
|
844
|
-
value >>= 8n
|
|
845
|
-
}
|
|
846
|
-
return bytes
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Generate a new ed25519 stealth meta-address keypair
|
|
851
|
-
*
|
|
852
|
-
* @param chain - Target chain (must be ed25519-compatible: solana, near)
|
|
853
|
-
* @param label - Optional human-readable label
|
|
854
|
-
* @returns Stealth meta-address and private keys
|
|
855
|
-
* @throws {ValidationError} If chain is invalid or not ed25519-compatible
|
|
856
|
-
*/
|
|
857
|
-
export function generateEd25519StealthMetaAddress(
|
|
858
|
-
chain: ChainId,
|
|
859
|
-
label?: string,
|
|
860
|
-
): {
|
|
861
|
-
metaAddress: StealthMetaAddress
|
|
862
|
-
spendingPrivateKey: HexString
|
|
863
|
-
viewingPrivateKey: HexString
|
|
864
|
-
} {
|
|
865
|
-
// Validate chain
|
|
866
|
-
if (!isValidChainId(chain)) {
|
|
867
|
-
throw new ValidationError(
|
|
868
|
-
`invalid chain '${chain}', must be one of: solana, ethereum, near, zcash, polygon, arbitrum, optimism, base, bitcoin, aptos, sui, cosmos, osmosis, injective, celestia, sei, dydx`,
|
|
869
|
-
'chain'
|
|
870
|
-
)
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
if (!isEd25519Chain(chain)) {
|
|
874
|
-
throw new ValidationError(
|
|
875
|
-
`chain '${chain}' does not use ed25519, use generateStealthMetaAddress() for secp256k1 chains`,
|
|
876
|
-
'chain'
|
|
877
|
-
)
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Generate random private keys (32-byte seeds)
|
|
881
|
-
const spendingPrivateKey = randomBytes(32)
|
|
882
|
-
const viewingPrivateKey = randomBytes(32)
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
// Derive public keys using ed25519
|
|
886
|
-
const spendingKey = ed25519.getPublicKey(spendingPrivateKey)
|
|
887
|
-
const viewingKey = ed25519.getPublicKey(viewingPrivateKey)
|
|
888
|
-
|
|
889
|
-
// Convert to hex strings before wiping buffers
|
|
890
|
-
const result = {
|
|
891
|
-
metaAddress: {
|
|
892
|
-
spendingKey: `0x${bytesToHex(spendingKey)}` as HexString,
|
|
893
|
-
viewingKey: `0x${bytesToHex(viewingKey)}` as HexString,
|
|
894
|
-
chain,
|
|
895
|
-
label,
|
|
896
|
-
},
|
|
897
|
-
spendingPrivateKey: `0x${bytesToHex(spendingPrivateKey)}` as HexString,
|
|
898
|
-
viewingPrivateKey: `0x${bytesToHex(viewingPrivateKey)}` as HexString,
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
return result
|
|
902
|
-
} finally {
|
|
903
|
-
// Securely wipe private key buffers
|
|
904
|
-
secureWipeAll(spendingPrivateKey, viewingPrivateKey)
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
/**
|
|
909
|
-
* Generate a one-time ed25519 stealth address for a recipient
|
|
910
|
-
*
|
|
911
|
-
* Algorithm (DKSAP for ed25519):
|
|
912
|
-
* 1. Generate ephemeral keypair (r, R = r*G)
|
|
913
|
-
* 2. Compute shared secret: S = r * P_spend (ephemeral scalar * spending public)
|
|
914
|
-
* 3. Hash shared secret: h = SHA256(S)
|
|
915
|
-
* 4. Derive stealth public key: P_stealth = P_view + h*G
|
|
916
|
-
*
|
|
917
|
-
* @param recipientMetaAddress - Recipient's published stealth meta-address
|
|
918
|
-
* @returns Stealth address data (address + ephemeral key for publication)
|
|
919
|
-
* @throws {ValidationError} If recipientMetaAddress is invalid
|
|
920
|
-
*/
|
|
921
|
-
export function generateEd25519StealthAddress(
|
|
922
|
-
recipientMetaAddress: StealthMetaAddress,
|
|
923
|
-
): {
|
|
924
|
-
stealthAddress: StealthAddress
|
|
925
|
-
sharedSecret: HexString
|
|
926
|
-
} {
|
|
927
|
-
// Validate input
|
|
928
|
-
validateEd25519StealthMetaAddress(recipientMetaAddress)
|
|
929
|
-
|
|
930
|
-
// Generate ephemeral keypair
|
|
931
|
-
const ephemeralPrivateKey = randomBytes(32)
|
|
932
|
-
|
|
933
|
-
try {
|
|
934
|
-
const ephemeralPublicKey = ed25519.getPublicKey(ephemeralPrivateKey)
|
|
935
|
-
|
|
936
|
-
// Parse recipient's keys (remove 0x prefix)
|
|
937
|
-
const spendingKeyBytes = hexToBytes(recipientMetaAddress.spendingKey.slice(2))
|
|
938
|
-
const viewingKeyBytes = hexToBytes(recipientMetaAddress.viewingKey.slice(2))
|
|
939
|
-
|
|
940
|
-
// Get ephemeral scalar from private key and reduce mod L
|
|
941
|
-
// ed25519 clamping produces values that may exceed L, so we reduce
|
|
942
|
-
const rawEphemeralScalar = getEd25519Scalar(ephemeralPrivateKey)
|
|
943
|
-
const ephemeralScalar = rawEphemeralScalar % ED25519_ORDER
|
|
944
|
-
if (ephemeralScalar === 0n) {
|
|
945
|
-
throw new Error('CRITICAL: Zero ephemeral scalar after reduction - investigate RNG')
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// Convert spending public key to extended point and multiply by ephemeral scalar
|
|
949
|
-
// S = ephemeral_scalar * P_spend
|
|
950
|
-
const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes)
|
|
951
|
-
const sharedSecretPoint = spendingPoint.multiply(ephemeralScalar)
|
|
952
|
-
|
|
953
|
-
// Hash the shared secret point (compress to bytes first)
|
|
954
|
-
const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
|
|
955
|
-
|
|
956
|
-
// Derive stealth public key: P_stealth = P_view + hash(S)*G
|
|
957
|
-
// Convert hash to scalar (mod L to ensure it's valid and non-zero)
|
|
958
|
-
const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
|
|
959
|
-
if (hashScalar === 0n) {
|
|
960
|
-
throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Compute hash(S) * G
|
|
964
|
-
const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar)
|
|
965
|
-
|
|
966
|
-
// Add to viewing key: P_stealth = P_view + hash(S)*G
|
|
967
|
-
const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes)
|
|
968
|
-
const stealthPoint = viewingPoint.add(hashTimesG)
|
|
969
|
-
const stealthAddressBytes = stealthPoint.toRawBytes()
|
|
970
|
-
|
|
971
|
-
// Compute view tag (first byte of hash for efficient scanning)
|
|
972
|
-
const viewTag = sharedSecretHash[0]
|
|
973
|
-
|
|
974
|
-
return {
|
|
975
|
-
stealthAddress: {
|
|
976
|
-
address: `0x${bytesToHex(stealthAddressBytes)}` as HexString,
|
|
977
|
-
ephemeralPublicKey: `0x${bytesToHex(ephemeralPublicKey)}` as HexString,
|
|
978
|
-
viewTag,
|
|
979
|
-
},
|
|
980
|
-
sharedSecret: `0x${bytesToHex(sharedSecretHash)}` as HexString,
|
|
981
|
-
}
|
|
982
|
-
} finally {
|
|
983
|
-
// Securely wipe ephemeral private key
|
|
984
|
-
secureWipe(ephemeralPrivateKey)
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* Derive the private key for an ed25519 stealth address (for recipient to claim funds)
|
|
990
|
-
*
|
|
991
|
-
* Algorithm:
|
|
992
|
-
* 1. Compute shared secret: S = spend_scalar * R (spending scalar * ephemeral public)
|
|
993
|
-
* 2. Hash shared secret: h = SHA256(S)
|
|
994
|
-
* 3. Derive stealth private key: s_stealth = s_view + h (mod L)
|
|
995
|
-
*
|
|
996
|
-
* **IMPORTANT: Derived Key Format**
|
|
997
|
-
*
|
|
998
|
-
* The returned `privateKey` is a **raw scalar** in little-endian format, NOT a standard
|
|
999
|
-
* ed25519 seed. This is because the stealth private key is derived mathematically
|
|
1000
|
-
* (s_view + hash), not generated from a seed.
|
|
1001
|
-
*
|
|
1002
|
-
* To compute the public key from the derived private key:
|
|
1003
|
-
* ```typescript
|
|
1004
|
-
* // CORRECT: Direct scalar multiplication
|
|
1005
|
-
* const scalar = bytesToBigIntLE(hexToBytes(privateKey.slice(2)))
|
|
1006
|
-
* const publicKey = ed25519.ExtendedPoint.BASE.multiply(scalar)
|
|
1007
|
-
*
|
|
1008
|
-
* // WRONG: Do NOT use ed25519.getPublicKey() - it will hash and clamp the input,
|
|
1009
|
-
* // producing a different (incorrect) public key
|
|
1010
|
-
* ```
|
|
1011
|
-
*
|
|
1012
|
-
* @param stealthAddress - The stealth address to recover
|
|
1013
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
1014
|
-
* @param viewingPrivateKey - Recipient's viewing private key
|
|
1015
|
-
* @returns Recovery data including derived private key (raw scalar, little-endian)
|
|
1016
|
-
* @throws {ValidationError} If any input is invalid
|
|
1017
|
-
*/
|
|
1018
|
-
export function deriveEd25519StealthPrivateKey(
|
|
1019
|
-
stealthAddress: StealthAddress,
|
|
1020
|
-
spendingPrivateKey: HexString,
|
|
1021
|
-
viewingPrivateKey: HexString,
|
|
1022
|
-
): StealthAddressRecovery {
|
|
1023
|
-
// Validate stealth address
|
|
1024
|
-
validateEd25519StealthAddress(stealthAddress)
|
|
1025
|
-
|
|
1026
|
-
// Validate private keys (32 bytes)
|
|
1027
|
-
if (!isValidPrivateKey(spendingPrivateKey)) {
|
|
1028
|
-
throw new ValidationError(
|
|
1029
|
-
'must be a valid 32-byte hex string',
|
|
1030
|
-
'spendingPrivateKey'
|
|
1031
|
-
)
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
1035
|
-
throw new ValidationError(
|
|
1036
|
-
'must be a valid 32-byte hex string',
|
|
1037
|
-
'viewingPrivateKey'
|
|
1038
|
-
)
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Parse keys
|
|
1042
|
-
const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
1043
|
-
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
1044
|
-
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
1045
|
-
|
|
1046
|
-
try {
|
|
1047
|
-
// Get spending scalar from private key and reduce mod L
|
|
1048
|
-
const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes)
|
|
1049
|
-
const spendingScalar = rawSpendingScalar % ED25519_ORDER
|
|
1050
|
-
if (spendingScalar === 0n) {
|
|
1051
|
-
throw new Error('CRITICAL: Zero spending scalar after reduction - investigate key derivation')
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// Compute shared secret: S = spending_scalar * R
|
|
1055
|
-
const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes)
|
|
1056
|
-
const sharedSecretPoint = ephemeralPoint.multiply(spendingScalar)
|
|
1057
|
-
|
|
1058
|
-
// Hash the shared secret
|
|
1059
|
-
const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
|
|
1060
|
-
|
|
1061
|
-
// Get viewing scalar from private key and reduce mod L
|
|
1062
|
-
const rawViewingScalar = getEd25519Scalar(viewingPrivBytes)
|
|
1063
|
-
const viewingScalar = rawViewingScalar % ED25519_ORDER
|
|
1064
|
-
if (viewingScalar === 0n) {
|
|
1065
|
-
throw new Error('CRITICAL: Zero viewing scalar after reduction - investigate key derivation')
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// Derive stealth private key: s_stealth = s_view + hash(S) mod L
|
|
1069
|
-
const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
|
|
1070
|
-
if (hashScalar === 0n) {
|
|
1071
|
-
throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
|
|
1072
|
-
}
|
|
1073
|
-
const stealthPrivateScalar = (viewingScalar + hashScalar) % ED25519_ORDER
|
|
1074
|
-
if (stealthPrivateScalar === 0n) {
|
|
1075
|
-
throw new Error('CRITICAL: Zero stealth scalar after reduction - investigate key derivation')
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// Convert back to bytes (little-endian for ed25519)
|
|
1079
|
-
// Note: We need to store this as a seed that will produce this scalar
|
|
1080
|
-
// For simplicity, we store the scalar directly (32 bytes, little-endian)
|
|
1081
|
-
const stealthPrivateKey = bigIntToBytesLE(stealthPrivateScalar, 32)
|
|
1082
|
-
|
|
1083
|
-
const result = {
|
|
1084
|
-
stealthAddress: stealthAddress.address,
|
|
1085
|
-
ephemeralPublicKey: stealthAddress.ephemeralPublicKey,
|
|
1086
|
-
privateKey: `0x${bytesToHex(stealthPrivateKey)}` as HexString,
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// Wipe derived key buffer after converting to hex
|
|
1090
|
-
secureWipe(stealthPrivateKey)
|
|
1091
|
-
|
|
1092
|
-
return result
|
|
1093
|
-
} finally {
|
|
1094
|
-
// Securely wipe input private key buffers
|
|
1095
|
-
secureWipeAll(spendingPrivBytes, viewingPrivBytes)
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Check if an ed25519 stealth address was intended for this recipient
|
|
1101
|
-
* Uses view tag for efficient filtering before full computation
|
|
1102
|
-
*
|
|
1103
|
-
* @param stealthAddress - Stealth address to check
|
|
1104
|
-
* @param spendingPrivateKey - Recipient's spending private key
|
|
1105
|
-
* @param viewingPrivateKey - Recipient's viewing private key
|
|
1106
|
-
* @returns true if this address belongs to the recipient
|
|
1107
|
-
* @throws {ValidationError} If any input is invalid
|
|
1108
|
-
*/
|
|
1109
|
-
export function checkEd25519StealthAddress(
|
|
1110
|
-
stealthAddress: StealthAddress,
|
|
1111
|
-
spendingPrivateKey: HexString,
|
|
1112
|
-
viewingPrivateKey: HexString,
|
|
1113
|
-
): boolean {
|
|
1114
|
-
// Validate stealth address
|
|
1115
|
-
validateEd25519StealthAddress(stealthAddress)
|
|
1116
|
-
|
|
1117
|
-
// Validate private keys
|
|
1118
|
-
if (!isValidPrivateKey(spendingPrivateKey)) {
|
|
1119
|
-
throw new ValidationError(
|
|
1120
|
-
'must be a valid 32-byte hex string',
|
|
1121
|
-
'spendingPrivateKey'
|
|
1122
|
-
)
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
1126
|
-
throw new ValidationError(
|
|
1127
|
-
'must be a valid 32-byte hex string',
|
|
1128
|
-
'viewingPrivateKey'
|
|
1129
|
-
)
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Parse keys
|
|
1133
|
-
const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
1134
|
-
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
1135
|
-
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
1136
|
-
|
|
1137
|
-
try {
|
|
1138
|
-
// Get spending scalar from private key and reduce mod L
|
|
1139
|
-
const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes)
|
|
1140
|
-
const spendingScalar = rawSpendingScalar % ED25519_ORDER
|
|
1141
|
-
if (spendingScalar === 0n) {
|
|
1142
|
-
throw new Error('CRITICAL: Zero spending scalar after reduction - investigate key derivation')
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// Compute shared secret: S = spending_scalar * R
|
|
1146
|
-
const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes)
|
|
1147
|
-
const sharedSecretPoint = ephemeralPoint.multiply(spendingScalar)
|
|
1148
|
-
|
|
1149
|
-
// Hash the shared secret
|
|
1150
|
-
const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
|
|
1151
|
-
|
|
1152
|
-
// View tag check (optimization - reject quickly if doesn't match)
|
|
1153
|
-
if (sharedSecretHash[0] !== stealthAddress.viewTag) {
|
|
1154
|
-
return false
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// Full check: derive the expected stealth address
|
|
1158
|
-
const rawViewingScalar = getEd25519Scalar(viewingPrivBytes)
|
|
1159
|
-
const viewingScalar = rawViewingScalar % ED25519_ORDER
|
|
1160
|
-
if (viewingScalar === 0n) {
|
|
1161
|
-
throw new Error('CRITICAL: Zero viewing scalar after reduction - investigate key derivation')
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
|
|
1165
|
-
if (hashScalar === 0n) {
|
|
1166
|
-
throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
|
|
1167
|
-
}
|
|
1168
|
-
const stealthPrivateScalar = (viewingScalar + hashScalar) % ED25519_ORDER
|
|
1169
|
-
if (stealthPrivateScalar === 0n) {
|
|
1170
|
-
throw new Error('CRITICAL: Zero stealth scalar after reduction - investigate key derivation')
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// Compute expected public key from derived scalar
|
|
1174
|
-
const expectedPubKey = ed25519.ExtendedPoint.BASE.multiply(stealthPrivateScalar)
|
|
1175
|
-
const expectedPubKeyBytes = expectedPubKey.toRawBytes()
|
|
1176
|
-
|
|
1177
|
-
// Compare with provided stealth address
|
|
1178
|
-
const providedAddress = hexToBytes(stealthAddress.address.slice(2))
|
|
1179
|
-
|
|
1180
|
-
return bytesToHex(expectedPubKeyBytes) === bytesToHex(providedAddress)
|
|
1181
|
-
} finally {
|
|
1182
|
-
// Securely wipe input private key buffers
|
|
1183
|
-
secureWipeAll(spendingPrivBytes, viewingPrivBytes)
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// ─── Base58 Encoding for Solana ────────────────────────────────────────────────
|
|
1188
|
-
|
|
1189
|
-
/** Base58 alphabet (Bitcoin/Solana standard) */
|
|
1190
|
-
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
1191
|
-
|
|
1192
|
-
/**
|
|
1193
|
-
* Encode bytes to base58 string
|
|
1194
|
-
* Used for Solana address encoding
|
|
1195
|
-
*/
|
|
1196
|
-
function bytesToBase58(bytes: Uint8Array): string {
|
|
1197
|
-
// Count leading zeros
|
|
1198
|
-
let leadingZeros = 0
|
|
1199
|
-
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
|
|
1200
|
-
leadingZeros++
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Convert bytes to bigint
|
|
1204
|
-
let value = 0n
|
|
1205
|
-
for (const byte of bytes) {
|
|
1206
|
-
value = value * 256n + BigInt(byte)
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// Convert to base58
|
|
1210
|
-
let result = ''
|
|
1211
|
-
while (value > 0n) {
|
|
1212
|
-
const remainder = value % 58n
|
|
1213
|
-
value = value / 58n
|
|
1214
|
-
result = BASE58_ALPHABET[Number(remainder)] + result
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
// Add leading '1's for each leading zero byte
|
|
1218
|
-
return '1'.repeat(leadingZeros) + result
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
/**
|
|
1222
|
-
* Decode base58 string to bytes
|
|
1223
|
-
* Used for Solana address validation
|
|
1224
|
-
*/
|
|
1225
|
-
function base58ToBytes(str: string): Uint8Array {
|
|
1226
|
-
// Count leading '1's (they represent leading zero bytes)
|
|
1227
|
-
let leadingOnes = 0
|
|
1228
|
-
for (let i = 0; i < str.length && str[i] === '1'; i++) {
|
|
1229
|
-
leadingOnes++
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Convert from base58 to bigint
|
|
1233
|
-
let value = 0n
|
|
1234
|
-
for (const char of str) {
|
|
1235
|
-
const index = BASE58_ALPHABET.indexOf(char)
|
|
1236
|
-
if (index === -1) {
|
|
1237
|
-
throw new ValidationError(`Invalid base58 character: ${char}`, 'address')
|
|
1238
|
-
}
|
|
1239
|
-
value = value * 58n + BigInt(index)
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// Convert bigint to bytes
|
|
1243
|
-
const bytes: number[] = []
|
|
1244
|
-
while (value > 0n) {
|
|
1245
|
-
bytes.unshift(Number(value % 256n))
|
|
1246
|
-
value = value / 256n
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Add leading zeros
|
|
1250
|
-
const result = new Uint8Array(leadingOnes + bytes.length)
|
|
1251
|
-
for (let i = 0; i < leadingOnes; i++) {
|
|
1252
|
-
result[i] = 0
|
|
1253
|
-
}
|
|
1254
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
1255
|
-
result[leadingOnes + i] = bytes[i]
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
return result
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// ─── Solana Address Derivation ─────────────────────────────────────────────────
|
|
1262
|
-
|
|
1263
|
-
/**
|
|
1264
|
-
* Convert an ed25519 public key (hex) to a Solana address (base58)
|
|
1265
|
-
*
|
|
1266
|
-
* Solana addresses are base58-encoded 32-byte ed25519 public keys.
|
|
1267
|
-
*
|
|
1268
|
-
* @param publicKey - 32-byte ed25519 public key as hex string (with 0x prefix)
|
|
1269
|
-
* @returns Base58-encoded Solana address
|
|
1270
|
-
* @throws {ValidationError} If public key is invalid
|
|
1271
|
-
*
|
|
1272
|
-
* @example
|
|
1273
|
-
* ```typescript
|
|
1274
|
-
* const { stealthAddress } = generateEd25519StealthAddress(metaAddress)
|
|
1275
|
-
* const solanaAddress = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
1276
|
-
* // Returns: "7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPBJCyqLqzQPvN" (example)
|
|
1277
|
-
* ```
|
|
1278
|
-
*/
|
|
1279
|
-
export function ed25519PublicKeyToSolanaAddress(publicKey: HexString): string {
|
|
1280
|
-
// Validate input
|
|
1281
|
-
if (!isValidHex(publicKey)) {
|
|
1282
|
-
throw new ValidationError(
|
|
1283
|
-
'publicKey must be a valid hex string with 0x prefix',
|
|
1284
|
-
'publicKey'
|
|
1285
|
-
)
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
if (!isValidEd25519PublicKey(publicKey)) {
|
|
1289
|
-
throw new ValidationError(
|
|
1290
|
-
'publicKey must be 32 bytes (64 hex characters)',
|
|
1291
|
-
'publicKey'
|
|
1292
|
-
)
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// Convert hex to bytes (remove 0x prefix)
|
|
1296
|
-
const publicKeyBytes = hexToBytes(publicKey.slice(2))
|
|
1297
|
-
|
|
1298
|
-
// Encode as base58
|
|
1299
|
-
return bytesToBase58(publicKeyBytes)
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
/**
|
|
1303
|
-
* Validate a Solana address format
|
|
1304
|
-
*
|
|
1305
|
-
* Checks that the address:
|
|
1306
|
-
* - Is a valid base58 string
|
|
1307
|
-
* - Decodes to exactly 32 bytes (ed25519 public key size)
|
|
1308
|
-
*
|
|
1309
|
-
* @param address - Base58-encoded Solana address
|
|
1310
|
-
* @returns true if valid, false otherwise
|
|
1311
|
-
*
|
|
1312
|
-
* @example
|
|
1313
|
-
* ```typescript
|
|
1314
|
-
* isValidSolanaAddress('7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPBJCyqLqzQPvN') // true
|
|
1315
|
-
* isValidSolanaAddress('invalid') // false
|
|
1316
|
-
* ```
|
|
1317
|
-
*/
|
|
1318
|
-
export function isValidSolanaAddress(address: string): boolean {
|
|
1319
|
-
if (typeof address !== 'string' || address.length === 0) {
|
|
1320
|
-
return false
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
// Solana addresses are typically 32-44 characters
|
|
1324
|
-
if (address.length < 32 || address.length > 44) {
|
|
1325
|
-
return false
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
try {
|
|
1329
|
-
const decoded = base58ToBytes(address)
|
|
1330
|
-
// Valid Solana address is exactly 32 bytes
|
|
1331
|
-
return decoded.length === 32
|
|
1332
|
-
} catch {
|
|
1333
|
-
return false
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
/**
|
|
1338
|
-
* Convert a Solana address (base58) back to ed25519 public key (hex)
|
|
1339
|
-
*
|
|
1340
|
-
* @param address - Base58-encoded Solana address
|
|
1341
|
-
* @returns 32-byte ed25519 public key as hex string (with 0x prefix)
|
|
1342
|
-
* @throws {ValidationError} If address is invalid
|
|
1343
|
-
*
|
|
1344
|
-
* @example
|
|
1345
|
-
* ```typescript
|
|
1346
|
-
* const publicKey = solanaAddressToEd25519PublicKey('7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPBJCyqLqzQPvN')
|
|
1347
|
-
* // Returns: "0x..." (64 hex characters)
|
|
1348
|
-
* ```
|
|
1349
|
-
*/
|
|
1350
|
-
export function solanaAddressToEd25519PublicKey(address: string): HexString {
|
|
1351
|
-
if (!isValidSolanaAddress(address)) {
|
|
1352
|
-
throw new ValidationError(
|
|
1353
|
-
'Invalid Solana address format',
|
|
1354
|
-
'address'
|
|
1355
|
-
)
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
const decoded = base58ToBytes(address)
|
|
1359
|
-
return `0x${bytesToHex(decoded)}` as HexString
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// ─── NEAR Address Derivation ────────────────────────────────────────────────────
|
|
1363
|
-
|
|
1364
|
-
/**
|
|
1365
|
-
* Convert ed25519 public key to NEAR implicit account address
|
|
1366
|
-
*
|
|
1367
|
-
* NEAR implicit accounts are lowercase hex-encoded ed25519 public keys (64 characters).
|
|
1368
|
-
* No prefix, just raw 32 bytes as lowercase hex.
|
|
1369
|
-
*
|
|
1370
|
-
* @param publicKey - 32-byte ed25519 public key as hex string (with 0x prefix)
|
|
1371
|
-
* @returns NEAR implicit account address (64 lowercase hex characters, no prefix)
|
|
1372
|
-
* @throws {ValidationError} If public key is invalid
|
|
1373
|
-
*
|
|
1374
|
-
* @example
|
|
1375
|
-
* ```typescript
|
|
1376
|
-
* const { stealthAddress } = generateEd25519StealthAddress(metaAddress)
|
|
1377
|
-
* const nearAddress = ed25519PublicKeyToNearAddress(stealthAddress.address)
|
|
1378
|
-
* // Returns: "ab12cd34..." (64 hex chars)
|
|
1379
|
-
* ```
|
|
1380
|
-
*/
|
|
1381
|
-
export function ed25519PublicKeyToNearAddress(publicKey: HexString): string {
|
|
1382
|
-
// Validate input
|
|
1383
|
-
if (!isValidHex(publicKey)) {
|
|
1384
|
-
throw new ValidationError(
|
|
1385
|
-
'publicKey must be a valid hex string with 0x prefix',
|
|
1386
|
-
'publicKey'
|
|
1387
|
-
)
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
if (!isValidEd25519PublicKey(publicKey)) {
|
|
1391
|
-
throw new ValidationError(
|
|
1392
|
-
'publicKey must be 32 bytes (64 hex characters)',
|
|
1393
|
-
'publicKey'
|
|
1394
|
-
)
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// NEAR implicit accounts are lowercase hex without 0x prefix
|
|
1398
|
-
return publicKey.slice(2).toLowerCase()
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
/**
|
|
1402
|
-
* Convert NEAR implicit account address back to ed25519 public key
|
|
1403
|
-
*
|
|
1404
|
-
* @param address - NEAR implicit account address (64 hex characters)
|
|
1405
|
-
* @returns ed25519 public key as HexString (with 0x prefix)
|
|
1406
|
-
* @throws {ValidationError} If address is invalid
|
|
1407
|
-
*
|
|
1408
|
-
* @example
|
|
1409
|
-
* ```typescript
|
|
1410
|
-
* const publicKey = nearAddressToEd25519PublicKey("ab12cd34...")
|
|
1411
|
-
* // Returns: "0xab12cd34..."
|
|
1412
|
-
* ```
|
|
1413
|
-
*/
|
|
1414
|
-
export function nearAddressToEd25519PublicKey(address: string): HexString {
|
|
1415
|
-
if (!isValidNearImplicitAddress(address)) {
|
|
1416
|
-
throw new ValidationError(
|
|
1417
|
-
'Invalid NEAR implicit address format',
|
|
1418
|
-
'address'
|
|
1419
|
-
)
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
return `0x${address.toLowerCase()}` as HexString
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
/**
|
|
1426
|
-
* Validate a NEAR implicit account address
|
|
1427
|
-
*
|
|
1428
|
-
* NEAR implicit accounts are:
|
|
1429
|
-
* - Exactly 64 lowercase hex characters
|
|
1430
|
-
* - No prefix (no "0x")
|
|
1431
|
-
* - Represent a 32-byte ed25519 public key
|
|
1432
|
-
*
|
|
1433
|
-
* @param address - Address to validate
|
|
1434
|
-
* @returns true if valid NEAR implicit account address
|
|
1435
|
-
*
|
|
1436
|
-
* @example
|
|
1437
|
-
* ```typescript
|
|
1438
|
-
* isValidNearImplicitAddress("ab12cd34ef...") // true (64 hex chars)
|
|
1439
|
-
* isValidNearImplicitAddress("0xab12...") // false (has prefix)
|
|
1440
|
-
* isValidNearImplicitAddress("alice.near") // false (named account)
|
|
1441
|
-
* isValidNearImplicitAddress("AB12CD...") // false (uppercase)
|
|
1442
|
-
* ```
|
|
1443
|
-
*/
|
|
1444
|
-
export function isValidNearImplicitAddress(address: string): boolean {
|
|
1445
|
-
// Must be a string
|
|
1446
|
-
if (typeof address !== 'string' || address.length === 0) {
|
|
1447
|
-
return false
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
// Must be exactly 64 characters (32 bytes as hex)
|
|
1451
|
-
if (address.length !== 64) {
|
|
1452
|
-
return false
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// Must be lowercase hex only (no 0x prefix)
|
|
1456
|
-
return /^[0-9a-f]{64}$/.test(address)
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
/**
|
|
1460
|
-
* Check if a string is a valid NEAR account ID (named or implicit)
|
|
1461
|
-
*
|
|
1462
|
-
* Supports both:
|
|
1463
|
-
* - Named accounts: alice.near, bob.testnet
|
|
1464
|
-
* - Implicit accounts: 64 hex characters
|
|
1465
|
-
*
|
|
1466
|
-
* @param accountId - Account ID to validate
|
|
1467
|
-
* @returns true if valid NEAR account ID
|
|
1468
|
-
*
|
|
1469
|
-
* @example
|
|
1470
|
-
* ```typescript
|
|
1471
|
-
* isValidNearAccountId("alice.near") // true
|
|
1472
|
-
* isValidNearAccountId("bob.testnet") // true
|
|
1473
|
-
* isValidNearAccountId("ab12cd34...") // true (64 hex chars)
|
|
1474
|
-
* isValidNearAccountId("ALICE.near") // false (uppercase)
|
|
1475
|
-
* isValidNearAccountId("a") // false (too short)
|
|
1476
|
-
* ```
|
|
1477
|
-
*/
|
|
1478
|
-
export function isValidNearAccountId(accountId: string): boolean {
|
|
1479
|
-
// Must be a string
|
|
1480
|
-
if (typeof accountId !== 'string' || accountId.length === 0) {
|
|
1481
|
-
return false
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// Check if it's a valid implicit account (64 hex chars)
|
|
1485
|
-
if (isValidNearImplicitAddress(accountId)) {
|
|
1486
|
-
return true
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
// Named accounts: 2-64 characters, lowercase alphanumeric with . _ -
|
|
1490
|
-
// Must start and end with alphanumeric
|
|
1491
|
-
if (accountId.length < 2 || accountId.length > 64) {
|
|
1492
|
-
return false
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// NEAR account ID pattern
|
|
1496
|
-
const nearAccountPattern = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/
|
|
1497
|
-
if (!nearAccountPattern.test(accountId)) {
|
|
1498
|
-
return false
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
// Cannot have consecutive dots
|
|
1502
|
-
if (accountId.includes('..')) {
|
|
1503
|
-
return false
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
return true
|
|
1507
|
-
}
|
|
4
|
+
* This file re-exports from the modular stealth/ directory.
|
|
5
|
+
* For implementation details, see:
|
|
6
|
+
* - stealth/secp256k1.ts - EVM chains (Ethereum, Polygon, etc.)
|
|
7
|
+
* - stealth/ed25519.ts - Solana/NEAR/Aptos/Sui
|
|
8
|
+
* - stealth/address-derivation.ts - Chain-specific address formats
|
|
9
|
+
* - stealth/meta-address.ts - Encoding/decoding utilities
|
|
10
|
+
*
|
|
11
|
+
* @module stealth
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Re-export everything from the modular implementation
|
|
15
|
+
export {
|
|
16
|
+
// Unified API (auto-dispatch to correct curve)
|
|
17
|
+
generateStealthMetaAddress,
|
|
18
|
+
generateStealthAddress,
|
|
19
|
+
deriveStealthPrivateKey,
|
|
20
|
+
checkStealthAddress,
|
|
21
|
+
|
|
22
|
+
// Chain detection
|
|
23
|
+
isEd25519Chain,
|
|
24
|
+
getCurveForChain,
|
|
25
|
+
|
|
26
|
+
// ed25519 (Solana, NEAR, Aptos, Sui)
|
|
27
|
+
generateEd25519StealthMetaAddress,
|
|
28
|
+
generateEd25519StealthAddress,
|
|
29
|
+
deriveEd25519StealthPrivateKey,
|
|
30
|
+
checkEd25519StealthAddress,
|
|
31
|
+
|
|
32
|
+
// secp256k1 (Ethereum, Polygon, etc.)
|
|
33
|
+
publicKeyToEthAddress,
|
|
34
|
+
|
|
35
|
+
// Meta-address encoding
|
|
36
|
+
encodeStealthMetaAddress,
|
|
37
|
+
decodeStealthMetaAddress,
|
|
38
|
+
parseStealthAddress,
|
|
39
|
+
|
|
40
|
+
// Solana address derivation
|
|
41
|
+
ed25519PublicKeyToSolanaAddress,
|
|
42
|
+
solanaAddressToEd25519PublicKey,
|
|
43
|
+
isValidSolanaAddress,
|
|
44
|
+
|
|
45
|
+
// NEAR address derivation
|
|
46
|
+
ed25519PublicKeyToNearAddress,
|
|
47
|
+
nearAddressToEd25519PublicKey,
|
|
48
|
+
isValidNearImplicitAddress,
|
|
49
|
+
isValidNearAccountId,
|
|
50
|
+
} from './stealth/index'
|
|
51
|
+
|
|
52
|
+
// Re-export types
|
|
53
|
+
export type { StealthCurve } from './stealth/index'
|