@sip-protocol/sdk 0.7.2 → 0.7.4

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 (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
  4. package/dist/browser.d.mts +10 -4
  5. package/dist/browser.d.ts +10 -4
  6. package/dist/browser.js +48874 -18336
  7. package/dist/browser.mjs +674 -48
  8. package/dist/chunk-4GRJ5MAW.mjs +152 -0
  9. package/dist/chunk-5D7A3L3W.mjs +717 -0
  10. package/dist/chunk-64AYA5F5.mjs +7834 -0
  11. package/dist/chunk-GMDGB22A.mjs +379 -0
  12. package/dist/chunk-I534WKN7.mjs +328 -0
  13. package/dist/chunk-IBZVA5Y7.mjs +1003 -0
  14. package/dist/chunk-PRRZAWJE.mjs +223 -0
  15. package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
  16. package/dist/chunk-YWGJ77A2.mjs +33806 -0
  17. package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
  18. package/dist/constants-LHAAUC2T.mjs +51 -0
  19. package/dist/dist-2OGQ7FED.mjs +3957 -0
  20. package/dist/dist-IFHPYLDX.mjs +254 -0
  21. package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
  22. package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
  23. package/dist/index-DXh2IGkz.d.ts +24681 -0
  24. package/dist/index-DeE1ZzA4.d.mts +24681 -0
  25. package/dist/index.d.mts +9 -3
  26. package/dist/index.d.ts +9 -3
  27. package/dist/index.js +48676 -17318
  28. package/dist/index.mjs +583 -19
  29. package/dist/interface-Bf7w1PLW.d.mts +679 -0
  30. package/dist/interface-Bf7w1PLW.d.ts +679 -0
  31. package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
  32. package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
  33. package/dist/proofs/halo2.d.mts +151 -0
  34. package/dist/proofs/halo2.d.ts +151 -0
  35. package/dist/proofs/halo2.js +350 -0
  36. package/dist/proofs/halo2.mjs +11 -0
  37. package/dist/proofs/kimchi.d.mts +160 -0
  38. package/dist/proofs/kimchi.d.ts +160 -0
  39. package/dist/proofs/kimchi.js +431 -0
  40. package/dist/proofs/kimchi.mjs +13 -0
  41. package/dist/proofs/noir.d.mts +1 -1
  42. package/dist/proofs/noir.d.ts +1 -1
  43. package/dist/proofs/noir.js +74 -18
  44. package/dist/proofs/noir.mjs +84 -24
  45. package/dist/solana-U3MEGU7W.mjs +280 -0
  46. package/dist/validity_proof-3POXLPNY.mjs +21 -0
  47. package/package.json +54 -21
  48. package/src/adapters/index.ts +41 -0
  49. package/src/adapters/jupiter.ts +571 -0
  50. package/src/adapters/near-intents.ts +135 -0
  51. package/src/advisor/advisor.ts +653 -0
  52. package/src/advisor/index.ts +54 -0
  53. package/src/advisor/tools.ts +303 -0
  54. package/src/advisor/types.ts +164 -0
  55. package/src/chains/ethereum/announcement.ts +536 -0
  56. package/src/chains/ethereum/bnb-optimizations.ts +474 -0
  57. package/src/chains/ethereum/commitment.ts +522 -0
  58. package/src/chains/ethereum/constants.ts +462 -0
  59. package/src/chains/ethereum/deployment.ts +596 -0
  60. package/src/chains/ethereum/gas-estimation.ts +538 -0
  61. package/src/chains/ethereum/index.ts +268 -0
  62. package/src/chains/ethereum/optimizations.ts +614 -0
  63. package/src/chains/ethereum/privacy-adapter.ts +855 -0
  64. package/src/chains/ethereum/registry.ts +584 -0
  65. package/src/chains/ethereum/rpc.ts +905 -0
  66. package/src/chains/ethereum/stealth.ts +491 -0
  67. package/src/chains/ethereum/token.ts +790 -0
  68. package/src/chains/ethereum/transfer.ts +637 -0
  69. package/src/chains/ethereum/types.ts +456 -0
  70. package/src/chains/ethereum/viewing-key.ts +455 -0
  71. package/src/chains/near/commitment.ts +608 -0
  72. package/src/chains/near/constants.ts +284 -0
  73. package/src/chains/near/function-call.ts +871 -0
  74. package/src/chains/near/history.ts +654 -0
  75. package/src/chains/near/implicit-account.ts +840 -0
  76. package/src/chains/near/index.ts +393 -0
  77. package/src/chains/near/native-transfer.ts +658 -0
  78. package/src/chains/near/nep141.ts +775 -0
  79. package/src/chains/near/privacy-adapter.ts +889 -0
  80. package/src/chains/near/resolver.ts +971 -0
  81. package/src/chains/near/rpc.ts +1016 -0
  82. package/src/chains/near/stealth.ts +419 -0
  83. package/src/chains/near/types.ts +317 -0
  84. package/src/chains/near/viewing-key.ts +876 -0
  85. package/src/chains/solana/anchor-transfer.ts +386 -0
  86. package/src/chains/solana/commitment.ts +577 -0
  87. package/src/chains/solana/constants.ts +126 -12
  88. package/src/chains/solana/ephemeral-keys.ts +543 -0
  89. package/src/chains/solana/index.ts +276 -1
  90. package/src/chains/solana/key-derivation.ts +418 -0
  91. package/src/chains/solana/kit-compat.ts +334 -0
  92. package/src/chains/solana/optimizations.ts +560 -0
  93. package/src/chains/solana/privacy-adapter.ts +605 -0
  94. package/src/chains/solana/providers/generic.ts +201 -0
  95. package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
  96. package/src/chains/solana/providers/helius-enhanced.ts +623 -0
  97. package/src/chains/solana/providers/helius.ts +402 -0
  98. package/src/chains/solana/providers/index.ts +85 -0
  99. package/src/chains/solana/providers/interface.ts +221 -0
  100. package/src/chains/solana/providers/quicknode.ts +409 -0
  101. package/src/chains/solana/providers/triton.ts +426 -0
  102. package/src/chains/solana/providers/webhook.ts +790 -0
  103. package/src/chains/solana/rpc-client.ts +1150 -0
  104. package/src/chains/solana/scan.ts +170 -73
  105. package/src/chains/solana/sol-transfer.ts +732 -0
  106. package/src/chains/solana/spl-transfer.ts +886 -0
  107. package/src/chains/solana/stealth-scanner.ts +703 -0
  108. package/src/chains/solana/sunspot-verifier.ts +453 -0
  109. package/src/chains/solana/transaction-builder.ts +755 -0
  110. package/src/chains/solana/transfer.ts +74 -5
  111. package/src/chains/solana/types.ts +77 -7
  112. package/src/chains/solana/utils.ts +110 -0
  113. package/src/chains/solana/viewing-key.ts +807 -0
  114. package/src/compliance/fireblocks.ts +921 -0
  115. package/src/compliance/index.ts +37 -0
  116. package/src/compliance/range-sas.ts +956 -0
  117. package/src/config/endpoints.ts +100 -0
  118. package/src/crypto.ts +11 -8
  119. package/src/errors.ts +82 -0
  120. package/src/evm/erc4337-relayer.ts +830 -0
  121. package/src/evm/index.ts +47 -0
  122. package/src/fees/calculator.ts +396 -0
  123. package/src/fees/index.ts +87 -0
  124. package/src/fees/near-contract.ts +429 -0
  125. package/src/fees/types.ts +268 -0
  126. package/src/index.ts +785 -1
  127. package/src/intent.ts +6 -3
  128. package/src/logger.ts +324 -0
  129. package/src/network/index.ts +80 -0
  130. package/src/network/proxy.ts +691 -0
  131. package/src/optimizations/index.ts +541 -0
  132. package/src/oracle/types.ts +1 -0
  133. package/src/privacy-backends/arcium-types.ts +727 -0
  134. package/src/privacy-backends/arcium.ts +719 -0
  135. package/src/privacy-backends/combined-privacy.ts +866 -0
  136. package/src/privacy-backends/cspl-token.ts +595 -0
  137. package/src/privacy-backends/cspl-types.ts +512 -0
  138. package/src/privacy-backends/cspl.ts +907 -0
  139. package/src/privacy-backends/health.ts +488 -0
  140. package/src/privacy-backends/inco-types.ts +323 -0
  141. package/src/privacy-backends/inco.ts +616 -0
  142. package/src/privacy-backends/index.ts +336 -0
  143. package/src/privacy-backends/interface.ts +906 -0
  144. package/src/privacy-backends/lru-cache.ts +343 -0
  145. package/src/privacy-backends/magicblock.ts +458 -0
  146. package/src/privacy-backends/mock.ts +258 -0
  147. package/src/privacy-backends/privacycash-types.ts +278 -0
  148. package/src/privacy-backends/privacycash.ts +456 -0
  149. package/src/privacy-backends/private-swap.ts +570 -0
  150. package/src/privacy-backends/rate-limiter.ts +683 -0
  151. package/src/privacy-backends/registry.ts +690 -0
  152. package/src/privacy-backends/router.ts +626 -0
  153. package/src/privacy-backends/shadowwire.ts +449 -0
  154. package/src/privacy-backends/sip-native.ts +256 -0
  155. package/src/privacy-logger.ts +191 -0
  156. package/src/production-safety.ts +373 -0
  157. package/src/proofs/aggregator.ts +1029 -0
  158. package/src/proofs/browser-composer.ts +1150 -0
  159. package/src/proofs/browser.ts +113 -25
  160. package/src/proofs/cache/index.ts +127 -0
  161. package/src/proofs/cache/interface.ts +545 -0
  162. package/src/proofs/cache/key-generator.ts +188 -0
  163. package/src/proofs/cache/lru-cache.ts +481 -0
  164. package/src/proofs/cache/multi-tier-cache.ts +575 -0
  165. package/src/proofs/cache/persistent-cache.ts +788 -0
  166. package/src/proofs/compliance-proof.ts +872 -0
  167. package/src/proofs/composer/base.ts +923 -0
  168. package/src/proofs/composer/index.ts +25 -0
  169. package/src/proofs/composer/interface.ts +518 -0
  170. package/src/proofs/composer/types.ts +383 -0
  171. package/src/proofs/converters/halo2.ts +452 -0
  172. package/src/proofs/converters/index.ts +208 -0
  173. package/src/proofs/converters/interface.ts +363 -0
  174. package/src/proofs/converters/kimchi.ts +462 -0
  175. package/src/proofs/converters/noir.ts +451 -0
  176. package/src/proofs/fallback.ts +888 -0
  177. package/src/proofs/halo2.ts +42 -0
  178. package/src/proofs/index.ts +471 -0
  179. package/src/proofs/interface.ts +13 -0
  180. package/src/proofs/kimchi.ts +42 -0
  181. package/src/proofs/lazy.ts +1004 -0
  182. package/src/proofs/mock.ts +25 -1
  183. package/src/proofs/noir.ts +111 -30
  184. package/src/proofs/orchestrator.ts +960 -0
  185. package/src/proofs/parallel/concurrency.ts +297 -0
  186. package/src/proofs/parallel/dependency-graph.ts +602 -0
  187. package/src/proofs/parallel/executor.ts +420 -0
  188. package/src/proofs/parallel/index.ts +131 -0
  189. package/src/proofs/parallel/interface.ts +685 -0
  190. package/src/proofs/parallel/worker-pool.ts +644 -0
  191. package/src/proofs/providers/halo2.ts +560 -0
  192. package/src/proofs/providers/index.ts +34 -0
  193. package/src/proofs/providers/kimchi.ts +641 -0
  194. package/src/proofs/validator.ts +881 -0
  195. package/src/proofs/verifier.ts +867 -0
  196. package/src/quantum/index.ts +112 -0
  197. package/src/quantum/winternitz-vault.ts +639 -0
  198. package/src/quantum/wots.ts +611 -0
  199. package/src/settlement/backends/direct-chain.ts +1 -0
  200. package/src/settlement/index.ts +9 -0
  201. package/src/settlement/router.ts +732 -46
  202. package/src/solana/index.ts +72 -0
  203. package/src/solana/jito-relayer.ts +687 -0
  204. package/src/solana/noir-verifier-types.ts +430 -0
  205. package/src/solana/noir-verifier.ts +816 -0
  206. package/src/stealth/address-derivation.ts +193 -0
  207. package/src/stealth/ed25519.ts +431 -0
  208. package/src/stealth/index.ts +233 -0
  209. package/src/stealth/meta-address.ts +221 -0
  210. package/src/stealth/secp256k1.ts +368 -0
  211. package/src/stealth/utils.ts +194 -0
  212. package/src/stealth.ts +50 -1504
  213. package/src/surveillance/algorithms/address-reuse.ts +143 -0
  214. package/src/surveillance/algorithms/cluster.ts +247 -0
  215. package/src/surveillance/algorithms/exchange.ts +295 -0
  216. package/src/surveillance/algorithms/temporal.ts +337 -0
  217. package/src/surveillance/analyzer.ts +442 -0
  218. package/src/surveillance/index.ts +64 -0
  219. package/src/surveillance/scoring.ts +372 -0
  220. package/src/surveillance/types.ts +264 -0
  221. package/src/sync/index.ts +106 -0
  222. package/src/sync/manager.ts +504 -0
  223. package/src/sync/mock-provider.ts +318 -0
  224. package/src/sync/oblivious.ts +625 -0
  225. package/src/tokens/index.ts +15 -0
  226. package/src/tokens/registry.ts +301 -0
  227. package/src/utils/deprecation.ts +94 -0
  228. package/src/utils/index.ts +9 -0
  229. package/src/wallet/ethereum/index.ts +68 -0
  230. package/src/wallet/ethereum/metamask-privacy.ts +420 -0
  231. package/src/wallet/ethereum/multi-wallet.ts +646 -0
  232. package/src/wallet/ethereum/privacy-adapter.ts +700 -0
  233. package/src/wallet/ethereum/types.ts +3 -1
  234. package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
  235. package/src/wallet/hardware/index.ts +10 -0
  236. package/src/wallet/hardware/ledger-privacy.ts +414 -0
  237. package/src/wallet/index.ts +71 -0
  238. package/src/wallet/near/adapter.ts +626 -0
  239. package/src/wallet/near/index.ts +86 -0
  240. package/src/wallet/near/meteor-wallet.ts +1153 -0
  241. package/src/wallet/near/my-near-wallet.ts +790 -0
  242. package/src/wallet/near/wallet-selector.ts +702 -0
  243. package/src/wallet/solana/adapter.ts +6 -4
  244. package/src/wallet/solana/index.ts +13 -0
  245. package/src/wallet/solana/privacy-adapter.ts +567 -0
  246. package/src/wallet/sui/types.ts +6 -4
  247. package/src/zcash/rpc-client.ts +13 -6
  248. package/dist/chunk-3INS3PR5.mjs +0 -884
  249. package/dist/chunk-3OVABDRH.mjs +0 -17096
  250. package/dist/chunk-DLDWZFYC.mjs +0 -1495
  251. package/dist/chunk-E6SZWREQ.mjs +0 -57
  252. package/dist/chunk-G33LB27A.mjs +0 -16166
  253. package/dist/chunk-HGU6HZRC.mjs +0 -231
  254. package/dist/chunk-L2K34JCU.mjs +0 -1496
  255. package/dist/chunk-SN4ZDTVW.mjs +0 -16166
  256. package/dist/constants-VOI7BSLK.mjs +0 -27
  257. package/dist/index-BYZbDjal.d.ts +0 -11390
  258. package/dist/index-CHB3KuOB.d.mts +0 -11859
  259. package/dist/index-CzWPI6Le.d.ts +0 -11859
  260. package/dist/index-xbWjohNq.d.mts +0 -11390
  261. package/dist/solana-5EMCTPTS.mjs +0 -46
  262. package/dist/solana-Q4NAVBTS.mjs +0 -46
@@ -0,0 +1,807 @@
1
+ /**
2
+ * Solana Viewing Key Management
3
+ *
4
+ * Provides viewing key generation, export/import, encryption, and storage
5
+ * for selective disclosure and compliance on Solana.
6
+ *
7
+ * ## Architecture
8
+ *
9
+ * ```
10
+ * Spending Private Key
11
+ * │
12
+ * ▼ HMAC-SHA256(context)
13
+ * Viewing Private Key (32 bytes)
14
+ * │
15
+ * ▼ ed25519.getPublicKey()
16
+ * Viewing Public Key (32 bytes)
17
+ * │
18
+ * ▼ sha256()
19
+ * Viewing Key Hash (32 bytes) ← Used for announcement matching
20
+ * ```
21
+ *
22
+ * ## Security Properties
23
+ *
24
+ * - Viewing keys can decrypt but NOT spend funds
25
+ * - Hash is safe to publish on-chain for announcement matching
26
+ * - XChaCha20-Poly1305 provides authenticated encryption
27
+ *
28
+ * @module chains/solana/viewing-key
29
+ */
30
+
31
+ import { ed25519 } from '@noble/curves/ed25519'
32
+ import { sha256 } from '@noble/hashes/sha256'
33
+ import { hmac } from '@noble/hashes/hmac'
34
+ import { hkdf } from '@noble/hashes/hkdf'
35
+ import { bytesToHex, hexToBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'
36
+ import { xchacha20poly1305 } from '@noble/ciphers/chacha.js'
37
+ import type { HexString, Hash } from '@sip-protocol/types'
38
+ import { ValidationError, CryptoError, ErrorCode } from '../../errors'
39
+ import { isValidHex } from '../../validation'
40
+ import { secureWipe } from '../../secure-memory'
41
+
42
+ // ─── Constants ────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * Domain separation for viewing key derivation from spending key
46
+ */
47
+ const VIEWING_KEY_CONTEXT = 'SIP-viewing-key-v1'
48
+
49
+ /**
50
+ * Domain separation for encryption key derivation
51
+ */
52
+ const ENCRYPTION_DOMAIN = 'SIP-SOLANA-VIEWING-KEY-ENCRYPTION-V1'
53
+
54
+ /**
55
+ * XChaCha20-Poly1305 nonce size (24 bytes)
56
+ */
57
+ const NONCE_SIZE = 24
58
+
59
+ /**
60
+ * Standard export format version for viewing keys
61
+ */
62
+ const EXPORT_VERSION = 1
63
+
64
+ // ─── Types ────────────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * A Solana viewing key with associated metadata
68
+ */
69
+ export interface SolanaViewingKey {
70
+ /**
71
+ * The viewing private key (32 bytes)
72
+ * Used for decryption and stealth address scanning
73
+ */
74
+ privateKey: HexString
75
+
76
+ /**
77
+ * The viewing public key (32 bytes)
78
+ * Can be shared for encryption
79
+ */
80
+ publicKey: HexString
81
+
82
+ /**
83
+ * Hash of the viewing public key for announcement matching
84
+ * This is published on-chain to enable efficient scanning
85
+ */
86
+ hash: Hash
87
+
88
+ /**
89
+ * Optional label for this viewing key
90
+ */
91
+ label?: string
92
+
93
+ /**
94
+ * Timestamp when this key was created
95
+ */
96
+ createdAt: number
97
+ }
98
+
99
+ /**
100
+ * Standard export format for viewing keys
101
+ */
102
+ export interface ViewingKeyExport {
103
+ /**
104
+ * Export format version
105
+ */
106
+ version: number
107
+
108
+ /**
109
+ * The chain this key is for
110
+ */
111
+ chain: 'solana'
112
+
113
+ /**
114
+ * The viewing private key (hex encoded)
115
+ */
116
+ privateKey: HexString
117
+
118
+ /**
119
+ * The viewing public key (hex encoded)
120
+ */
121
+ publicKey: HexString
122
+
123
+ /**
124
+ * Hash for announcement matching
125
+ */
126
+ hash: Hash
127
+
128
+ /**
129
+ * Optional label
130
+ */
131
+ label?: string
132
+
133
+ /**
134
+ * Creation timestamp
135
+ */
136
+ createdAt: number
137
+
138
+ /**
139
+ * Export timestamp
140
+ */
141
+ exportedAt: number
142
+ }
143
+
144
+ /**
145
+ * Encrypted data structure for viewing key operations
146
+ */
147
+ export interface EncryptedPayload {
148
+ /**
149
+ * The encrypted ciphertext (hex encoded)
150
+ */
151
+ ciphertext: HexString
152
+
153
+ /**
154
+ * The nonce used for encryption (hex encoded, 24 bytes)
155
+ */
156
+ nonce: HexString
157
+
158
+ /**
159
+ * Hash of the viewing key that can decrypt this
160
+ */
161
+ viewingKeyHash: Hash
162
+ }
163
+
164
+ /**
165
+ * Transaction data that can be encrypted for viewing
166
+ */
167
+ export interface SolanaTransactionData {
168
+ /**
169
+ * Sender's public key or address
170
+ */
171
+ sender: string
172
+
173
+ /**
174
+ * Recipient's stealth address
175
+ */
176
+ recipient: string
177
+
178
+ /**
179
+ * Amount in smallest units (string for bigint serialization)
180
+ */
181
+ amount: string
182
+
183
+ /**
184
+ * Token mint address (null for native SOL)
185
+ */
186
+ mint: string | null
187
+
188
+ /**
189
+ * Transaction timestamp
190
+ */
191
+ timestamp: number
192
+
193
+ /**
194
+ * Optional memo
195
+ */
196
+ memo?: string
197
+ }
198
+
199
+ /**
200
+ * Interface for viewing key storage providers
201
+ */
202
+ export interface ViewingKeyStorage {
203
+ /**
204
+ * Store a viewing key
205
+ * @param key - The viewing key to store
206
+ * @returns Promise resolving to the key's hash (for identification)
207
+ */
208
+ save(key: SolanaViewingKey): Promise<Hash>
209
+
210
+ /**
211
+ * Retrieve a viewing key by its hash
212
+ * @param hash - The viewing key hash
213
+ * @returns Promise resolving to the key or null if not found
214
+ */
215
+ load(hash: Hash): Promise<SolanaViewingKey | null>
216
+
217
+ /**
218
+ * List all stored viewing keys
219
+ * @returns Promise resolving to array of keys
220
+ */
221
+ list(): Promise<SolanaViewingKey[]>
222
+
223
+ /**
224
+ * Delete a viewing key by its hash
225
+ * @param hash - The viewing key hash
226
+ * @returns Promise resolving to true if deleted, false if not found
227
+ */
228
+ delete(hash: Hash): Promise<boolean>
229
+ }
230
+
231
+ // ─── Viewing Key Generation ───────────────────────────────────────────────────
232
+
233
+ /**
234
+ * Generate a viewing key from a spending private key
235
+ *
236
+ * The viewing key is derived deterministically using HMAC-SHA256 with domain
237
+ * separation, ensuring it cannot be used to derive the spending key.
238
+ *
239
+ * @param spendingPrivateKey - The spending private key (32 bytes, hex)
240
+ * @param label - Optional label for the viewing key
241
+ * @returns The generated viewing key with public key and hash
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const viewingKey = generateViewingKeyFromSpending(
246
+ * spendingPrivateKey,
247
+ * 'My Wallet'
248
+ * )
249
+ *
250
+ * // Share the public key for encryption
251
+ * console.log('Public key:', viewingKey.publicKey)
252
+ *
253
+ * // Use hash for on-chain announcement matching
254
+ * console.log('Hash:', viewingKey.hash)
255
+ * ```
256
+ */
257
+ export function generateViewingKeyFromSpending(
258
+ spendingPrivateKey: HexString,
259
+ label?: string
260
+ ): SolanaViewingKey {
261
+ // Validate input
262
+ if (!spendingPrivateKey || !spendingPrivateKey.startsWith('0x')) {
263
+ throw new ValidationError(
264
+ 'spendingPrivateKey must be a hex string with 0x prefix',
265
+ 'spendingPrivateKey'
266
+ )
267
+ }
268
+
269
+ const spendingBytes = hexToBytes(spendingPrivateKey.slice(2))
270
+
271
+ if (spendingBytes.length !== 32) {
272
+ throw new ValidationError(
273
+ 'spendingPrivateKey must be 32 bytes',
274
+ 'spendingPrivateKey'
275
+ )
276
+ }
277
+
278
+ let viewingPrivateBytes: Uint8Array | null = null
279
+
280
+ try {
281
+ // Derive viewing key using HMAC-SHA256 with domain separation
282
+ viewingPrivateBytes = hmac(
283
+ sha256,
284
+ utf8ToBytes(VIEWING_KEY_CONTEXT),
285
+ spendingBytes
286
+ )
287
+
288
+ // Derive public key
289
+ const viewingPublicBytes = ed25519.getPublicKey(viewingPrivateBytes)
290
+
291
+ // Compute hash for announcement matching
292
+ const hashBytes = sha256(viewingPublicBytes)
293
+
294
+ return {
295
+ privateKey: `0x${bytesToHex(viewingPrivateBytes)}` as HexString,
296
+ publicKey: `0x${bytesToHex(viewingPublicBytes)}` as HexString,
297
+ hash: `0x${bytesToHex(hashBytes)}` as Hash,
298
+ label,
299
+ createdAt: Date.now(),
300
+ }
301
+ } finally {
302
+ // Secure wipe sensitive data
303
+ secureWipe(spendingBytes)
304
+ if (viewingPrivateBytes) secureWipe(viewingPrivateBytes)
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Generate a new random viewing key
310
+ *
311
+ * Creates a cryptographically random viewing key that is NOT derived from
312
+ * a spending key. Use this for standalone viewing keys or testing.
313
+ *
314
+ * @param label - Optional label for the viewing key
315
+ * @returns The generated viewing key
316
+ *
317
+ * @example
318
+ * ```typescript
319
+ * const viewingKey = generateRandomViewingKey('Audit Key')
320
+ * ```
321
+ */
322
+ export function generateRandomViewingKey(label?: string): SolanaViewingKey {
323
+ const privateBytes = randomBytes(32)
324
+
325
+ try {
326
+ const publicBytes = ed25519.getPublicKey(privateBytes)
327
+ const hashBytes = sha256(publicBytes)
328
+
329
+ return {
330
+ privateKey: `0x${bytesToHex(privateBytes)}` as HexString,
331
+ publicKey: `0x${bytesToHex(publicBytes)}` as HexString,
332
+ hash: `0x${bytesToHex(hashBytes)}` as Hash,
333
+ label,
334
+ createdAt: Date.now(),
335
+ }
336
+ } finally {
337
+ secureWipe(privateBytes)
338
+ }
339
+ }
340
+
341
+ // ─── Viewing Key Hash ─────────────────────────────────────────────────────────
342
+
343
+ /**
344
+ * Compute the viewing key hash from a public key
345
+ *
346
+ * The hash is used for announcement matching on-chain. Recipients publish
347
+ * their viewing key hash, and senders include it in transaction announcements.
348
+ *
349
+ * @param viewingPublicKey - The viewing public key (32 bytes, hex)
350
+ * @returns The hash for announcement matching
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * const hash = computeViewingKeyHash(viewingKey.publicKey)
355
+ * // Use hash in transaction announcements
356
+ * ```
357
+ */
358
+ export function computeViewingKeyHash(viewingPublicKey: HexString): Hash {
359
+ if (!viewingPublicKey || !viewingPublicKey.startsWith('0x')) {
360
+ throw new ValidationError(
361
+ 'viewingPublicKey must be a hex string with 0x prefix',
362
+ 'viewingPublicKey'
363
+ )
364
+ }
365
+
366
+ const publicBytes = hexToBytes(viewingPublicKey.slice(2))
367
+
368
+ if (publicBytes.length !== 32) {
369
+ throw new ValidationError(
370
+ 'viewingPublicKey must be 32 bytes',
371
+ 'viewingPublicKey'
372
+ )
373
+ }
374
+
375
+ const hashBytes = sha256(publicBytes)
376
+ return `0x${bytesToHex(hashBytes)}` as Hash
377
+ }
378
+
379
+ /**
380
+ * Compute viewing key hash from a private key
381
+ *
382
+ * Derives the public key and computes its hash.
383
+ *
384
+ * @param viewingPrivateKey - The viewing private key (32 bytes, hex)
385
+ * @returns The hash for announcement matching
386
+ */
387
+ export function computeViewingKeyHashFromPrivate(
388
+ viewingPrivateKey: HexString
389
+ ): Hash {
390
+ if (!viewingPrivateKey || !viewingPrivateKey.startsWith('0x')) {
391
+ throw new ValidationError(
392
+ 'viewingPrivateKey must be a hex string with 0x prefix',
393
+ 'viewingPrivateKey'
394
+ )
395
+ }
396
+
397
+ const privateBytes = hexToBytes(viewingPrivateKey.slice(2))
398
+
399
+ if (privateBytes.length !== 32) {
400
+ throw new ValidationError(
401
+ 'viewingPrivateKey must be 32 bytes',
402
+ 'viewingPrivateKey'
403
+ )
404
+ }
405
+
406
+ try {
407
+ const publicBytes = ed25519.getPublicKey(privateBytes)
408
+ const hashBytes = sha256(publicBytes)
409
+ return `0x${bytesToHex(hashBytes)}` as Hash
410
+ } finally {
411
+ secureWipe(privateBytes)
412
+ }
413
+ }
414
+
415
+ // ─── Export/Import ────────────────────────────────────────────────────────────
416
+
417
+ /**
418
+ * Export a viewing key in standard JSON format
419
+ *
420
+ * The export format includes version information for forward compatibility
421
+ * and can be safely serialized to JSON.
422
+ *
423
+ * @param viewingKey - The viewing key to export
424
+ * @returns The export object (serialize with JSON.stringify)
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * const exported = exportViewingKey(viewingKey)
429
+ * const json = JSON.stringify(exported)
430
+ *
431
+ * // Save to file or send to auditor
432
+ * ```
433
+ */
434
+ export function exportViewingKey(viewingKey: SolanaViewingKey): ViewingKeyExport {
435
+ return {
436
+ version: EXPORT_VERSION,
437
+ chain: 'solana',
438
+ privateKey: viewingKey.privateKey,
439
+ publicKey: viewingKey.publicKey,
440
+ hash: viewingKey.hash,
441
+ label: viewingKey.label,
442
+ createdAt: viewingKey.createdAt,
443
+ exportedAt: Date.now(),
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Import a viewing key from standard JSON format
449
+ *
450
+ * Validates the export format and reconstructs the viewing key object.
451
+ *
452
+ * @param exported - The exported viewing key data
453
+ * @returns The imported viewing key
454
+ * @throws {ValidationError} If the export format is invalid
455
+ *
456
+ * @example
457
+ * ```typescript
458
+ * const json = await readFile('viewing-key.json')
459
+ * const exported = JSON.parse(json)
460
+ * const viewingKey = importViewingKey(exported)
461
+ * ```
462
+ */
463
+ export function importViewingKey(exported: ViewingKeyExport): SolanaViewingKey {
464
+ // Validate version
465
+ if (exported.version !== EXPORT_VERSION) {
466
+ throw new ValidationError(
467
+ `Unsupported export version: ${exported.version}. Expected: ${EXPORT_VERSION}`,
468
+ 'version'
469
+ )
470
+ }
471
+
472
+ // Validate chain
473
+ if (exported.chain !== 'solana') {
474
+ throw new ValidationError(
475
+ `Invalid chain: ${exported.chain}. Expected: solana`,
476
+ 'chain'
477
+ )
478
+ }
479
+
480
+ // Validate keys
481
+ if (!isValidHex(exported.privateKey) || exported.privateKey.length !== 66) {
482
+ throw new ValidationError('Invalid private key format', 'privateKey')
483
+ }
484
+
485
+ if (!isValidHex(exported.publicKey) || exported.publicKey.length !== 66) {
486
+ throw new ValidationError('Invalid public key format', 'publicKey')
487
+ }
488
+
489
+ if (!isValidHex(exported.hash) || exported.hash.length !== 66) {
490
+ throw new ValidationError('Invalid hash format', 'hash')
491
+ }
492
+
493
+ // Verify the hash matches the public key
494
+ const computedHash = computeViewingKeyHash(exported.publicKey)
495
+ if (computedHash !== exported.hash) {
496
+ throw new ValidationError(
497
+ 'Hash does not match public key',
498
+ 'hash',
499
+ { expected: computedHash, received: exported.hash }
500
+ )
501
+ }
502
+
503
+ // Verify public key matches private key
504
+ const privateBytes = hexToBytes(exported.privateKey.slice(2))
505
+ try {
506
+ const derivedPublic = `0x${bytesToHex(ed25519.getPublicKey(privateBytes))}` as HexString
507
+ if (derivedPublic !== exported.publicKey) {
508
+ throw new ValidationError(
509
+ 'Public key does not match private key',
510
+ 'publicKey'
511
+ )
512
+ }
513
+ } finally {
514
+ secureWipe(privateBytes)
515
+ }
516
+
517
+ return {
518
+ privateKey: exported.privateKey,
519
+ publicKey: exported.publicKey,
520
+ hash: exported.hash,
521
+ label: exported.label,
522
+ createdAt: exported.createdAt,
523
+ }
524
+ }
525
+
526
+ // ─── Encryption/Decryption ────────────────────────────────────────────────────
527
+
528
+ /**
529
+ * Derive an encryption key from a viewing key using HKDF
530
+ *
531
+ * @param viewingKey - The viewing key (private or public depending on operation)
532
+ * @param isPublic - Whether the key is public (for encryption) or private (for key agreement)
533
+ * @returns 32-byte encryption key (caller must wipe after use)
534
+ */
535
+ function deriveEncryptionKey(key: HexString, salt?: Uint8Array): Uint8Array {
536
+ const keyBytes = hexToBytes(key.slice(2))
537
+
538
+ try {
539
+ // Use HKDF to derive a proper encryption key
540
+ const hkdfSalt = salt ?? utf8ToBytes(ENCRYPTION_DOMAIN)
541
+ return hkdf(sha256, keyBytes, hkdfSalt, utf8ToBytes('encryption'), 32)
542
+ } finally {
543
+ secureWipe(keyBytes)
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Encrypt transaction data for viewing key holders
549
+ *
550
+ * Uses XChaCha20-Poly1305 authenticated encryption with a random nonce.
551
+ * The encryption key is derived from the viewing private key using HKDF.
552
+ *
553
+ * @param data - Transaction data to encrypt
554
+ * @param viewingKey - The viewing key for encryption
555
+ * @returns Encrypted payload with nonce and key hash
556
+ *
557
+ * @example
558
+ * ```typescript
559
+ * const encrypted = encryptForViewing({
560
+ * sender: senderPubkey.toBase58(),
561
+ * recipient: stealthAddress.toBase58(),
562
+ * amount: '1000000',
563
+ * mint: USDC_MINT.toBase58(),
564
+ * timestamp: Date.now(),
565
+ * }, viewingKey)
566
+ *
567
+ * // Store encrypted.ciphertext on-chain or off-chain
568
+ * ```
569
+ */
570
+ export function encryptForViewing(
571
+ data: SolanaTransactionData,
572
+ viewingKey: SolanaViewingKey
573
+ ): EncryptedPayload {
574
+ // Derive encryption key from viewing private key
575
+ const encKey = deriveEncryptionKey(viewingKey.privateKey)
576
+
577
+ try {
578
+ // Generate random nonce
579
+ const nonce = randomBytes(NONCE_SIZE)
580
+
581
+ // Serialize data to JSON
582
+ const plaintext = utf8ToBytes(JSON.stringify(data))
583
+
584
+ // Encrypt with XChaCha20-Poly1305
585
+ const cipher = xchacha20poly1305(encKey, nonce)
586
+ const ciphertext = cipher.encrypt(plaintext)
587
+
588
+ return {
589
+ ciphertext: `0x${bytesToHex(ciphertext)}` as HexString,
590
+ nonce: `0x${bytesToHex(nonce)}` as HexString,
591
+ viewingKeyHash: viewingKey.hash,
592
+ }
593
+ } finally {
594
+ secureWipe(encKey)
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Decrypt transaction data with a viewing key
600
+ *
601
+ * @param encrypted - The encrypted payload
602
+ * @param viewingKey - The viewing key for decryption
603
+ * @returns The decrypted transaction data
604
+ * @throws {CryptoError} If decryption fails (wrong key or tampered data)
605
+ *
606
+ * @example
607
+ * ```typescript
608
+ * const data = decryptWithViewing(encrypted, viewingKey)
609
+ * console.log('Amount:', data.amount)
610
+ * console.log('Sender:', data.sender)
611
+ * ```
612
+ */
613
+ export function decryptWithViewing(
614
+ encrypted: EncryptedPayload,
615
+ viewingKey: SolanaViewingKey
616
+ ): SolanaTransactionData {
617
+ // Verify the viewing key can decrypt this
618
+ if (encrypted.viewingKeyHash !== viewingKey.hash) {
619
+ throw new CryptoError(
620
+ 'Viewing key hash does not match encrypted payload',
621
+ ErrorCode.DECRYPTION_FAILED,
622
+ { context: { expected: encrypted.viewingKeyHash, received: viewingKey.hash } }
623
+ )
624
+ }
625
+
626
+ // Derive encryption key
627
+ const encKey = deriveEncryptionKey(viewingKey.privateKey)
628
+
629
+ try {
630
+ // Parse ciphertext and nonce
631
+ const ciphertext = hexToBytes(encrypted.ciphertext.slice(2))
632
+ const nonce = hexToBytes(encrypted.nonce.slice(2))
633
+
634
+ if (nonce.length !== NONCE_SIZE) {
635
+ throw new ValidationError(
636
+ `Invalid nonce length: ${nonce.length}. Expected: ${NONCE_SIZE}`,
637
+ 'nonce'
638
+ )
639
+ }
640
+
641
+ // Decrypt with XChaCha20-Poly1305
642
+ const cipher = xchacha20poly1305(encKey, nonce)
643
+ const plaintext = cipher.decrypt(ciphertext)
644
+
645
+ // Parse JSON
646
+ const json = new TextDecoder().decode(plaintext)
647
+ return JSON.parse(json) as SolanaTransactionData
648
+ } catch (error) {
649
+ if (error instanceof ValidationError || error instanceof CryptoError) {
650
+ throw error
651
+ }
652
+ throw new CryptoError(
653
+ 'Failed to decrypt: authentication failed or data corrupted',
654
+ ErrorCode.DECRYPTION_FAILED,
655
+ { cause: error instanceof Error ? error : undefined }
656
+ )
657
+ } finally {
658
+ secureWipe(encKey)
659
+ }
660
+ }
661
+
662
+ // ─── In-Memory Storage ────────────────────────────────────────────────────────
663
+
664
+ /**
665
+ * Simple in-memory viewing key storage
666
+ *
667
+ * Useful for testing and temporary storage. For production use,
668
+ * implement a persistent storage provider.
669
+ *
670
+ * @example
671
+ * ```typescript
672
+ * const storage = createMemoryStorage()
673
+ *
674
+ * // Store a key
675
+ * await storage.save(viewingKey)
676
+ *
677
+ * // List all keys
678
+ * const keys = await storage.list()
679
+ *
680
+ * // Load a specific key
681
+ * const key = await storage.load(hash)
682
+ * ```
683
+ */
684
+ export function createMemoryStorage(): ViewingKeyStorage {
685
+ const keys = new Map<Hash, SolanaViewingKey>()
686
+
687
+ return {
688
+ async save(key: SolanaViewingKey): Promise<Hash> {
689
+ keys.set(key.hash, { ...key })
690
+ return key.hash
691
+ },
692
+
693
+ async load(hash: Hash): Promise<SolanaViewingKey | null> {
694
+ const key = keys.get(hash)
695
+ return key ? { ...key } : null
696
+ },
697
+
698
+ async list(): Promise<SolanaViewingKey[]> {
699
+ return Array.from(keys.values()).map(k => ({ ...k }))
700
+ },
701
+
702
+ async delete(hash: Hash): Promise<boolean> {
703
+ return keys.delete(hash)
704
+ },
705
+ }
706
+ }
707
+
708
+ // ─── Utilities ────────────────────────────────────────────────────────────────
709
+
710
+ /**
711
+ * Check if an announcement hash matches a viewing key
712
+ *
713
+ * Used during scanning to efficiently filter announcements that belong
714
+ * to this viewing key.
715
+ *
716
+ * @param announcementHash - Hash from the on-chain announcement
717
+ * @param viewingKey - The viewing key to check against
718
+ * @returns true if the announcement is for this viewing key
719
+ */
720
+ export function isAnnouncementForViewingKey(
721
+ announcementHash: Hash,
722
+ viewingKey: SolanaViewingKey
723
+ ): boolean {
724
+ return announcementHash === viewingKey.hash
725
+ }
726
+
727
+ /**
728
+ * Derive a child viewing key for hierarchical key management
729
+ *
730
+ * Uses HMAC-SHA256 with the parent key and child path to derive
731
+ * a new independent viewing key.
732
+ *
733
+ * @param parentKey - The parent viewing key
734
+ * @param childPath - A path string for derivation (e.g., "audit/2024")
735
+ * @param label - Optional label for the child key
736
+ * @returns The derived child viewing key
737
+ *
738
+ * @example
739
+ * ```typescript
740
+ * const auditKey = deriveChildViewingKey(masterKey, 'audit/2024', 'Audit 2024')
741
+ * const accountingKey = deriveChildViewingKey(masterKey, 'accounting', 'Accounting')
742
+ * ```
743
+ */
744
+ export function deriveChildViewingKey(
745
+ parentKey: SolanaViewingKey,
746
+ childPath: string,
747
+ label?: string
748
+ ): SolanaViewingKey {
749
+ if (!childPath || typeof childPath !== 'string') {
750
+ throw new ValidationError('childPath must be a non-empty string', 'childPath')
751
+ }
752
+
753
+ const parentBytes = hexToBytes(parentKey.privateKey.slice(2))
754
+
755
+ try {
756
+ // Derive child key using HMAC-SHA256
757
+ const childBytes = hmac(sha256, utf8ToBytes(childPath), parentBytes)
758
+ const publicBytes = ed25519.getPublicKey(childBytes)
759
+ const hashBytes = sha256(publicBytes)
760
+
761
+ const result: SolanaViewingKey = {
762
+ privateKey: `0x${bytesToHex(childBytes)}` as HexString,
763
+ publicKey: `0x${bytesToHex(publicBytes)}` as HexString,
764
+ hash: `0x${bytesToHex(hashBytes)}` as Hash,
765
+ label: label ?? `${parentKey.label ?? 'Key'}/${childPath}`,
766
+ createdAt: Date.now(),
767
+ }
768
+
769
+ // Wipe child bytes after creating hex
770
+ secureWipe(childBytes)
771
+
772
+ return result
773
+ } finally {
774
+ secureWipe(parentBytes)
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Get the public key from a viewing private key
780
+ *
781
+ * @param viewingPrivateKey - The viewing private key
782
+ * @returns The corresponding public key
783
+ */
784
+ export function getViewingPublicKey(viewingPrivateKey: HexString): HexString {
785
+ if (!viewingPrivateKey || !viewingPrivateKey.startsWith('0x')) {
786
+ throw new ValidationError(
787
+ 'viewingPrivateKey must be a hex string with 0x prefix',
788
+ 'viewingPrivateKey'
789
+ )
790
+ }
791
+
792
+ const privateBytes = hexToBytes(viewingPrivateKey.slice(2))
793
+
794
+ if (privateBytes.length !== 32) {
795
+ throw new ValidationError(
796
+ 'viewingPrivateKey must be 32 bytes',
797
+ 'viewingPrivateKey'
798
+ )
799
+ }
800
+
801
+ try {
802
+ const publicBytes = ed25519.getPublicKey(privateBytes)
803
+ return `0x${bytesToHex(publicBytes)}` as HexString
804
+ } finally {
805
+ secureWipe(privateBytes)
806
+ }
807
+ }