@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.
Files changed (263) hide show
  1. package/README.md +267 -0
  2. package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
  3. package/dist/browser.d.mts +10 -4
  4. package/dist/browser.d.ts +10 -4
  5. package/dist/browser.js +47556 -19603
  6. package/dist/browser.mjs +628 -48
  7. package/dist/chunk-4GRJ5MAW.mjs +152 -0
  8. package/dist/chunk-5D7A3L3W.mjs +717 -0
  9. package/dist/chunk-64AYA5F5.mjs +7834 -0
  10. package/dist/chunk-GMDGB22A.mjs +379 -0
  11. package/dist/chunk-I534WKN7.mjs +328 -0
  12. package/dist/chunk-IBZVA5Y7.mjs +1003 -0
  13. package/dist/chunk-PRRZAWJE.mjs +223 -0
  14. package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
  15. package/dist/{chunk-3M3HNQCW.mjs → chunk-YWGJ77A2.mjs} +28656 -13103
  16. package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
  17. package/dist/constants-LHAAUC2T.mjs +51 -0
  18. package/dist/dist-2OGQ7FED.mjs +3957 -0
  19. package/dist/dist-IFHPYLDX.mjs +254 -0
  20. package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
  21. package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
  22. package/dist/{index-DIBZHOOQ.d.ts → index-DXh2IGkz.d.ts} +21239 -10304
  23. package/dist/{index-8MQz13eJ.d.mts → index-DeE1ZzA4.d.mts} +21239 -10304
  24. package/dist/index.d.mts +9 -3
  25. package/dist/index.d.ts +9 -3
  26. package/dist/index.js +48396 -19623
  27. package/dist/index.mjs +537 -19
  28. package/dist/interface-Bf7w1PLW.d.mts +679 -0
  29. package/dist/interface-Bf7w1PLW.d.ts +679 -0
  30. package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
  31. package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
  32. package/dist/proofs/halo2.d.mts +151 -0
  33. package/dist/proofs/halo2.d.ts +151 -0
  34. package/dist/proofs/halo2.js +350 -0
  35. package/dist/proofs/halo2.mjs +11 -0
  36. package/dist/proofs/kimchi.d.mts +160 -0
  37. package/dist/proofs/kimchi.d.ts +160 -0
  38. package/dist/proofs/kimchi.js +431 -0
  39. package/dist/proofs/kimchi.mjs +13 -0
  40. package/dist/proofs/noir.d.mts +1 -1
  41. package/dist/proofs/noir.d.ts +1 -1
  42. package/dist/proofs/noir.js +74 -18
  43. package/dist/proofs/noir.mjs +84 -24
  44. package/dist/solana-U3MEGU7W.mjs +280 -0
  45. package/dist/validity_proof-3POXLPNY.mjs +21 -0
  46. package/package.json +44 -11
  47. package/src/adapters/index.ts +41 -0
  48. package/src/adapters/jupiter.ts +571 -0
  49. package/src/adapters/near-intents.ts +135 -0
  50. package/src/advisor/advisor.ts +653 -0
  51. package/src/advisor/index.ts +54 -0
  52. package/src/advisor/tools.ts +303 -0
  53. package/src/advisor/types.ts +164 -0
  54. package/src/chains/ethereum/announcement.ts +536 -0
  55. package/src/chains/ethereum/bnb-optimizations.ts +474 -0
  56. package/src/chains/ethereum/commitment.ts +522 -0
  57. package/src/chains/ethereum/constants.ts +462 -0
  58. package/src/chains/ethereum/deployment.ts +596 -0
  59. package/src/chains/ethereum/gas-estimation.ts +538 -0
  60. package/src/chains/ethereum/index.ts +268 -0
  61. package/src/chains/ethereum/optimizations.ts +614 -0
  62. package/src/chains/ethereum/privacy-adapter.ts +855 -0
  63. package/src/chains/ethereum/registry.ts +584 -0
  64. package/src/chains/ethereum/rpc.ts +905 -0
  65. package/src/chains/ethereum/stealth.ts +491 -0
  66. package/src/chains/ethereum/token.ts +790 -0
  67. package/src/chains/ethereum/transfer.ts +637 -0
  68. package/src/chains/ethereum/types.ts +456 -0
  69. package/src/chains/ethereum/viewing-key.ts +455 -0
  70. package/src/chains/near/commitment.ts +608 -0
  71. package/src/chains/near/constants.ts +284 -0
  72. package/src/chains/near/function-call.ts +871 -0
  73. package/src/chains/near/history.ts +654 -0
  74. package/src/chains/near/implicit-account.ts +840 -0
  75. package/src/chains/near/index.ts +393 -0
  76. package/src/chains/near/native-transfer.ts +658 -0
  77. package/src/chains/near/nep141.ts +775 -0
  78. package/src/chains/near/privacy-adapter.ts +889 -0
  79. package/src/chains/near/resolver.ts +971 -0
  80. package/src/chains/near/rpc.ts +1016 -0
  81. package/src/chains/near/stealth.ts +419 -0
  82. package/src/chains/near/types.ts +317 -0
  83. package/src/chains/near/viewing-key.ts +876 -0
  84. package/src/chains/solana/anchor-transfer.ts +386 -0
  85. package/src/chains/solana/commitment.ts +577 -0
  86. package/src/chains/solana/constants.ts +126 -12
  87. package/src/chains/solana/ephemeral-keys.ts +543 -0
  88. package/src/chains/solana/index.ts +252 -1
  89. package/src/chains/solana/key-derivation.ts +418 -0
  90. package/src/chains/solana/kit-compat.ts +334 -0
  91. package/src/chains/solana/optimizations.ts +560 -0
  92. package/src/chains/solana/privacy-adapter.ts +605 -0
  93. package/src/chains/solana/providers/generic.ts +47 -6
  94. package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
  95. package/src/chains/solana/providers/helius-enhanced.ts +623 -0
  96. package/src/chains/solana/providers/helius.ts +186 -33
  97. package/src/chains/solana/providers/index.ts +31 -0
  98. package/src/chains/solana/providers/interface.ts +61 -18
  99. package/src/chains/solana/providers/quicknode.ts +409 -0
  100. package/src/chains/solana/providers/triton.ts +426 -0
  101. package/src/chains/solana/providers/webhook.ts +338 -67
  102. package/src/chains/solana/rpc-client.ts +1150 -0
  103. package/src/chains/solana/scan.ts +83 -66
  104. package/src/chains/solana/sol-transfer.ts +732 -0
  105. package/src/chains/solana/spl-transfer.ts +886 -0
  106. package/src/chains/solana/stealth-scanner.ts +703 -0
  107. package/src/chains/solana/sunspot-verifier.ts +453 -0
  108. package/src/chains/solana/transaction-builder.ts +755 -0
  109. package/src/chains/solana/transfer.ts +74 -5
  110. package/src/chains/solana/types.ts +57 -6
  111. package/src/chains/solana/utils.ts +110 -0
  112. package/src/chains/solana/viewing-key.ts +807 -0
  113. package/src/compliance/fireblocks.ts +921 -0
  114. package/src/compliance/index.ts +23 -0
  115. package/src/compliance/range-sas.ts +398 -33
  116. package/src/config/endpoints.ts +100 -0
  117. package/src/crypto.ts +11 -8
  118. package/src/errors.ts +82 -0
  119. package/src/evm/erc4337-relayer.ts +830 -0
  120. package/src/evm/index.ts +47 -0
  121. package/src/fees/calculator.ts +396 -0
  122. package/src/fees/index.ts +87 -0
  123. package/src/fees/near-contract.ts +429 -0
  124. package/src/fees/types.ts +268 -0
  125. package/src/index.ts +686 -1
  126. package/src/intent.ts +6 -3
  127. package/src/logger.ts +324 -0
  128. package/src/network/index.ts +80 -0
  129. package/src/network/proxy.ts +691 -0
  130. package/src/optimizations/index.ts +541 -0
  131. package/src/oracle/types.ts +1 -0
  132. package/src/privacy-backends/arcium-types.ts +727 -0
  133. package/src/privacy-backends/arcium.ts +719 -0
  134. package/src/privacy-backends/combined-privacy.ts +866 -0
  135. package/src/privacy-backends/cspl-token.ts +595 -0
  136. package/src/privacy-backends/cspl-types.ts +512 -0
  137. package/src/privacy-backends/cspl.ts +907 -0
  138. package/src/privacy-backends/health.ts +488 -0
  139. package/src/privacy-backends/inco-types.ts +323 -0
  140. package/src/privacy-backends/inco.ts +616 -0
  141. package/src/privacy-backends/index.ts +254 -4
  142. package/src/privacy-backends/interface.ts +649 -6
  143. package/src/privacy-backends/lru-cache.ts +343 -0
  144. package/src/privacy-backends/magicblock.ts +458 -0
  145. package/src/privacy-backends/mock.ts +258 -0
  146. package/src/privacy-backends/privacycash.ts +13 -17
  147. package/src/privacy-backends/private-swap.ts +570 -0
  148. package/src/privacy-backends/rate-limiter.ts +683 -0
  149. package/src/privacy-backends/registry.ts +414 -2
  150. package/src/privacy-backends/router.ts +283 -3
  151. package/src/privacy-backends/shadowwire.ts +449 -0
  152. package/src/privacy-backends/sip-native.ts +3 -0
  153. package/src/privacy-logger.ts +191 -0
  154. package/src/production-safety.ts +373 -0
  155. package/src/proofs/aggregator.ts +1029 -0
  156. package/src/proofs/browser-composer.ts +1150 -0
  157. package/src/proofs/browser.ts +113 -25
  158. package/src/proofs/cache/index.ts +127 -0
  159. package/src/proofs/cache/interface.ts +545 -0
  160. package/src/proofs/cache/key-generator.ts +188 -0
  161. package/src/proofs/cache/lru-cache.ts +481 -0
  162. package/src/proofs/cache/multi-tier-cache.ts +575 -0
  163. package/src/proofs/cache/persistent-cache.ts +788 -0
  164. package/src/proofs/compliance-proof.ts +872 -0
  165. package/src/proofs/composer/base.ts +923 -0
  166. package/src/proofs/composer/index.ts +25 -0
  167. package/src/proofs/composer/interface.ts +518 -0
  168. package/src/proofs/composer/types.ts +383 -0
  169. package/src/proofs/converters/halo2.ts +452 -0
  170. package/src/proofs/converters/index.ts +208 -0
  171. package/src/proofs/converters/interface.ts +363 -0
  172. package/src/proofs/converters/kimchi.ts +462 -0
  173. package/src/proofs/converters/noir.ts +451 -0
  174. package/src/proofs/fallback.ts +888 -0
  175. package/src/proofs/halo2.ts +42 -0
  176. package/src/proofs/index.ts +471 -0
  177. package/src/proofs/interface.ts +13 -0
  178. package/src/proofs/kimchi.ts +42 -0
  179. package/src/proofs/lazy.ts +1004 -0
  180. package/src/proofs/mock.ts +25 -1
  181. package/src/proofs/noir.ts +110 -29
  182. package/src/proofs/orchestrator.ts +960 -0
  183. package/src/proofs/parallel/concurrency.ts +297 -0
  184. package/src/proofs/parallel/dependency-graph.ts +602 -0
  185. package/src/proofs/parallel/executor.ts +420 -0
  186. package/src/proofs/parallel/index.ts +131 -0
  187. package/src/proofs/parallel/interface.ts +685 -0
  188. package/src/proofs/parallel/worker-pool.ts +644 -0
  189. package/src/proofs/providers/halo2.ts +560 -0
  190. package/src/proofs/providers/index.ts +34 -0
  191. package/src/proofs/providers/kimchi.ts +641 -0
  192. package/src/proofs/validator.ts +881 -0
  193. package/src/proofs/verifier.ts +867 -0
  194. package/src/quantum/index.ts +112 -0
  195. package/src/quantum/winternitz-vault.ts +639 -0
  196. package/src/quantum/wots.ts +611 -0
  197. package/src/settlement/backends/direct-chain.ts +1 -0
  198. package/src/settlement/index.ts +9 -0
  199. package/src/settlement/router.ts +732 -46
  200. package/src/solana/index.ts +72 -0
  201. package/src/solana/jito-relayer.ts +687 -0
  202. package/src/solana/noir-verifier-types.ts +430 -0
  203. package/src/solana/noir-verifier.ts +816 -0
  204. package/src/stealth/address-derivation.ts +193 -0
  205. package/src/stealth/ed25519.ts +431 -0
  206. package/src/stealth/index.ts +233 -0
  207. package/src/stealth/meta-address.ts +221 -0
  208. package/src/stealth/secp256k1.ts +368 -0
  209. package/src/stealth/utils.ts +194 -0
  210. package/src/stealth.ts +50 -1504
  211. package/src/sync/index.ts +106 -0
  212. package/src/sync/manager.ts +504 -0
  213. package/src/sync/mock-provider.ts +318 -0
  214. package/src/sync/oblivious.ts +625 -0
  215. package/src/tokens/index.ts +15 -0
  216. package/src/tokens/registry.ts +301 -0
  217. package/src/utils/deprecation.ts +94 -0
  218. package/src/utils/index.ts +9 -0
  219. package/src/wallet/ethereum/index.ts +68 -0
  220. package/src/wallet/ethereum/metamask-privacy.ts +420 -0
  221. package/src/wallet/ethereum/multi-wallet.ts +646 -0
  222. package/src/wallet/ethereum/privacy-adapter.ts +700 -0
  223. package/src/wallet/ethereum/types.ts +3 -1
  224. package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
  225. package/src/wallet/hardware/index.ts +10 -0
  226. package/src/wallet/hardware/ledger-privacy.ts +414 -0
  227. package/src/wallet/index.ts +71 -0
  228. package/src/wallet/near/adapter.ts +626 -0
  229. package/src/wallet/near/index.ts +86 -0
  230. package/src/wallet/near/meteor-wallet.ts +1153 -0
  231. package/src/wallet/near/my-near-wallet.ts +790 -0
  232. package/src/wallet/near/wallet-selector.ts +702 -0
  233. package/src/wallet/solana/adapter.ts +6 -4
  234. package/src/wallet/solana/index.ts +13 -0
  235. package/src/wallet/solana/privacy-adapter.ts +567 -0
  236. package/src/wallet/sui/types.ts +6 -4
  237. package/src/zcash/rpc-client.ts +13 -6
  238. package/dist/chunk-2XIVXWHA.mjs +0 -1930
  239. package/dist/chunk-3INS3PR5.mjs +0 -884
  240. package/dist/chunk-3OVABDRH.mjs +0 -17096
  241. package/dist/chunk-7RFRWDCW.mjs +0 -1504
  242. package/dist/chunk-DLDWZFYC.mjs +0 -1495
  243. package/dist/chunk-E6SZWREQ.mjs +0 -57
  244. package/dist/chunk-F6F73W35.mjs +0 -16166
  245. package/dist/chunk-G33LB27A.mjs +0 -16166
  246. package/dist/chunk-HGU6HZRC.mjs +0 -231
  247. package/dist/chunk-L2K34JCU.mjs +0 -1496
  248. package/dist/chunk-OFDBEIEK.mjs +0 -16166
  249. package/dist/chunk-SF7YSLF5.mjs +0 -1515
  250. package/dist/chunk-SN4ZDTVW.mjs +0 -16166
  251. package/dist/chunk-WWUSGOXE.mjs +0 -17129
  252. package/dist/constants-VOI7BSLK.mjs +0 -27
  253. package/dist/index-B71aXVzk.d.ts +0 -13264
  254. package/dist/index-BYZbDjal.d.ts +0 -11390
  255. package/dist/index-CHB3KuOB.d.mts +0 -11859
  256. package/dist/index-CzWPI6Le.d.ts +0 -11859
  257. package/dist/index-pOIIuwfV.d.mts +0 -13264
  258. package/dist/index-xbWjohNq.d.mts +0 -11390
  259. package/dist/solana-4O4K45VU.mjs +0 -46
  260. package/dist/solana-5EMCTPTS.mjs +0 -46
  261. package/dist/solana-NDABAZ6P.mjs +0 -56
  262. package/dist/solana-Q4NAVBTS.mjs +0 -46
  263. 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
