@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.
- package/.claude/settings.local.json +7 -1
- package/BUILD_OPTIMIZATION_PLAN.md +640 -0
- package/BUILD_RESULTS.md +282 -0
- package/BUN_MIGRATION.md +415 -0
- package/CHANGELOG_SECURITY.md +573 -0
- package/IMPLEMENTATION_SUMMARY.md +494 -0
- package/SECURITY_AUDIT.md +1124 -0
- package/bun.lock +553 -0
- package/dist/IChainWallet.js +0 -5
- package/dist/bip32Old.js +0 -885
- package/dist/bip32Small.js +0 -79
- package/dist/bipTest.js +0 -362
- package/dist/constant.js +0 -17
- package/dist/english.js +0 -1
- package/dist/evm/aa-service/index.d.ts +0 -5
- package/dist/evm/aa-service/index.js +0 -14
- package/dist/evm/aa-service/lib/account-adapter.d.ts +0 -22
- package/dist/evm/aa-service/lib/account-adapter.js +0 -24
- package/dist/evm/aa-service/lib/kernel-account.d.ts +0 -30
- package/dist/evm/aa-service/lib/kernel-account.js +2 -67
- package/dist/evm/aa-service/lib/kernel-modules.d.ts +0 -177
- package/dist/evm/aa-service/lib/kernel-modules.js +4 -202
- package/dist/evm/aa-service/lib/session-keys.d.ts +0 -118
- package/dist/evm/aa-service/lib/session-keys.js +7 -151
- package/dist/evm/aa-service/lib/type.d.ts +0 -55
- package/dist/evm/aa-service/lib/type.js +0 -10
- package/dist/evm/aa-service/services/account-abstraction.d.ts +0 -426
- package/dist/evm/aa-service/services/account-abstraction.js +0 -461
- package/dist/evm/aa-service/services/bundler.d.ts +0 -6
- package/dist/evm/aa-service/services/bundler.js +0 -54
- package/dist/evm/evm.d.ts +9 -51
- package/dist/evm/evm.js +338 -76
- package/dist/evm/index.js +0 -3
- package/dist/evm/script.js +3 -17
- package/dist/evm/smartWallet.d.ts +0 -173
- package/dist/evm/smartWallet.js +0 -206
- package/dist/evm/smartWallet.types.d.ts +0 -6
- package/dist/evm/smartWallet.types.js +0 -8
- package/dist/evm/transaction.utils.d.ts +0 -242
- package/dist/evm/transaction.utils.js +4 -320
- package/dist/evm/transactionParsing.d.ts +0 -11
- package/dist/evm/transactionParsing.js +28 -147
- package/dist/evm/utils.d.ts +0 -46
- package/dist/evm/utils.js +1 -57
- package/dist/helpers/index.d.ts +0 -4
- package/dist/helpers/index.js +8 -44
- package/dist/helpers/routeScan.js +0 -1
- package/dist/index.js +0 -1
- package/dist/old.js +0 -884
- package/dist/price.js +0 -1
- package/dist/price.types.js +0 -2
- package/dist/rate-limiter.d.ts +28 -0
- package/dist/rate-limiter.js +95 -0
- package/dist/retry-logic.d.ts +14 -0
- package/dist/retry-logic.js +120 -0
- package/dist/savings/index.js +0 -1
- package/dist/savings/saving-manager.d.ts +10 -11
- package/dist/savings/saving-manager.js +79 -22
- package/dist/savings/savings-operations.d.ts +39 -0
- package/dist/savings/savings-operations.js +141 -0
- package/dist/savings/smart-savings.d.ts +0 -63
- package/dist/savings/smart-savings.js +0 -78
- package/dist/savings/types.d.ts +0 -69
- package/dist/savings/types.js +0 -7
- package/dist/savings/validation.d.ts +9 -0
- package/dist/savings/validation.js +85 -0
- package/dist/svm/constant.js +0 -1
- package/dist/svm/index.js +0 -1
- package/dist/svm/svm.d.ts +11 -1
- package/dist/svm/svm.js +267 -27
- package/dist/svm/transactionParsing.d.ts +0 -7
- package/dist/svm/transactionParsing.js +3 -41
- package/dist/svm/transactionSender.js +0 -9
- package/dist/svm/utils.d.ts +0 -12
- package/dist/svm/utils.js +9 -60
- package/dist/test.d.ts +0 -4
- package/dist/test.js +6 -98
- package/dist/transaction-utils.d.ts +38 -0
- package/dist/transaction-utils.js +168 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +0 -1
- package/dist/utils.js +0 -1
- package/dist/vm-validation.d.ts +11 -0
- package/dist/vm-validation.js +151 -0
- package/dist/vm.d.ts +12 -2
- package/dist/vm.js +61 -16
- package/dist/walletBip32.js +15 -70
- package/package.json +9 -4
- package/test-discovery.ts +235 -0
- package/test-pocket-discovery.ts +84 -0
- package/tsconfig.json +18 -11
- package/tsconfig.prod.json +10 -0
- package/utils/evm/evm.ts +554 -8
- package/utils/rate-limiter.ts +179 -0
- package/utils/retry-logic.ts +271 -0
- package/utils/savings/EXAMPLES.md +883 -0
- package/utils/savings/SECURITY.md +731 -0
- package/utils/savings/saving-manager.ts +526 -16
- package/utils/savings/savings-operations.ts +509 -0
- package/utils/savings/validation.ts +187 -0
- package/utils/svm/svm.ts +476 -5
- package/utils/test.ts +2 -2
- package/utils/transaction-utils.ts +394 -0
- package/utils/types.ts +100 -0
- package/utils/vm-validation.ts +280 -0
- package/utils/vm.ts +197 -10
- package/utils/walletBip32.ts +39 -3
- package/dist/IChainWallet.js.map +0 -1
- package/dist/bip32.d.ts +0 -9
- package/dist/bip32.js +0 -172
- package/dist/bip32.js.map +0 -1
- package/dist/bip32Old.js.map +0 -1
- package/dist/bip32Small.js.map +0 -1
- package/dist/bipTest.js.map +0 -1
- package/dist/constant.js.map +0 -1
- package/dist/english.js.map +0 -1
- package/dist/evm/SMART_WALLET_EXAMPLES.d.ts +0 -20
- package/dist/evm/SMART_WALLET_EXAMPLES.js +0 -451
- package/dist/evm/SMART_WALLET_EXAMPLES.js.map +0 -1
- package/dist/evm/aa-service/index.js.map +0 -1
- package/dist/evm/aa-service/lib/account-adapter.js.map +0 -1
- package/dist/evm/aa-service/lib/kernel-account.js.map +0 -1
- package/dist/evm/aa-service/lib/kernel-modules.js.map +0 -1
- package/dist/evm/aa-service/lib/session-keys.js.map +0 -1
- package/dist/evm/aa-service/lib/type.js.map +0 -1
- package/dist/evm/aa-service/services/account-abstraction.js.map +0 -1
- package/dist/evm/aa-service/services/bundler.js.map +0 -1
- package/dist/evm/evm.js.map +0 -1
- package/dist/evm/index.js.map +0 -1
- package/dist/evm/script.js.map +0 -1
- package/dist/evm/smartWallet.js.map +0 -1
- package/dist/evm/smartWallet.types.js.map +0 -1
- package/dist/evm/transaction.utils.js.map +0 -1
- package/dist/evm/transactionParsing.js.map +0 -1
- package/dist/evm/utils.js.map +0 -1
- package/dist/helpers/index.js.map +0 -1
- package/dist/helpers/routeScan.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/old.js.map +0 -1
- package/dist/price.js.map +0 -1
- package/dist/price.types.js.map +0 -1
- package/dist/privacy/artifact-manager.d.ts +0 -117
- package/dist/privacy/artifact-manager.js +0 -251
- package/dist/privacy/artifact-manager.js.map +0 -1
- package/dist/privacy/broadcaster-client.d.ts +0 -166
- package/dist/privacy/broadcaster-client.js +0 -261
- package/dist/privacy/broadcaster-client.js.map +0 -1
- package/dist/privacy/index.d.ts +0 -34
- package/dist/privacy/index.js +0 -56
- package/dist/privacy/index.js.map +0 -1
- package/dist/privacy/network-config.d.ts +0 -57
- package/dist/privacy/network-config.js +0 -118
- package/dist/privacy/network-config.js.map +0 -1
- package/dist/privacy/poi-helper.d.ts +0 -161
- package/dist/privacy/poi-helper.js +0 -249
- package/dist/privacy/poi-helper.js.map +0 -1
- package/dist/privacy/railgun-engine.d.ts +0 -135
- package/dist/privacy/railgun-engine.js +0 -205
- package/dist/privacy/railgun-engine.js.map +0 -1
- package/dist/privacy/railgun-privacy-wallet.d.ts +0 -288
- package/dist/privacy/railgun-privacy-wallet.js +0 -539
- package/dist/privacy/railgun-privacy-wallet.js.map +0 -1
- package/dist/privacy/types.d.ts +0 -229
- package/dist/privacy/types.js +0 -26
- package/dist/privacy/types.js.map +0 -1
- package/dist/savings/index.js.map +0 -1
- package/dist/savings/saving-actions.d.ts +0 -0
- package/dist/savings/saving-actions.js +0 -78
- package/dist/savings/saving-actions.js.map +0 -1
- package/dist/savings/saving-manager.js.map +0 -1
- package/dist/savings/savings-manager.d.ts +0 -126
- package/dist/savings/savings-manager.js +0 -234
- package/dist/savings/savings-manager.js.map +0 -1
- package/dist/savings/smart-savings.js.map +0 -1
- package/dist/savings/types.js.map +0 -1
- package/dist/svm/constant.js.map +0 -1
- package/dist/svm/index.js.map +0 -1
- package/dist/svm/svm.js.map +0 -1
- package/dist/svm/transactionParsing.js.map +0 -1
- package/dist/svm/transactionSender.js.map +0 -1
- package/dist/svm/utils.js.map +0 -1
- package/dist/test.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/dist/vm.js.map +0 -1
- 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
|
+
}
|