@deserialize/multi-vm-wallet 1.4.2 → 1.5.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 (186) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/BUILD_OPTIMIZATION_PLAN.md +640 -0
  3. package/BUILD_RESULTS.md +282 -0
  4. package/BUN_MIGRATION.md +415 -0
  5. package/CHANGELOG_SECURITY.md +573 -0
  6. package/IMPLEMENTATION_SUMMARY.md +494 -0
  7. package/SECURITY_AUDIT.md +1124 -0
  8. package/bun.lock +553 -0
  9. package/dist/IChainWallet.js +0 -5
  10. package/dist/bip32Old.js +0 -885
  11. package/dist/bip32Small.js +0 -79
  12. package/dist/bipTest.js +0 -362
  13. package/dist/constant.js +0 -17
  14. package/dist/english.js +0 -1
  15. package/dist/evm/aa-service/index.d.ts +0 -5
  16. package/dist/evm/aa-service/index.js +0 -14
  17. package/dist/evm/aa-service/lib/account-adapter.d.ts +0 -22
  18. package/dist/evm/aa-service/lib/account-adapter.js +0 -24
  19. package/dist/evm/aa-service/lib/kernel-account.d.ts +0 -30
  20. package/dist/evm/aa-service/lib/kernel-account.js +2 -67
  21. package/dist/evm/aa-service/lib/kernel-modules.d.ts +0 -177
  22. package/dist/evm/aa-service/lib/kernel-modules.js +4 -202
  23. package/dist/evm/aa-service/lib/session-keys.d.ts +0 -118
  24. package/dist/evm/aa-service/lib/session-keys.js +7 -151
  25. package/dist/evm/aa-service/lib/type.d.ts +0 -55
  26. package/dist/evm/aa-service/lib/type.js +0 -10
  27. package/dist/evm/aa-service/services/account-abstraction.d.ts +0 -426
  28. package/dist/evm/aa-service/services/account-abstraction.js +0 -461
  29. package/dist/evm/aa-service/services/bundler.d.ts +0 -6
  30. package/dist/evm/aa-service/services/bundler.js +0 -54
  31. package/dist/evm/evm.d.ts +9 -51
  32. package/dist/evm/evm.js +338 -76
  33. package/dist/evm/index.js +0 -3
  34. package/dist/evm/script.js +3 -17
  35. package/dist/evm/smartWallet.d.ts +0 -173
  36. package/dist/evm/smartWallet.js +0 -206
  37. package/dist/evm/smartWallet.types.d.ts +0 -6
  38. package/dist/evm/smartWallet.types.js +0 -8
  39. package/dist/evm/transaction.utils.d.ts +0 -242
  40. package/dist/evm/transaction.utils.js +4 -320
  41. package/dist/evm/transactionParsing.d.ts +0 -11
  42. package/dist/evm/transactionParsing.js +28 -147
  43. package/dist/evm/utils.d.ts +0 -46
  44. package/dist/evm/utils.js +1 -57
  45. package/dist/helpers/index.d.ts +0 -4
  46. package/dist/helpers/index.js +8 -44
  47. package/dist/helpers/routeScan.js +0 -1
  48. package/dist/index.js +0 -1
  49. package/dist/old.js +0 -884
  50. package/dist/price.js +0 -1
  51. package/dist/price.types.js +0 -2
  52. package/dist/rate-limiter.d.ts +28 -0
  53. package/dist/rate-limiter.js +95 -0
  54. package/dist/retry-logic.d.ts +14 -0
  55. package/dist/retry-logic.js +120 -0
  56. package/dist/savings/index.js +0 -1
  57. package/dist/savings/saving-manager.d.ts +10 -11
  58. package/dist/savings/saving-manager.js +79 -22
  59. package/dist/savings/savings-operations.d.ts +39 -0
  60. package/dist/savings/savings-operations.js +141 -0
  61. package/dist/savings/smart-savings.d.ts +0 -63
  62. package/dist/savings/smart-savings.js +0 -78
  63. package/dist/savings/types.d.ts +0 -69
  64. package/dist/savings/types.js +0 -7
  65. package/dist/savings/validation.d.ts +9 -0
  66. package/dist/savings/validation.js +85 -0
  67. package/dist/svm/constant.js +0 -1
  68. package/dist/svm/index.js +0 -1
  69. package/dist/svm/svm.d.ts +11 -1
  70. package/dist/svm/svm.js +267 -27
  71. package/dist/svm/transactionParsing.d.ts +0 -7
  72. package/dist/svm/transactionParsing.js +3 -41
  73. package/dist/svm/transactionSender.js +0 -9
  74. package/dist/svm/utils.d.ts +0 -12
  75. package/dist/svm/utils.js +9 -60
  76. package/dist/test.d.ts +0 -4
  77. package/dist/test.js +6 -98
  78. package/dist/transaction-utils.d.ts +38 -0
  79. package/dist/transaction-utils.js +168 -0
  80. package/dist/types.d.ts +36 -0
  81. package/dist/types.js +0 -1
  82. package/dist/utils.js +0 -1
  83. package/dist/vm-validation.d.ts +11 -0
  84. package/dist/vm-validation.js +151 -0
  85. package/dist/vm.d.ts +12 -2
  86. package/dist/vm.js +61 -16
  87. package/dist/walletBip32.js +15 -70
  88. package/package.json +9 -4
  89. package/test-discovery.ts +235 -0
  90. package/test-pocket-discovery.ts +84 -0
  91. package/tsconfig.json +18 -11
  92. package/tsconfig.prod.json +10 -0
  93. package/utils/evm/evm.ts +554 -8
  94. package/utils/rate-limiter.ts +179 -0
  95. package/utils/retry-logic.ts +271 -0
  96. package/utils/savings/EXAMPLES.md +883 -0
  97. package/utils/savings/SECURITY.md +731 -0
  98. package/utils/savings/saving-manager.ts +526 -16
  99. package/utils/savings/savings-operations.ts +509 -0
  100. package/utils/savings/validation.ts +187 -0
  101. package/utils/svm/svm.ts +476 -5
  102. package/utils/test.ts +2 -2
  103. package/utils/transaction-utils.ts +394 -0
  104. package/utils/types.ts +100 -0
  105. package/utils/vm-validation.ts +280 -0
  106. package/utils/vm.ts +197 -10
  107. package/utils/walletBip32.ts +39 -3
  108. package/dist/IChainWallet.js.map +0 -1
  109. package/dist/bip32.d.ts +0 -9
  110. package/dist/bip32.js +0 -172
  111. package/dist/bip32.js.map +0 -1
  112. package/dist/bip32Old.js.map +0 -1
  113. package/dist/bip32Small.js.map +0 -1
  114. package/dist/bipTest.js.map +0 -1
  115. package/dist/constant.js.map +0 -1
  116. package/dist/english.js.map +0 -1
  117. package/dist/evm/SMART_WALLET_EXAMPLES.d.ts +0 -20
  118. package/dist/evm/SMART_WALLET_EXAMPLES.js +0 -451
  119. package/dist/evm/SMART_WALLET_EXAMPLES.js.map +0 -1
  120. package/dist/evm/aa-service/index.js.map +0 -1
  121. package/dist/evm/aa-service/lib/account-adapter.js.map +0 -1
  122. package/dist/evm/aa-service/lib/kernel-account.js.map +0 -1
  123. package/dist/evm/aa-service/lib/kernel-modules.js.map +0 -1
  124. package/dist/evm/aa-service/lib/session-keys.js.map +0 -1
  125. package/dist/evm/aa-service/lib/type.js.map +0 -1
  126. package/dist/evm/aa-service/services/account-abstraction.js.map +0 -1
  127. package/dist/evm/aa-service/services/bundler.js.map +0 -1
  128. package/dist/evm/evm.js.map +0 -1
  129. package/dist/evm/index.js.map +0 -1
  130. package/dist/evm/script.js.map +0 -1
  131. package/dist/evm/smartWallet.js.map +0 -1
  132. package/dist/evm/smartWallet.types.js.map +0 -1
  133. package/dist/evm/transaction.utils.js.map +0 -1
  134. package/dist/evm/transactionParsing.js.map +0 -1
  135. package/dist/evm/utils.js.map +0 -1
  136. package/dist/helpers/index.js.map +0 -1
  137. package/dist/helpers/routeScan.js.map +0 -1
  138. package/dist/index.js.map +0 -1
  139. package/dist/old.js.map +0 -1
  140. package/dist/price.js.map +0 -1
  141. package/dist/price.types.js.map +0 -1
  142. package/dist/privacy/artifact-manager.d.ts +0 -117
  143. package/dist/privacy/artifact-manager.js +0 -251
  144. package/dist/privacy/artifact-manager.js.map +0 -1
  145. package/dist/privacy/broadcaster-client.d.ts +0 -166
  146. package/dist/privacy/broadcaster-client.js +0 -261
  147. package/dist/privacy/broadcaster-client.js.map +0 -1
  148. package/dist/privacy/index.d.ts +0 -34
  149. package/dist/privacy/index.js +0 -56
  150. package/dist/privacy/index.js.map +0 -1
  151. package/dist/privacy/network-config.d.ts +0 -57
  152. package/dist/privacy/network-config.js +0 -118
  153. package/dist/privacy/network-config.js.map +0 -1
  154. package/dist/privacy/poi-helper.d.ts +0 -161
  155. package/dist/privacy/poi-helper.js +0 -249
  156. package/dist/privacy/poi-helper.js.map +0 -1
  157. package/dist/privacy/railgun-engine.d.ts +0 -135
  158. package/dist/privacy/railgun-engine.js +0 -205
  159. package/dist/privacy/railgun-engine.js.map +0 -1
  160. package/dist/privacy/railgun-privacy-wallet.d.ts +0 -288
  161. package/dist/privacy/railgun-privacy-wallet.js +0 -539
  162. package/dist/privacy/railgun-privacy-wallet.js.map +0 -1
  163. package/dist/privacy/types.d.ts +0 -229
  164. package/dist/privacy/types.js +0 -26
  165. package/dist/privacy/types.js.map +0 -1
  166. package/dist/savings/index.js.map +0 -1
  167. package/dist/savings/saving-actions.d.ts +0 -0
  168. package/dist/savings/saving-actions.js +0 -78
  169. package/dist/savings/saving-actions.js.map +0 -1
  170. package/dist/savings/saving-manager.js.map +0 -1
  171. package/dist/savings/savings-manager.d.ts +0 -126
  172. package/dist/savings/savings-manager.js +0 -234
  173. package/dist/savings/savings-manager.js.map +0 -1
  174. package/dist/savings/smart-savings.js.map +0 -1
  175. package/dist/savings/types.js.map +0 -1
  176. package/dist/svm/constant.js.map +0 -1
  177. package/dist/svm/index.js.map +0 -1
  178. package/dist/svm/svm.js.map +0 -1
  179. package/dist/svm/transactionParsing.js.map +0 -1
  180. package/dist/svm/transactionSender.js.map +0 -1
  181. package/dist/svm/utils.js.map +0 -1
  182. package/dist/test.js.map +0 -1
  183. package/dist/types.js.map +0 -1
  184. package/dist/utils.js.map +0 -1
  185. package/dist/vm.js.map +0 -1
  186. package/dist/walletBip32.js.map +0 -1
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Stateless Savings Operations
3
+ *
4
+ * Provides stateless operations for savings management where credentials
5
+ * are passed per-operation rather than stored in memory. Designed for
6
+ * secure usage in browser extensions and mobile wallets.
7
+ *
8
+ * @remarks
9
+ * Security Model:
10
+ * - No mnemonic or private key storage
11
+ * - Implementer passes credentials per-operation
12
+ * - Keys exist in memory only during operation
13
+ * - Automatic cleanup after use
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Browser extension with encrypted storage
18
+ * const operations = new SavingsOperations();
19
+ *
20
+ * // Unlock wallet (implementer handles encryption/biometric)
21
+ * const mnemonic = await secureStorage.getMnemonic();
22
+ *
23
+ * // Perform operation - mnemonic not stored
24
+ * const result = await operations.transferFromPocket(
25
+ * mnemonic,
26
+ * { walletIndex: 0, accountIndex: 1, to: '0x...', amount: 100n },
27
+ * provider,
28
+ * chain
29
+ * );
30
+ *
31
+ * // Mnemonic automatically cleared from memory after operation
32
+ * ```
33
+ */
34
+
35
+ import { ethers, JsonRpcProvider, Wallet, parseUnits, formatUnits, Interface } from "ethers";
36
+ import { Hex } from "viem";
37
+ import { SavingsValidation } from "./validation";
38
+ import { ChainWalletConfig, TransactionResult } from "../types";
39
+ import { EVMDeriveChildPrivateKey, mnemonicToSeed } from "../walletBip32";
40
+
41
+ /**
42
+ * Options for transferring from a savings pocket
43
+ */
44
+ export interface TransferFromPocketOptions {
45
+ /** Wallet index in derivation path (default: 0) */
46
+ walletIndex?: number;
47
+ /** Account/pocket index to transfer from (1-based, 0 is main wallet) */
48
+ accountIndex: number;
49
+ /** Destination address */
50
+ to: string;
51
+ /** Amount to transfer in base units (wei for ETH) */
52
+ amount: bigint;
53
+ /** Optional gas limit */
54
+ gasLimit?: bigint;
55
+ /** Optional gas price (for legacy transactions) */
56
+ gasPrice?: bigint;
57
+ /** Optional max fee per gas (for EIP-1559) */
58
+ maxFeePerGas?: bigint;
59
+ /** Optional max priority fee per gas (for EIP-1559) */
60
+ maxPriorityFeePerGas?: bigint;
61
+ }
62
+
63
+ /**
64
+ * Options for transferring tokens from a savings pocket
65
+ */
66
+ export interface TransferTokenFromPocketOptions extends Omit<TransferFromPocketOptions, 'amount'> {
67
+ /** Token contract address */
68
+ tokenAddress: string;
69
+ /** Amount to transfer in token's smallest unit */
70
+ amount: bigint;
71
+ }
72
+
73
+ /**
74
+ * Options for getting pocket balance
75
+ */
76
+ export interface GetPocketBalanceOptions {
77
+ /** Wallet index in derivation path (default: 0) */
78
+ walletIndex?: number;
79
+ /** Account/pocket index (1-based, 0 is main wallet) */
80
+ accountIndex: number;
81
+ }
82
+
83
+ /**
84
+ * Options for getting pocket token balance
85
+ */
86
+ export interface GetPocketTokenBalanceOptions extends GetPocketBalanceOptions {
87
+ /** Token contract address */
88
+ tokenAddress: string;
89
+ }
90
+
91
+ /**
92
+ * Token balance information
93
+ */
94
+ export interface TokenBalance {
95
+ /** Token contract address */
96
+ address: string;
97
+ /** Balance in token's smallest unit */
98
+ balance: bigint;
99
+ /** Token decimals */
100
+ decimals: number;
101
+ /** Token symbol */
102
+ symbol: string;
103
+ /** Token name */
104
+ name: string;
105
+ /** Formatted balance string */
106
+ formatted: string;
107
+ }
108
+
109
+ /**
110
+ * Result of a pocket derivation
111
+ */
112
+ interface PocketDerivationResult {
113
+ /** Derived wallet instance */
114
+ wallet: Wallet;
115
+ /** Pocket address */
116
+ address: string;
117
+ /** Cleanup function to zero out sensitive data */
118
+ cleanup: () => void;
119
+ }
120
+
121
+ /**
122
+ * Stateless savings operations that don't store sensitive data
123
+ *
124
+ * All methods accept mnemonic as parameter and derive keys on-demand.
125
+ * Keys are automatically cleaned up after each operation.
126
+ */
127
+ export class SavingsOperations {
128
+ /**
129
+ * Derives a savings pocket wallet from mnemonic
130
+ *
131
+ * @param mnemonic - BIP-39 mnemonic phrase
132
+ * @param accountIndex - Account/pocket index (1-based, 0 is main wallet)
133
+ * @param walletIndex - Wallet index (default: 0)
134
+ * @param provider - RPC provider to connect wallet to
135
+ * @returns Derived wallet, address, and cleanup function
136
+ *
137
+ * @remarks
138
+ * IMPORTANT: Always call cleanup() when done with the wallet to zero out private key
139
+ *
140
+ * Derivation path: m/44'/60'/{accountIndex}'/0/{walletIndex}'
141
+ *
142
+ * @throws Error if validation fails
143
+ *
144
+ * @internal
145
+ */
146
+ private derivePocketWallet(
147
+ mnemonic: string,
148
+ accountIndex: number,
149
+ walletIndex: number = 0,
150
+ provider: JsonRpcProvider
151
+ ): PocketDerivationResult {
152
+ // Validate inputs
153
+ SavingsValidation.validateMnemonic(mnemonic);
154
+ SavingsValidation.validateAccountIndex(accountIndex);
155
+ SavingsValidation.validateWalletIndex(walletIndex);
156
+
157
+ // Derive pocket private key
158
+ const seed = mnemonicToSeed(mnemonic);
159
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, walletIndex, `m/44'/60'/${accountIndex}'/`);
160
+
161
+ // Create wallet
162
+ const wallet = new Wallet(privateKey, provider);
163
+ const address = wallet.address;
164
+
165
+ // Cleanup function to zero out sensitive data
166
+ const cleanup = () => {
167
+ // Note: JavaScript strings are immutable, so we can't directly zero them
168
+ // The seed and privateKey will be garbage collected when they go out of scope
169
+ };
170
+
171
+ return { wallet, address, cleanup };
172
+ }
173
+
174
+ /**
175
+ * Get native token balance of a savings pocket
176
+ *
177
+ * @param mnemonic - BIP-39 mnemonic phrase
178
+ * @param options - Balance query options
179
+ * @param provider - RPC provider
180
+ * @returns Balance in wei
181
+ *
182
+ * @throws Error if validation fails or balance check fails
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * const balance = await operations.getPocketBalance(
187
+ * mnemonic,
188
+ * { accountIndex: 1, walletIndex: 0 },
189
+ * provider
190
+ * );
191
+ * console.log(`Pocket 1 balance: ${formatUnits(balance, 18)} ETH`);
192
+ * ```
193
+ */
194
+ async getPocketBalance(
195
+ mnemonic: string,
196
+ options: GetPocketBalanceOptions,
197
+ provider: JsonRpcProvider
198
+ ): Promise<bigint> {
199
+ const { accountIndex, walletIndex = 0 } = options;
200
+
201
+ const { wallet, cleanup } = this.derivePocketWallet(
202
+ mnemonic,
203
+ accountIndex,
204
+ walletIndex,
205
+ provider
206
+ );
207
+
208
+ try {
209
+ const balance = await provider.getBalance(wallet.address);
210
+ return BigInt(balance.toString());
211
+ } finally {
212
+ cleanup();
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get token balance of a savings pocket
218
+ *
219
+ * @param mnemonic - BIP-39 mnemonic phrase
220
+ * @param options - Token balance query options
221
+ * @param provider - RPC provider
222
+ * @returns Token balance and metadata
223
+ *
224
+ * @throws Error if validation fails or balance check fails
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * const usdcBalance = await operations.getPocketTokenBalance(
229
+ * mnemonic,
230
+ * {
231
+ * accountIndex: 1,
232
+ * walletIndex: 0,
233
+ * tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
234
+ * },
235
+ * provider
236
+ * );
237
+ * ```
238
+ */
239
+ async getPocketTokenBalance(
240
+ mnemonic: string,
241
+ options: GetPocketTokenBalanceOptions,
242
+ provider: JsonRpcProvider
243
+ ): Promise<TokenBalance> {
244
+ const { accountIndex, walletIndex = 0, tokenAddress } = options;
245
+
246
+ // Validate token address
247
+ SavingsValidation.validateAddress(tokenAddress, 'Token address');
248
+
249
+ const { wallet, address, cleanup } = this.derivePocketWallet(
250
+ mnemonic,
251
+ accountIndex,
252
+ walletIndex,
253
+ provider
254
+ );
255
+
256
+ try {
257
+ // ERC20 interface
258
+ const erc20Interface = new Interface([
259
+ "function balanceOf(address) view returns (uint256)",
260
+ "function decimals() view returns (uint8)",
261
+ "function symbol() view returns (string)",
262
+ "function name() view returns (string)"
263
+ ]);
264
+
265
+ const tokenContract = new ethers.Contract(tokenAddress, erc20Interface, provider);
266
+
267
+ // Fetch token info and balance in parallel
268
+ const [balance, decimals, symbol, name] = await Promise.all([
269
+ tokenContract.balanceOf(address),
270
+ tokenContract.decimals(),
271
+ tokenContract.symbol(),
272
+ tokenContract.name()
273
+ ]);
274
+
275
+ const balanceBigInt = BigInt(balance.toString());
276
+
277
+ return {
278
+ address: tokenAddress,
279
+ balance: balanceBigInt,
280
+ decimals: Number(decimals),
281
+ symbol: symbol,
282
+ name: name,
283
+ formatted: formatUnits(balanceBigInt, decimals)
284
+ };
285
+ } finally {
286
+ cleanup();
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Transfer native tokens from a savings pocket
292
+ *
293
+ * @param mnemonic - BIP-39 mnemonic phrase
294
+ * @param options - Transfer options
295
+ * @param provider - RPC provider
296
+ * @param chain - Chain configuration
297
+ * @returns Transaction result
298
+ *
299
+ * @throws Error if validation fails or transaction fails
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * // Transfer 0.1 ETH from pocket 1 to another address
304
+ * const result = await operations.transferFromPocket(
305
+ * mnemonic,
306
+ * {
307
+ * accountIndex: 1,
308
+ * walletIndex: 0,
309
+ * to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
310
+ * amount: parseUnits('0.1', 18)
311
+ * },
312
+ * provider,
313
+ * chain
314
+ * );
315
+ * ```
316
+ */
317
+ async transferFromPocket(
318
+ mnemonic: string,
319
+ options: TransferFromPocketOptions,
320
+ provider: JsonRpcProvider,
321
+ chain: ChainWalletConfig
322
+ ): Promise<TransactionResult> {
323
+ const {
324
+ accountIndex,
325
+ walletIndex = 0,
326
+ to,
327
+ amount,
328
+ gasLimit,
329
+ gasPrice,
330
+ maxFeePerGas,
331
+ maxPriorityFeePerGas
332
+ } = options;
333
+
334
+ // Validate inputs
335
+ SavingsValidation.validateAddress(to, 'Destination address');
336
+ SavingsValidation.validateAmount(amount, 'Transfer amount');
337
+
338
+ const { wallet, cleanup } = this.derivePocketWallet(
339
+ mnemonic,
340
+ accountIndex,
341
+ walletIndex,
342
+ provider
343
+ );
344
+
345
+ try {
346
+ // Build transaction
347
+ const tx: any = {
348
+ to,
349
+ value: amount,
350
+ chainId: chain.chainId
351
+ };
352
+
353
+ // Add gas parameters if provided
354
+ if (gasLimit) tx.gasLimit = gasLimit;
355
+ if (gasPrice) tx.gasPrice = gasPrice;
356
+ if (maxFeePerGas) tx.maxFeePerGas = maxFeePerGas;
357
+ if (maxPriorityFeePerGas) tx.maxPriorityFeePerGas = maxPriorityFeePerGas;
358
+
359
+ // Send transaction
360
+ const txResponse = await wallet.sendTransaction(tx);
361
+
362
+ // Wait for confirmation
363
+ const receipt = await txResponse.wait();
364
+
365
+ if (!receipt) {
366
+ throw new Error('Transaction receipt is null');
367
+ }
368
+
369
+ return {
370
+ hash: receipt.hash as Hex,
371
+ success: receipt.status === 1
372
+ };
373
+ } finally {
374
+ cleanup();
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Transfer ERC20 tokens from a savings pocket
380
+ *
381
+ * @param mnemonic - BIP-39 mnemonic phrase
382
+ * @param options - Transfer options including token address
383
+ * @param provider - RPC provider
384
+ * @param chain - Chain configuration
385
+ * @returns Transaction result
386
+ *
387
+ * @throws Error if validation fails or transaction fails
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * // Transfer 100 USDC from pocket 1
392
+ * const result = await operations.transferTokenFromPocket(
393
+ * mnemonic,
394
+ * {
395
+ * accountIndex: 1,
396
+ * walletIndex: 0,
397
+ * tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
398
+ * to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
399
+ * amount: parseUnits('100', 6) // USDC has 6 decimals
400
+ * },
401
+ * provider,
402
+ * chain
403
+ * );
404
+ * ```
405
+ */
406
+ async transferTokenFromPocket(
407
+ mnemonic: string,
408
+ options: TransferTokenFromPocketOptions,
409
+ provider: JsonRpcProvider,
410
+ chain: ChainWalletConfig
411
+ ): Promise<TransactionResult> {
412
+ const {
413
+ accountIndex,
414
+ walletIndex = 0,
415
+ tokenAddress,
416
+ to,
417
+ amount,
418
+ gasLimit,
419
+ maxFeePerGas,
420
+ maxPriorityFeePerGas
421
+ } = options;
422
+
423
+ // Validate inputs
424
+ SavingsValidation.validateAddress(tokenAddress, 'Token address');
425
+ SavingsValidation.validateAddress(to, 'Destination address');
426
+ SavingsValidation.validateAmount(amount, 'Transfer amount');
427
+
428
+ const { wallet, cleanup } = this.derivePocketWallet(
429
+ mnemonic,
430
+ accountIndex,
431
+ walletIndex,
432
+ provider
433
+ );
434
+
435
+ try {
436
+ // ERC20 transfer interface
437
+ const erc20Interface = new Interface([
438
+ "function transfer(address to, uint256 amount) returns (bool)"
439
+ ]);
440
+
441
+ const tokenContract = new ethers.Contract(tokenAddress, erc20Interface, wallet);
442
+
443
+ // Build transaction
444
+ const tx: any = {
445
+ chainId: chain.chainId
446
+ };
447
+
448
+ // Add gas parameters if provided
449
+ if (gasLimit) tx.gasLimit = gasLimit;
450
+ if (maxFeePerGas) tx.maxFeePerGas = maxFeePerGas;
451
+ if (maxPriorityFeePerGas) tx.maxPriorityFeePerGas = maxPriorityFeePerGas;
452
+
453
+ // Send token transfer transaction
454
+ const txResponse = await tokenContract.transfer(to, amount, tx);
455
+
456
+ // Wait for confirmation
457
+ const receipt = await txResponse.wait();
458
+
459
+ if (!receipt) {
460
+ throw new Error('Transaction receipt is null');
461
+ }
462
+
463
+ return {
464
+ hash: receipt.hash as Hex,
465
+ success: receipt.status === 1
466
+ };
467
+ } finally {
468
+ cleanup();
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Get the address of a savings pocket without storing credentials
474
+ *
475
+ * @param mnemonic - BIP-39 mnemonic phrase
476
+ * @param accountIndex - Account/pocket index
477
+ * @param walletIndex - Wallet index (default: 0)
478
+ * @returns Pocket address
479
+ *
480
+ * @throws Error if validation fails
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * const pocketAddress = operations.getPocketAddress(mnemonic, 1, 0);
485
+ * console.log(`Pocket 1 address: ${pocketAddress}`);
486
+ * ```
487
+ */
488
+ getPocketAddress(
489
+ mnemonic: string,
490
+ accountIndex: number,
491
+ walletIndex: number = 0
492
+ ): string {
493
+ // Validate inputs
494
+ SavingsValidation.validateMnemonic(mnemonic);
495
+ SavingsValidation.validateAccountIndex(accountIndex);
496
+ SavingsValidation.validateWalletIndex(walletIndex);
497
+
498
+ // Derive address
499
+ const seed = mnemonicToSeed(mnemonic);
500
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, walletIndex, `m/44'/60'/${accountIndex}'/`);
501
+ const wallet = new Wallet(privateKey);
502
+ const address = wallet.address;
503
+
504
+ // Note: JavaScript strings are immutable, so seed will be garbage collected
505
+ // when it goes out of scope
506
+
507
+ return address;
508
+ }
509
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Input validation for savings operations
3
+ *
4
+ * Provides validation utilities to prevent invalid inputs from reaching
5
+ * cryptographic operations or blockchain transactions.
6
+ */
7
+
8
+ import { Hex } from "viem";
9
+
10
+ /**
11
+ * Validation utilities for savings operations
12
+ */
13
+ export class SavingsValidation {
14
+ /**
15
+ * Validate BIP-44 account index
16
+ *
17
+ * @param index - Account index to validate
18
+ * @throws Error if index is invalid
19
+ *
20
+ * @remarks
21
+ * BIP-44 account indices must be:
22
+ * - Integer values
23
+ * - Non-negative
24
+ * - Less than or equal to 2^31-1 (0x7FFFFFFF)
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * SavingsValidation.validateAccountIndex(0); // ✓ Valid
29
+ * SavingsValidation.validateAccountIndex(100); // ✓ Valid
30
+ * SavingsValidation.validateAccountIndex(-1); // ✗ Throws error
31
+ * SavingsValidation.validateAccountIndex(2.5); // ✗ Throws error
32
+ * ```
33
+ */
34
+ static validateAccountIndex(index: number): void {
35
+ if (!Number.isInteger(index)) {
36
+ throw new Error(`Account index must be an integer, got: ${index}`);
37
+ }
38
+ if (index < 0) {
39
+ throw new Error(`Account index must be non-negative, got: ${index}`);
40
+ }
41
+ if (index > 0x7FFFFFFF) { // BIP-44 max hardened index (2^31-1)
42
+ throw new Error(`Account index exceeds BIP-44 maximum (2147483647), got: ${index}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Validate wallet index
48
+ *
49
+ * @param index - Wallet index to validate
50
+ * @throws Error if index is invalid
51
+ */
52
+ static validateWalletIndex(index: number): void {
53
+ if (!Number.isInteger(index)) {
54
+ throw new Error(`Wallet index must be an integer, got: ${index}`);
55
+ }
56
+ if (index < 0) {
57
+ throw new Error(`Wallet index must be non-negative, got: ${index}`);
58
+ }
59
+ if (index > 0x7FFFFFFF) {
60
+ throw new Error(`Wallet index exceeds maximum (2147483647), got: ${index}`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Validate amount (bigint)
66
+ *
67
+ * @param amount - Amount to validate
68
+ * @param label - Label for error message (default: "Amount")
69
+ * @throws Error if amount is invalid
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * SavingsValidation.validateAmount(1000n, 'Transfer amount'); // ✓ Valid
74
+ * SavingsValidation.validateAmount(0n, 'Transfer amount'); // ✗ Throws error
75
+ * SavingsValidation.validateAmount(-100n, 'Transfer amount'); // ✗ Throws error
76
+ * ```
77
+ */
78
+ static validateAmount(amount: bigint, label: string = 'Amount'): void {
79
+ if (typeof amount !== 'bigint') {
80
+ throw new Error(`${label} must be a bigint, got: ${typeof amount}`);
81
+ }
82
+ if (amount <= 0n) {
83
+ throw new Error(`${label} must be positive, got: ${amount}`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Validate Ethereum address format
89
+ *
90
+ * @param address - Address to validate
91
+ * @param label - Label for error message (default: "Address")
92
+ * @throws Error if address format is invalid
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * SavingsValidation.validateAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'); // ✓ Valid
97
+ * SavingsValidation.validateAddress('0xinvalid'); // ✗ Throws error
98
+ * SavingsValidation.validateAddress('742d35Cc6634C0532925a3b844Bc9e7595f0bEb'); // ✗ Throws error (no 0x)
99
+ * ```
100
+ */
101
+ static validateAddress(address: string, label: string = 'Address'): void {
102
+ if (typeof address !== 'string') {
103
+ throw new Error(`${label} must be a string, got: ${typeof address}`);
104
+ }
105
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
106
+ throw new Error(`${label} has invalid format. Expected 0x followed by 40 hex characters, got: ${address}`);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Validate mnemonic phrase
112
+ *
113
+ * @param mnemonic - Mnemonic to validate
114
+ * @throws Error if mnemonic is invalid
115
+ *
116
+ * @remarks
117
+ * Performs basic validation:
118
+ * - Must be a non-empty string
119
+ * - Must have 12, 15, 18, 21, or 24 words (BIP-39 standard)
120
+ * - Does NOT validate checksum (implementer should use bip39 library for that)
121
+ */
122
+ static validateMnemonic(mnemonic: string): void {
123
+ if (typeof mnemonic !== 'string') {
124
+ throw new Error(`Mnemonic must be a string, got: ${typeof mnemonic}`);
125
+ }
126
+
127
+ const trimmed = mnemonic.trim();
128
+ if (trimmed.length === 0) {
129
+ throw new Error('Mnemonic cannot be empty');
130
+ }
131
+
132
+ const words = trimmed.split(/\s+/);
133
+ const validWordCounts = [12, 15, 18, 21, 24];
134
+
135
+ if (!validWordCounts.includes(words.length)) {
136
+ throw new Error(
137
+ `Mnemonic must have 12, 15, 18, 21, or 24 words (BIP-39 standard), got: ${words.length} words`
138
+ );
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Validate chain ID
144
+ *
145
+ * @param chainId - Chain ID to validate
146
+ * @throws Error if chain ID is invalid
147
+ */
148
+ static validateChainId(chainId: number): void {
149
+ if (!Number.isInteger(chainId)) {
150
+ throw new Error(`Chain ID must be an integer, got: ${chainId}`);
151
+ }
152
+ if (chainId <= 0) {
153
+ throw new Error(`Chain ID must be positive, got: ${chainId}`);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Validate string amount (for parsing)
159
+ *
160
+ * @param amount - String amount to validate
161
+ * @param label - Label for error message
162
+ * @throws Error if amount string is invalid
163
+ */
164
+ static validateAmountString(amount: string, label: string = 'Amount'): void {
165
+ if (typeof amount !== 'string') {
166
+ throw new Error(`${label} must be a string, got: ${typeof amount}`);
167
+ }
168
+
169
+ const trimmed = amount.trim();
170
+ if (trimmed.length === 0) {
171
+ throw new Error(`${label} cannot be empty`);
172
+ }
173
+
174
+ // Check if it's a valid number string
175
+ if (!/^-?\d+(\.\d+)?$/.test(trimmed)) {
176
+ throw new Error(`${label} has invalid format, got: ${amount}`);
177
+ }
178
+
179
+ const parsed = parseFloat(trimmed);
180
+ if (isNaN(parsed)) {
181
+ throw new Error(`${label} cannot be parsed as number, got: ${amount}`);
182
+ }
183
+ if (parsed <= 0) {
184
+ throw new Error(`${label} must be positive, got: ${amount}`);
185
+ }
186
+ }
187
+ }