- * Implements EIP-5564 style stealth addresses using secp256k1.
5
- * Provides unlinkable one-time addresses for privacy-preserving transactions.
6
- *
7
- * Flow:
8
- * 1. Recipient generates stealth meta-address (spending key P, viewing key Q)
9
- * 2. Sender generates ephemeral keypair (r, R = r*G)
10
- * 3. Sender computes shared secret: S = r * P
11
- * 4. Sender derives stealth address: A = Q + hash(S)*G
12
- * 5. Recipient scans: for each R, compute S = p * R, check if A matches
13
- */
14
-
15
- import { secp256k1 } from '@noble/curves/secp256k1'
16
- import { ed25519 } from '@noble/curves/ed25519'
17
- import { sha256 } from '@noble/hashes/sha256'
18
- import { sha512 } from '@noble/hashes/sha512'
19
- import { keccak_256 } from '@noble/hashes/sha3'
20
- import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils'
21
- import type {
22
- StealthMetaAddress,
23
- StealthAddress,
24
- StealthAddressRecovery,
25
- ChainId,
26
- HexString,
27
- } from '@sip-protocol/types'
28
- import { ValidationError } from './errors'
29
- import {
30
- isValidChainId,
31
- isValidHex,
32
- isValidCompressedPublicKey,
33
- isValidEd25519PublicKey,
34
- isValidPrivateKey,
35
- } from './validation'
36
- import { secureWipe, secureWipeAll } from './secure-memory'
37
-
38
- /**
39
- * Generate a new stealth meta-address keypair for receiving private payments
40
- *
41
- * Creates a reusable meta-address that senders can use to derive one-time stealth
42
- * addresses. The recipient publishes the meta-address publicly, and senders generate
43
- * unique payment addresses from it.
44
- *
45
- * **Security:** Private keys must be stored securely and never shared. The meta-address
46
- * (containing only public keys) can be safely published.
47
- *
48
- * **Algorithm:** Uses secp256k1 elliptic curve (EIP-5564 style) for:
49
- * - Ethereum, Polygon, Arbitrum, Optimism, Base, Bitcoin, Zcash
50
- *
51
- * For Solana/NEAR/Aptos/Sui chains, use {@link generateEd25519StealthMetaAddress} instead.
52
- *
53
- * @param chain - Target blockchain network (determines address format)
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'