@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
@@ -4,6 +4,11 @@
4
4
  * Token mints, RPC endpoints, and configuration for Solana same-chain privacy.
5
5
  */
6
6
 
7
+ import {
8
+ SOLANA_RPC_ENDPOINTS as SOLANA_RPC_CONFIG,
9
+ SOLANA_EXPLORER_ENDPOINTS as SOLANA_EXPLORER_CONFIG,
10
+ } from '../../config/endpoints'
11
+
7
12
  /**
8
13
  * Common SPL token mint addresses on Solana mainnet
9
14
  */
@@ -28,13 +33,14 @@ export const SOLANA_TOKEN_DECIMALS: Record<string, number> = {
28
33
 
29
34
  /**
30
35
  * RPC endpoints for Solana clusters
36
+ * Localnet is configurable via SOLANA_LOCALNET_RPC environment variable
31
37
  */
32
38
  export const SOLANA_RPC_ENDPOINTS = {
33
- 'mainnet-beta': 'https://api.mainnet-beta.solana.com',
34
- mainnet: 'https://api.mainnet-beta.solana.com',
35
- devnet: 'https://api.devnet.solana.com',
36
- testnet: 'https://api.testnet.solana.com',
37
- localnet: 'http://localhost:8899',
39
+ 'mainnet-beta': SOLANA_RPC_CONFIG.mainnet,
40
+ mainnet: SOLANA_RPC_CONFIG.mainnet,
41
+ devnet: SOLANA_RPC_CONFIG.devnet,
42
+ testnet: SOLANA_RPC_CONFIG.testnet,
43
+ localnet: SOLANA_RPC_CONFIG.localnet,
38
44
  } as const
39
45
 
40
46
  /**
@@ -44,13 +50,14 @@ export type SolanaCluster = keyof typeof SOLANA_RPC_ENDPOINTS
44
50
 
45
51
  /**
46
52
  * Explorer URLs for transaction viewing
53
+ * Localnet is configurable via SOLANA_LOCALNET_EXPLORER environment variable
47
54
  */
48
55
  export const SOLANA_EXPLORER_URLS = {
49
- 'mainnet-beta': 'https://solscan.io',
50
- mainnet: 'https://solscan.io',
51
- devnet: 'https://solscan.io?cluster=devnet',
52
- testnet: 'https://solscan.io?cluster=testnet',
53
- localnet: 'http://localhost:3000',
56
+ 'mainnet-beta': SOLANA_EXPLORER_CONFIG.mainnet,
57
+ mainnet: SOLANA_EXPLORER_CONFIG.mainnet,
58
+ devnet: `${SOLANA_EXPLORER_CONFIG.devnet}?cluster=devnet`,
59
+ testnet: `${SOLANA_EXPLORER_CONFIG.testnet}?cluster=testnet`,
60
+ localnet: SOLANA_EXPLORER_CONFIG.localnet,
54
61
  } as const
55
62
 
56
63
  /**
@@ -75,6 +82,43 @@ export const ESTIMATED_TX_FEE_LAMPORTS = 5000n
75
82
  */
76
83
  export const ATA_RENT_LAMPORTS = 2039280n
77
84
 
85
+ // ============================================================================
86
+ // Named constants for magic numbers
87
+ // ============================================================================
88
+
89
+ /** Solana address minimum length (base58 encoded 32-byte public keys) */
90
+ export const SOLANA_ADDRESS_MIN_LENGTH = 32
91
+
92
+ /** Solana address maximum length (base58 encoded 32-byte public keys) */
93
+ export const SOLANA_ADDRESS_MAX_LENGTH = 44
94
+
95
+ /** View tag minimum value */
96
+ export const VIEW_TAG_MIN = 0
97
+
98
+ /** View tag maximum value (1 byte) */
99
+ export const VIEW_TAG_MAX = 255
100
+
101
+ /** Ed25519 key size in bytes */
102
+ export const ED25519_KEY_BYTES = 32
103
+
104
+ /** Ed25519 key hex length including '0x' prefix */
105
+ export const ED25519_KEY_HEX_LENGTH = 66
106
+
107
+ /** Default scan limit for pagination */
108
+ export const DEFAULT_SCAN_LIMIT = 100
109
+
110
+ /** Helius DAS API page limit */
111
+ export const HELIUS_DAS_PAGE_LIMIT = 1000
112
+
113
+ /** Helius maximum pages for pagination */
114
+ export const HELIUS_MAX_PAGES = 100
115
+
116
+ /** Helius API key minimum length */
117
+ export const HELIUS_API_KEY_MIN_LENGTH = 8
118
+
119
+ /** Webhook batch processing limit */
120
+ export const WEBHOOK_MAX_BATCH_SIZE = 100
121
+
78
122
  /**
79
123
  * Get explorer URL for a transaction
80
124
  */
@@ -94,8 +138,78 @@ export function getTokenMint(symbol: string): string | undefined {
94
138
  }
95
139
 
96
140
  /**
97
- * Get token decimals from symbol
141
+ * Get token decimals from symbol (Solana-specific, legacy)
142
+ *
143
+ * @deprecated Use `getTokenDecimals(symbol, 'solana')` from the main SDK export
144
+ * which throws on unknown tokens instead of silently returning 9.
98
145
  */
99
- export function getTokenDecimals(symbol: string): number {
146
+ export function getSolanaTokenDecimals(symbol: string): number {
100
147
  return SOLANA_TOKEN_DECIMALS[symbol] ?? 9 // Default to 9 (SOL decimals)
101
148
  }
149
+
150
+ /**
151
+ * Sanitize a URL by masking potential credentials
152
+ *
153
+ * Removes or masks:
154
+ * - API keys in query parameters (api-key, apiKey, key, token, x-token)
155
+ * - Credentials in URL path (common for QuickNode, Triton)
156
+ * - Basic auth credentials (user:pass@host)
157
+ *
158
+ * @param url - URL string to sanitize
159
+ * @returns Sanitized URL safe for logging/error messages
160
+ *
161
+ * @example
162
+ * sanitizeUrl('https://api.helius.xyz?api-key=secret123')
163
+ * // => 'https://api.helius.xyz?api-key=***'
164
+ *
165
+ * sanitizeUrl('https://example.quiknode.pro/abc123def456')
166
+ * // => 'https://example.quiknode.pro/***'
167
+ */
168
+ export function sanitizeUrl(url: string): string {
169
+ try {
170
+ const parsed = new URL(url)
171
+
172
+ // Remove basic auth credentials
173
+ if (parsed.username || parsed.password) {
174
+ parsed.username = '***'
175
+ parsed.password = ''
176
+ }
177
+
178
+ // Mask sensitive query parameters (case-insensitive)
179
+ const sensitivePatterns = ['api-key', 'apikey', 'api_key', 'key', 'token', 'x-token', 'xtoken', 'secret', 'auth']
180
+ const keysToMask: string[] = []
181
+ for (const [key] of parsed.searchParams) {
182
+ const keyLower = key.toLowerCase()
183
+ if (sensitivePatterns.some((pattern) => keyLower === pattern || keyLower.includes(pattern))) {
184
+ keysToMask.push(key)
185
+ }
186
+ }
187
+ for (const key of keysToMask) {
188
+ parsed.searchParams.set(key, '***')
189
+ }
190
+
191
+ // Mask path segments that look like API keys/tokens
192
+ // QuickNode: /abc123def456 (32+ char alphanumeric)
193
+ // Triton: /x-token-value
194
+ const pathParts = parsed.pathname.split('/')
195
+ const maskedParts = pathParts.map((part) => {
196
+ // Skip empty parts and common path segments
197
+ if (!part || part.length < 16) return part
198
+ // If part looks like a token (long alphanumeric string), mask it
199
+ if (/^[a-zA-Z0-9_-]{16,}$/.test(part)) {
200
+ return '***'
201
+ }
202
+ return part
203
+ })
204
+ parsed.pathname = maskedParts.join('/')
205
+
206
+ return parsed.toString()
207
+ } catch {
208
+ // If URL parsing fails, do basic string sanitization
209
+ return url
210
+ .replace(/api-key=[^&]+/gi, 'api-key=***')
211
+ .replace(/apikey=[^&]+/gi, 'apikey=***')
212
+ .replace(/token=[^&]+/gi, 'token=***')
213
+ .replace(/\/[a-zA-Z0-9_-]{16,}(\/|$)/g, '/***$1')
214
+ }
215
+ }
@@ -0,0 +1,543 @@
1
+ /**
2
+ * Solana Ephemeral Keypair Management
3
+ *
4
+ * Provides secure ephemeral keypair generation and management for stealth
5
+ * payments. Each stealth transfer requires a fresh ephemeral keypair.
6
+ *
7
+ * Security considerations:
8
+ * - Ephemeral private keys are wiped from memory after use
9
+ * - Keys are never persisted to storage
10
+ * - Batch generation uses cryptographically secure randomness
11
+ *
12
+ * @module chains/solana/ephemeral-keys
13
+ */
14
+
15
+ import { ed25519 } from '@noble/curves/ed25519'
16
+ import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils'
17
+ import { sha256 } from '@noble/hashes/sha256'
18
+ import { secureWipe } from '../../secure-memory'
19
+ import { ed25519PublicKeyToSolanaAddress } from '../../stealth'
20
+ import type { HexString } from '@sip-protocol/types'
21
+
22
+ // ─── Types ────────────────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * An ephemeral keypair for stealth transfers
26
+ */
27
+ export interface EphemeralKeypair {
28
+ /**
29
+ * Ephemeral private key (hex)
30
+ * @security SENSITIVE - must be wiped after shared secret computation
31
+ */
32
+ privateKey: HexString
33
+
34
+ /**
35
+ * Ephemeral public key (hex, ed25519 format)
36
+ */
37
+ publicKey: HexString
38
+
39
+ /**
40
+ * Ephemeral public key in Solana base58 format
41
+ */
42
+ publicKeyBase58: string
43
+ }
44
+
45
+ /**
46
+ * Result of using an ephemeral keypair for stealth address generation
47
+ */
48
+ export interface EphemeralKeyUsageResult {
49
+ /**
50
+ * Shared secret derived from ECDH
51
+ */
52
+ sharedSecret: HexString
53
+
54
+ /**
55
+ * View tag (first byte of shared secret hash)
56
+ */
57
+ viewTag: number
58
+
59
+ /**
60
+ * Stealth address (hex, ed25519 format)
61
+ */
62
+ stealthAddress: HexString
63
+
64
+ /**
65
+ * Stealth address in Solana base58 format
66
+ */
67
+ stealthAddressBase58: string
68
+
69
+ /**
70
+ * Ephemeral public key used (for announcement)
71
+ */
72
+ ephemeralPublicKey: HexString
73
+
74
+ /**
75
+ * Ephemeral public key in Solana base58 format
76
+ */
77
+ ephemeralPublicKeyBase58: string
78
+ }
79
+
80
+ /**
81
+ * Managed ephemeral keypair with automatic secure disposal
82
+ */
83
+ export interface ManagedEphemeralKeypair extends EphemeralKeypair {
84
+ /**
85
+ * Whether this keypair has been used (and thus disposed)
86
+ */
87
+ isDisposed: boolean
88
+
89
+ /**
90
+ * Securely dispose of this keypair
91
+ * Called automatically after use, but can be called manually
92
+ */
93
+ dispose(): void
94
+
95
+ /**
96
+ * Use this keypair to generate a stealth address
97
+ * Automatically disposes the keypair after use
98
+ */
99
+ useForStealthAddress(
100
+ recipientSpendingKey: HexString,
101
+ recipientViewingKey: HexString
102
+ ): EphemeralKeyUsageResult
103
+ }
104
+
105
+ /**
106
+ * Options for batch ephemeral key generation
107
+ */
108
+ export interface BatchGenerationOptions {
109
+ /**
110
+ * Number of keypairs to generate
111
+ */
112
+ count: number
113
+
114
+ /**
115
+ * Whether to add entropy mixing between generations
116
+ * @default true
117
+ */
118
+ entropyMixing?: boolean
119
+ }
120
+
121
+ // ─── Core Generation ──────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Generate a single ephemeral keypair
125
+ *
126
+ * @returns Fresh ephemeral keypair
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const ephemeral = generateEphemeralKeypair()
131
+ * console.log('Public key:', ephemeral.publicKeyBase58)
132
+ * // Use for stealth transfer, then dispose
133
+ * ```
134
+ */
135
+ export function generateEphemeralKeypair(): EphemeralKeypair {
136
+ const privateKeyBytes = randomBytes(32)
137
+ const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes)
138
+
139
+ const privateKey = `0x${bytesToHex(privateKeyBytes)}` as HexString
140
+ const publicKey = `0x${bytesToHex(publicKeyBytes)}` as HexString
141
+ const publicKeyBase58 = ed25519PublicKeyToSolanaAddress(publicKey)
142
+
143
+ // Wipe the raw bytes (caller owns the hex string now)
144
+ secureWipe(privateKeyBytes)
145
+
146
+ return {
147
+ privateKey,
148
+ publicKey,
149
+ publicKeyBase58,
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Generate a managed ephemeral keypair with automatic disposal
155
+ *
156
+ * The managed keypair tracks its usage state and automatically wipes
157
+ * the private key from memory after use.
158
+ *
159
+ * @returns Managed ephemeral keypair
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const managed = generateManagedEphemeralKeypair()
164
+ *
165
+ * // Use for stealth address generation (auto-disposes)
166
+ * const result = managed.useForStealthAddress(
167
+ * recipientSpendingKey,
168
+ * recipientViewingKey
169
+ * )
170
+ *
171
+ * console.log('Stealth address:', result.stealthAddressBase58)
172
+ * console.log('Is disposed:', managed.isDisposed) // true
173
+ * ```
174
+ */
175
+ export function generateManagedEphemeralKeypair(): ManagedEphemeralKeypair {
176
+ // Store private key bytes for secure disposal
177
+ const privateKeyBytes = randomBytes(32)
178
+ const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes)
179
+
180
+ const privateKeyHex = `0x${bytesToHex(privateKeyBytes)}` as HexString
181
+ const publicKeyHex = `0x${bytesToHex(publicKeyBytes)}` as HexString
182
+ const publicKeyBase58 = ed25519PublicKeyToSolanaAddress(publicKeyHex)
183
+
184
+ let disposed = false
185
+
186
+ const managed: ManagedEphemeralKeypair = {
187
+ get privateKey(): HexString {
188
+ if (disposed) {
189
+ throw new Error('Ephemeral keypair has been disposed')
190
+ }
191
+ return privateKeyHex
192
+ },
193
+ publicKey: publicKeyHex,
194
+ publicKeyBase58,
195
+
196
+ get isDisposed(): boolean {
197
+ return disposed
198
+ },
199
+
200
+ dispose(): void {
201
+ if (!disposed) {
202
+ secureWipe(privateKeyBytes)
203
+ disposed = true
204
+ }
205
+ },
206
+
207
+ useForStealthAddress(
208
+ recipientSpendingKey: HexString,
209
+ recipientViewingKey: HexString
210
+ ): EphemeralKeyUsageResult {
211
+ if (disposed) {
212
+ throw new Error('Ephemeral keypair has been disposed')
213
+ }
214
+
215
+ try {
216
+ const result = computeStealthAddress(
217
+ privateKeyBytes,
218
+ publicKeyBytes,
219
+ recipientSpendingKey,
220
+ recipientViewingKey
221
+ )
222
+
223
+ return {
224
+ ...result,
225
+ ephemeralPublicKey: publicKeyHex,
226
+ ephemeralPublicKeyBase58: publicKeyBase58,
227
+ }
228
+ } finally {
229
+ // Always dispose after use
230
+ managed.dispose()
231
+ }
232
+ },
233
+ }
234
+
235
+ return managed
236
+ }
237
+
238
+ /**
239
+ * Generate multiple ephemeral keypairs in batch
240
+ *
241
+ * Useful for preparing keypairs for multiple transfers or for
242
+ * pre-generating keypairs for performance.
243
+ *
244
+ * @param options - Batch generation options
245
+ * @returns Array of ephemeral keypairs
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * // Generate 10 keypairs for upcoming transfers
250
+ * const keypairs = batchGenerateEphemeralKeypairs({ count: 10 })
251
+ *
252
+ * // Use each keypair for a transfer
253
+ * for (const keypair of keypairs) {
254
+ * await sendPrivateTransfer({ ephemeralKeypair: keypair, ... })
255
+ * }
256
+ * ```
257
+ */
258
+ export function batchGenerateEphemeralKeypairs(
259
+ options: BatchGenerationOptions
260
+ ): EphemeralKeypair[] {
261
+ const { count, entropyMixing = true } = options
262
+
263
+ if (count <= 0 || !Number.isInteger(count)) {
264
+ throw new Error('count must be a positive integer')
265
+ }
266
+
267
+ if (count > 1000) {
268
+ throw new Error('count cannot exceed 1000 (security limit)')
269
+ }
270
+
271
+ const keypairs: EphemeralKeypair[] = []
272
+
273
+ // For entropy mixing, we hash extra randomness into each generation
274
+ let entropyState: Uint8Array | null = entropyMixing ? randomBytes(32) : null
275
+
276
+ for (let i = 0; i < count; i++) {
277
+ if (entropyMixing && entropyState) {
278
+ // Mix additional entropy for each keypair
279
+ const extraEntropy = randomBytes(32)
280
+ entropyState = sha256(new Uint8Array([...entropyState, ...extraEntropy]))
281
+ }
282
+
283
+ keypairs.push(generateEphemeralKeypair())
284
+ }
285
+
286
+ // Wipe entropy state
287
+ if (entropyState) {
288
+ secureWipe(entropyState)
289
+ }
290
+
291
+ return keypairs
292
+ }
293
+
294
+ /**
295
+ * Generate multiple managed ephemeral keypairs in batch
296
+ *
297
+ * @param options - Batch generation options
298
+ * @returns Array of managed ephemeral keypairs
299
+ */
300
+ export function batchGenerateManagedEphemeralKeypairs(
301
+ options: BatchGenerationOptions
302
+ ): ManagedEphemeralKeypair[] {
303
+ const { count, entropyMixing = true } = options
304
+
305
+ if (count <= 0 || !Number.isInteger(count)) {
306
+ throw new Error('count must be a positive integer')
307
+ }
308
+
309
+ if (count > 1000) {
310
+ throw new Error('count cannot exceed 1000 (security limit)')
311
+ }
312
+
313
+ const keypairs: ManagedEphemeralKeypair[] = []
314
+
315
+ // For entropy mixing, we hash extra randomness into each generation
316
+ let entropyState: Uint8Array | null = entropyMixing ? randomBytes(32) : null
317
+
318
+ for (let i = 0; i < count; i++) {
319
+ if (entropyMixing && entropyState) {
320
+ // Mix additional entropy for each keypair
321
+ const extraEntropy = randomBytes(32)
322
+ entropyState = sha256(new Uint8Array([...entropyState, ...extraEntropy]))
323
+ }
324
+
325
+ keypairs.push(generateManagedEphemeralKeypair())
326
+ }
327
+
328
+ // Wipe entropy state
329
+ if (entropyState) {
330
+ secureWipe(entropyState)
331
+ }
332
+
333
+ return keypairs
334
+ }
335
+
336
+ // ─── Disposal Utilities ───────────────────────────────────────────────────────
337
+
338
+ /**
339
+ * Dispose multiple ephemeral keypairs at once
340
+ *
341
+ * @param keypairs - Array of managed keypairs to dispose
342
+ */
343
+ export function disposeEphemeralKeypairs(
344
+ keypairs: ManagedEphemeralKeypair[]
345
+ ): void {
346
+ for (const keypair of keypairs) {
347
+ keypair.dispose()
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Securely wipe an ephemeral private key string from memory
353
+ *
354
+ * Note: Due to JavaScript string immutability, this creates a new
355
+ * Uint8Array from the hex string and wipes it. For better security,
356
+ * use ManagedEphemeralKeypair which maintains byte access.
357
+ *
358
+ * @param privateKeyHex - Private key hex string to wipe
359
+ */
360
+ export function wipeEphemeralPrivateKey(privateKeyHex: HexString): void {
361
+ // Convert hex to bytes and wipe the bytes
362
+ const bytes = hexToBytes(privateKeyHex.slice(2))
363
+ secureWipe(bytes)
364
+ }
365
+
366
+ // ─── Internal Helpers ─────────────────────────────────────────────────────────
367
+
368
+ /**
369
+ * ed25519 group order (L)
370
+ */
371
+ const ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n
372
+
373
+ /**
374
+ * Convert bytes to bigint (little-endian for ed25519)
375
+ */
376
+ function bytesToBigIntLE(bytes: Uint8Array): bigint {
377
+ let result = 0n
378
+ for (let i = bytes.length - 1; i >= 0; i--) {
379
+ result = (result << 8n) | BigInt(bytes[i])
380
+ }
381
+ return result
382
+ }
383
+
384
+ /**
385
+ * Get ed25519 scalar from private key bytes
386
+ * Follows standard ed25519 scalar clamping
387
+ */
388
+ function getEd25519Scalar(privateKey: Uint8Array): bigint {
389
+ const hash = sha256(privateKey)
390
+ // Clamp to valid scalar
391
+ hash[0] &= 248
392
+ hash[31] &= 127
393
+ hash[31] |= 64
394
+ return bytesToBigIntLE(hash.slice(0, 32))
395
+ }
396
+
397
+ /**
398
+ * Compute stealth address from ephemeral keypair and recipient keys
399
+ */
400
+ function computeStealthAddress(
401
+ ephemeralPrivateBytes: Uint8Array,
402
+ _ephemeralPublicBytes: Uint8Array, // Reserved for future validation
403
+ recipientSpendingKey: HexString,
404
+ recipientViewingKey: HexString
405
+ ): Omit<EphemeralKeyUsageResult, 'ephemeralPublicKey' | 'ephemeralPublicKeyBase58'> {
406
+ // Parse recipient keys
407
+ const spendingKeyBytes = hexToBytes(recipientSpendingKey.slice(2))
408
+ const viewingKeyBytes = hexToBytes(recipientViewingKey.slice(2))
409
+
410
+ // Get ephemeral scalar
411
+ const rawEphemeralScalar = getEd25519Scalar(ephemeralPrivateBytes)
412
+ const ephemeralScalar = rawEphemeralScalar % ED25519_ORDER
413
+ if (ephemeralScalar === 0n) {
414
+ throw new Error('Invalid ephemeral scalar')
415
+ }
416
+
417
+ // Compute shared secret: S = ephemeral_scalar * P_spend
418
+ const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes)
419
+ const sharedSecretPoint = spendingPoint.multiply(ephemeralScalar)
420
+ const sharedSecretBytes = sharedSecretPoint.toRawBytes()
421
+
422
+ // Hash the shared secret
423
+ const sharedSecretHash = sha256(sharedSecretBytes)
424
+ const viewTag = sharedSecretHash[0]
425
+
426
+ // Derive stealth address: P_stealth = P_view + hash(S)*G
427
+ const hashScalar = bytesToBigIntLE(sharedSecretHash) % ED25519_ORDER
428
+ if (hashScalar === 0n) {
429
+ throw new Error('Invalid hash scalar')
430
+ }
431
+
432
+ const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar)
433
+ const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes)
434
+ const stealthPoint = viewingPoint.add(hashTimesG)
435
+ const stealthAddressBytes = stealthPoint.toRawBytes()
436
+
437
+ const stealthAddress = `0x${bytesToHex(stealthAddressBytes)}` as HexString
438
+ const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress)
439
+
440
+ return {
441
+ sharedSecret: `0x${bytesToHex(sharedSecretHash)}` as HexString,
442
+ viewTag,
443
+ stealthAddress,
444
+ stealthAddressBase58,
445
+ }
446
+ }
447
+
448
+ // ─── Announcement Format ──────────────────────────────────────────────────────
449
+
450
+ /**
451
+ * Format ephemeral key data for Solana memo announcement
452
+ *
453
+ * @param ephemeralPublicKeyBase58 - Ephemeral public key in base58
454
+ * @param viewTag - View tag (0-255)
455
+ * @param stealthAddressBase58 - Optional stealth address for verification
456
+ * @returns Formatted announcement string
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * const memo = formatEphemeralAnnouncement(
461
+ * result.ephemeralPublicKeyBase58,
462
+ * result.viewTag,
463
+ * result.stealthAddressBase58
464
+ * )
465
+ * // "SIP:1:7xK9...:0a:8yL0..."
466
+ * ```
467
+ */
468
+ export function formatEphemeralAnnouncement(
469
+ ephemeralPublicKeyBase58: string,
470
+ viewTag: number,
471
+ stealthAddressBase58?: string
472
+ ): string {
473
+ const viewTagHex = viewTag.toString(16).padStart(2, '0')
474
+ const parts = ['SIP:1', ephemeralPublicKeyBase58, viewTagHex]
475
+
476
+ if (stealthAddressBase58) {
477
+ parts.push(stealthAddressBase58)
478
+ }
479
+
480
+ return parts.join(':')
481
+ }
482
+
483
+ /**
484
+ * Parse ephemeral key data from Solana memo announcement
485
+ *
486
+ * @param announcement - Announcement string from memo
487
+ * @returns Parsed ephemeral data or null if invalid
488
+ *
489
+ * @example
490
+ * ```typescript
491
+ * const parsed = parseEphemeralAnnouncement('SIP:1:7xK9...:0a:8yL0...')
492
+ * if (parsed) {
493
+ * console.log('Ephemeral key:', parsed.ephemeralPublicKeyBase58)
494
+ * console.log('View tag:', parsed.viewTag)
495
+ * }
496
+ * ```
497
+ */
498
+ export function parseEphemeralAnnouncement(
499
+ announcement: string
500
+ ): {
501
+ ephemeralPublicKeyBase58: string
502
+ viewTag: number
503
+ stealthAddressBase58?: string
504
+ } | null {
505
+ if (!announcement.startsWith('SIP:1:')) {
506
+ return null
507
+ }
508
+
509
+ const parts = announcement.slice(6).split(':')
510
+ if (parts.length < 2) {
511
+ return null
512
+ }
513
+
514
+ const ephemeralPublicKeyBase58 = parts[0]
515
+ const viewTagHex = parts[1]
516
+ const stealthAddressBase58 = parts[2]
517
+
518
+ // Validate ephemeral key (base58, 32-44 chars)
519
+ if (!ephemeralPublicKeyBase58 || ephemeralPublicKeyBase58.length < 32 || ephemeralPublicKeyBase58.length > 44) {
520
+ return null
521
+ }
522
+
523
+ // Validate view tag (1-2 hex chars)
524
+ if (!viewTagHex || viewTagHex.length > 2 || !/^[0-9a-fA-F]+$/.test(viewTagHex)) {
525
+ return null
526
+ }
527
+
528
+ const viewTag = parseInt(viewTagHex, 16)
529
+ if (viewTag < 0 || viewTag > 255) {
530
+ return null
531
+ }
532
+
533
+ // Validate stealth address if present
534
+ if (stealthAddressBase58 && (stealthAddressBase58.length < 32 || stealthAddressBase58.length > 44)) {
535
+ return null
536
+ }
537
+
538
+ return {
539
+ ephemeralPublicKeyBase58,
540
+ viewTag,
541
+ stealthAddressBase58,
542
+ }
543
+ }