@deserialize/multi-vm-wallet 1.5.11 → 1.5.21
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/MULTI_CHAIN_SAVINGS_PLAN.md +1028 -0
- package/MULTI_CHAIN_SAVINGS_USAGE.md +428 -0
- package/SAVINGS_TEST_EXAMPLES.md +411 -0
- package/dist/evm/evm.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +32 -2
- package/dist/savings/evm-savings.d.ts +52 -0
- package/dist/savings/evm-savings.js +159 -0
- package/dist/savings/index.d.ts +9 -1
- package/dist/savings/index.js +12 -1
- package/dist/savings/multi-chain-savings.d.ts +48 -0
- package/dist/savings/multi-chain-savings.js +203 -0
- package/dist/savings/savings-manager.d.ts +28 -0
- package/dist/savings/savings-manager.js +55 -0
- package/dist/savings/svm-savings.d.ts +40 -0
- package/dist/savings/svm-savings.js +176 -0
- package/dist/svm/index.d.ts +1 -0
- package/dist/svm/index.js +1 -0
- package/dist/test.js +111 -4
- package/package.json +1 -1
- package/utils/evm/evm.ts +1 -1
- package/utils/index.ts +12 -2
- package/utils/savings/evm-savings.ts +354 -0
- package/utils/savings/index.ts +18 -1
- package/utils/savings/multi-chain-savings.ts +458 -0
- package/utils/savings/savings-manager.ts +189 -0
- package/utils/savings/svm-savings.ts +394 -0
- package/utils/svm/index.ts +2 -1
- package/utils/test.ts +167 -8
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Chain Savings Manager
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates savings across multiple blockchain networks (EVM and Solana)
|
|
5
|
+
* Provides unified interface for managing savings pockets across chains
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EVMSavingsManager } from "./evm-savings";
|
|
9
|
+
import { SVMSavingsManager } from "./svm-savings";
|
|
10
|
+
import { ChainWalletConfig, Balance } from "../types";
|
|
11
|
+
import { Hex } from "viem";
|
|
12
|
+
import { PublicKey } from "@solana/web3.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Chain type identifier
|
|
16
|
+
*/
|
|
17
|
+
export type ChainType = 'EVM' | 'SVM';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Chain configuration for multi-chain manager
|
|
21
|
+
*/
|
|
22
|
+
export interface ChainConfig {
|
|
23
|
+
/** Unique identifier for the chain (e.g., "ethereum", "polygon", "solana") */
|
|
24
|
+
id: string;
|
|
25
|
+
/** Chain type (EVM or SVM) */
|
|
26
|
+
type: ChainType;
|
|
27
|
+
/** Chain-specific configuration */
|
|
28
|
+
config: ChainWalletConfig | { rpcUrl: string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pocket balance information for a specific chain
|
|
33
|
+
*/
|
|
34
|
+
export interface PocketBalance {
|
|
35
|
+
/** Chain identifier */
|
|
36
|
+
chainId: string;
|
|
37
|
+
/** Chain type */
|
|
38
|
+
chainType: ChainType;
|
|
39
|
+
/** Pocket index */
|
|
40
|
+
pocketIndex: number;
|
|
41
|
+
/** Pocket address (string format for compatibility) */
|
|
42
|
+
address: string;
|
|
43
|
+
/** Token balances */
|
|
44
|
+
balances: {
|
|
45
|
+
/** Token address or 'native' */
|
|
46
|
+
token: string;
|
|
47
|
+
/** Balance information */
|
|
48
|
+
balance: Balance;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Multi-Chain Savings Manager
|
|
54
|
+
*
|
|
55
|
+
* Manages savings pockets across multiple blockchain networks.
|
|
56
|
+
* - EVM chains (Ethereum, Polygon, BSC, etc.) share the same addresses (coin type 60)
|
|
57
|
+
* - Solana has different addresses (coin type 501)
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const manager = new MultiChainSavingsManager(
|
|
62
|
+
* mnemonic,
|
|
63
|
+
* [
|
|
64
|
+
* { id: 'ethereum', type: 'EVM', config: ethConfig },
|
|
65
|
+
* { id: 'polygon', type: 'EVM', config: polyConfig },
|
|
66
|
+
* { id: 'solana', type: 'SVM', config: { rpcUrl: '...' } }
|
|
67
|
+
* ],
|
|
68
|
+
* 0 // wallet index
|
|
69
|
+
* );
|
|
70
|
+
*
|
|
71
|
+
* // Get pocket address on Ethereum
|
|
72
|
+
* const ethAddress = manager.getPocketAddress('ethereum', 0);
|
|
73
|
+
*
|
|
74
|
+
* // Get pocket address on Polygon (same as Ethereum!)
|
|
75
|
+
* const polyAddress = manager.getPocketAddress('polygon', 0);
|
|
76
|
+
* console.log(ethAddress === polyAddress); // true
|
|
77
|
+
*
|
|
78
|
+
* // Get balances across all chains
|
|
79
|
+
* const balances = await manager.getPocketBalanceAcrossChains(0, tokensByChain);
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export class MultiChainSavingsManager {
|
|
83
|
+
private mnemonic: string;
|
|
84
|
+
private walletIndex: number;
|
|
85
|
+
|
|
86
|
+
// Separate managers by chain type
|
|
87
|
+
private evmManagers: Map<string, EVMSavingsManager> = new Map();
|
|
88
|
+
private svmManagers: Map<string, SVMSavingsManager> = new Map();
|
|
89
|
+
|
|
90
|
+
// Track chain configs
|
|
91
|
+
private chainConfigs: Map<string, ChainConfig> = new Map();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a new MultiChainSavingsManager
|
|
95
|
+
*
|
|
96
|
+
* @param mnemonic - BIP-39 mnemonic phrase
|
|
97
|
+
* @param chains - Array of chain configurations
|
|
98
|
+
* @param walletIndex - Wallet index in derivation path (default: 0)
|
|
99
|
+
*/
|
|
100
|
+
constructor(
|
|
101
|
+
mnemonic: string,
|
|
102
|
+
chains: ChainConfig[],
|
|
103
|
+
walletIndex: number = 0
|
|
104
|
+
) {
|
|
105
|
+
if (!mnemonic || typeof mnemonic !== 'string') {
|
|
106
|
+
throw new Error('Mnemonic must be a non-empty string');
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(chains) || chains.length === 0) {
|
|
109
|
+
throw new Error('Chains array must be non-empty');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.mnemonic = mnemonic;
|
|
113
|
+
this.walletIndex = walletIndex;
|
|
114
|
+
|
|
115
|
+
// Initialize managers for each chain
|
|
116
|
+
for (const chain of chains) {
|
|
117
|
+
this.addChain(chain);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Add a new chain to the manager
|
|
123
|
+
*
|
|
124
|
+
* @param chain - Chain configuration
|
|
125
|
+
*/
|
|
126
|
+
addChain(chain: ChainConfig): void {
|
|
127
|
+
if (!chain.id || !chain.type) {
|
|
128
|
+
throw new Error('Chain must have id and type');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (this.chainConfigs.has(chain.id)) {
|
|
132
|
+
throw new Error(`Chain with id '${chain.id}' already exists`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.chainConfigs.set(chain.id, chain);
|
|
136
|
+
|
|
137
|
+
if (chain.type === 'EVM') {
|
|
138
|
+
const manager = new EVMSavingsManager(
|
|
139
|
+
this.mnemonic,
|
|
140
|
+
chain.config as ChainWalletConfig,
|
|
141
|
+
this.walletIndex
|
|
142
|
+
);
|
|
143
|
+
this.evmManagers.set(chain.id, manager);
|
|
144
|
+
} else if (chain.type === 'SVM') {
|
|
145
|
+
const config = chain.config as { rpcUrl: string };
|
|
146
|
+
if (!config.rpcUrl) {
|
|
147
|
+
throw new Error(`SVM chain '${chain.id}' must have rpcUrl in config`);
|
|
148
|
+
}
|
|
149
|
+
const manager = new SVMSavingsManager(
|
|
150
|
+
this.mnemonic,
|
|
151
|
+
config.rpcUrl,
|
|
152
|
+
this.walletIndex
|
|
153
|
+
);
|
|
154
|
+
this.svmManagers.set(chain.id, manager);
|
|
155
|
+
} else {
|
|
156
|
+
throw new Error(`Unknown chain type: ${chain.type}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Remove a chain from the manager
|
|
162
|
+
*
|
|
163
|
+
* @param chainId - Chain identifier to remove
|
|
164
|
+
*/
|
|
165
|
+
removeChain(chainId: string): void {
|
|
166
|
+
if (this.evmManagers.has(chainId)) {
|
|
167
|
+
this.evmManagers.get(chainId)?.dispose();
|
|
168
|
+
this.evmManagers.delete(chainId);
|
|
169
|
+
}
|
|
170
|
+
if (this.svmManagers.has(chainId)) {
|
|
171
|
+
this.svmManagers.get(chainId)?.dispose();
|
|
172
|
+
this.svmManagers.delete(chainId);
|
|
173
|
+
}
|
|
174
|
+
this.chainConfigs.delete(chainId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get list of all chain IDs
|
|
179
|
+
*
|
|
180
|
+
* @returns Array of chain identifiers
|
|
181
|
+
*/
|
|
182
|
+
getChains(): string[] {
|
|
183
|
+
return Array.from(this.chainConfigs.keys());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get chain configuration
|
|
188
|
+
*
|
|
189
|
+
* @param chainId - Chain identifier
|
|
190
|
+
* @returns Chain configuration
|
|
191
|
+
*/
|
|
192
|
+
getChainConfig(chainId: string): ChainConfig {
|
|
193
|
+
const config = this.chainConfigs.get(chainId);
|
|
194
|
+
if (!config) {
|
|
195
|
+
throw new Error(`Chain not found: ${chainId}`);
|
|
196
|
+
}
|
|
197
|
+
return config;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get pocket address for a specific chain
|
|
202
|
+
*
|
|
203
|
+
* @param chainId - Chain identifier
|
|
204
|
+
* @param pocketIndex - Pocket index
|
|
205
|
+
* @returns Pocket address as string
|
|
206
|
+
*/
|
|
207
|
+
getPocketAddress(chainId: string, pocketIndex: number): string {
|
|
208
|
+
const chain = this.getChainConfig(chainId);
|
|
209
|
+
|
|
210
|
+
if (chain.type === 'EVM') {
|
|
211
|
+
const manager = this.evmManagers.get(chainId)!;
|
|
212
|
+
return manager.getPocket(pocketIndex).address;
|
|
213
|
+
} else {
|
|
214
|
+
const manager = this.svmManagers.get(chainId)!;
|
|
215
|
+
return manager.getPocket(pocketIndex).address.toBase58();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get main wallet address for a specific chain
|
|
221
|
+
*
|
|
222
|
+
* @param chainId - Chain identifier
|
|
223
|
+
* @returns Main wallet address as string
|
|
224
|
+
*/
|
|
225
|
+
getMainWalletAddress(chainId: string): string {
|
|
226
|
+
const chain = this.getChainConfig(chainId);
|
|
227
|
+
|
|
228
|
+
if (chain.type === 'EVM') {
|
|
229
|
+
const manager = this.evmManagers.get(chainId)!;
|
|
230
|
+
return manager.getMainWalletAddress();
|
|
231
|
+
} else {
|
|
232
|
+
const manager = this.svmManagers.get(chainId)!;
|
|
233
|
+
return manager.getMainWalletAddress().toBase58();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get pocket balance for a specific chain
|
|
239
|
+
*
|
|
240
|
+
* @param chainId - Chain identifier
|
|
241
|
+
* @param pocketIndex - Pocket index
|
|
242
|
+
* @param tokens - Array of token addresses to query
|
|
243
|
+
* @returns Pocket balance information
|
|
244
|
+
*/
|
|
245
|
+
async getPocketBalance(
|
|
246
|
+
chainId: string,
|
|
247
|
+
pocketIndex: number,
|
|
248
|
+
tokens: string[]
|
|
249
|
+
): Promise<PocketBalance> {
|
|
250
|
+
const chain = this.getChainConfig(chainId);
|
|
251
|
+
|
|
252
|
+
if (chain.type === 'EVM') {
|
|
253
|
+
const manager = this.evmManagers.get(chainId)!;
|
|
254
|
+
const balances = await manager.getPocketBalance(pocketIndex, tokens);
|
|
255
|
+
const pocket = manager.getPocket(pocketIndex);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
chainId,
|
|
259
|
+
chainType: 'EVM',
|
|
260
|
+
pocketIndex,
|
|
261
|
+
address: pocket.address,
|
|
262
|
+
balances: balances.map(b => ({
|
|
263
|
+
token: b.address === 'native' ? 'native' : b.address,
|
|
264
|
+
balance: b.balance
|
|
265
|
+
}))
|
|
266
|
+
};
|
|
267
|
+
} else {
|
|
268
|
+
const manager = this.svmManagers.get(chainId)!;
|
|
269
|
+
const balances = await manager.getPocketBalance(pocketIndex, tokens);
|
|
270
|
+
const pocket = manager.getPocket(pocketIndex);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
chainId,
|
|
274
|
+
chainType: 'SVM',
|
|
275
|
+
pocketIndex,
|
|
276
|
+
address: pocket.address.toBase58(),
|
|
277
|
+
balances: balances.map(b => ({
|
|
278
|
+
token: b.address === 'native' ? 'native' : b.address.toBase58(),
|
|
279
|
+
balance: b.balance
|
|
280
|
+
}))
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get pocket balance across multiple chains
|
|
287
|
+
*
|
|
288
|
+
* @param pocketIndex - Pocket index
|
|
289
|
+
* @param tokensByChain - Map of chain IDs to token addresses
|
|
290
|
+
* @returns Array of pocket balances per chain
|
|
291
|
+
*/
|
|
292
|
+
async getPocketBalanceAcrossChains(
|
|
293
|
+
pocketIndex: number,
|
|
294
|
+
tokensByChain: Map<string, string[]>
|
|
295
|
+
): Promise<PocketBalance[]> {
|
|
296
|
+
const promises: Promise<PocketBalance>[] = [];
|
|
297
|
+
|
|
298
|
+
for (const [chainId, tokens] of tokensByChain) {
|
|
299
|
+
promises.push(this.getPocketBalance(chainId, pocketIndex, tokens));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return await Promise.all(promises);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get balances for multiple pockets across multiple chains
|
|
307
|
+
*
|
|
308
|
+
* @param pocketIndices - Array of pocket indices
|
|
309
|
+
* @param tokensByChain - Map of chain IDs to token addresses
|
|
310
|
+
* @returns Map of pocket indices to their balances across chains
|
|
311
|
+
*/
|
|
312
|
+
async getAllPocketsBalanceAcrossChains(
|
|
313
|
+
pocketIndices: number[],
|
|
314
|
+
tokensByChain: Map<string, string[]>
|
|
315
|
+
): Promise<Map<number, PocketBalance[]>> {
|
|
316
|
+
const results = new Map<number, PocketBalance[]>();
|
|
317
|
+
|
|
318
|
+
// Fetch all pockets in parallel
|
|
319
|
+
await Promise.all(
|
|
320
|
+
pocketIndices.map(async (pocketIndex) => {
|
|
321
|
+
const balances = await this.getPocketBalanceAcrossChains(
|
|
322
|
+
pocketIndex,
|
|
323
|
+
tokensByChain
|
|
324
|
+
);
|
|
325
|
+
results.set(pocketIndex, balances);
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
return results;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get EVM manager for a chain (for advanced operations)
|
|
334
|
+
*
|
|
335
|
+
* @param chainId - Chain identifier
|
|
336
|
+
* @returns EVMSavingsManager instance
|
|
337
|
+
* @throws Error if chain is not EVM or not found
|
|
338
|
+
*/
|
|
339
|
+
getEVMManager(chainId: string): EVMSavingsManager {
|
|
340
|
+
const manager = this.evmManagers.get(chainId);
|
|
341
|
+
if (!manager) {
|
|
342
|
+
throw new Error(`EVM chain not found: ${chainId}`);
|
|
343
|
+
}
|
|
344
|
+
return manager;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get SVM manager for a chain (for advanced operations)
|
|
349
|
+
*
|
|
350
|
+
* @param chainId - Chain identifier
|
|
351
|
+
* @returns SVMSavingsManager instance
|
|
352
|
+
* @throws Error if chain is not SVM or not found
|
|
353
|
+
*/
|
|
354
|
+
getSVMManager(chainId: string): SVMSavingsManager {
|
|
355
|
+
const manager = this.svmManagers.get(chainId);
|
|
356
|
+
if (!manager) {
|
|
357
|
+
throw new Error(`SVM chain not found: ${chainId}`);
|
|
358
|
+
}
|
|
359
|
+
return manager;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Check if a chain is EVM-compatible
|
|
364
|
+
*
|
|
365
|
+
* @param chainId - Chain identifier
|
|
366
|
+
* @returns true if chain is EVM
|
|
367
|
+
*/
|
|
368
|
+
isEVMChain(chainId: string): boolean {
|
|
369
|
+
return this.evmManagers.has(chainId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check if a chain is Solana (SVM)
|
|
374
|
+
*
|
|
375
|
+
* @param chainId - Chain identifier
|
|
376
|
+
* @returns true if chain is SVM
|
|
377
|
+
*/
|
|
378
|
+
isSVMChain(chainId: string): boolean {
|
|
379
|
+
return this.svmManagers.has(chainId);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get all EVM chain IDs
|
|
384
|
+
*
|
|
385
|
+
* @returns Array of EVM chain identifiers
|
|
386
|
+
*/
|
|
387
|
+
getEVMChains(): string[] {
|
|
388
|
+
return Array.from(this.evmManagers.keys());
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get all SVM chain IDs
|
|
393
|
+
*
|
|
394
|
+
* @returns Array of SVM chain identifiers
|
|
395
|
+
*/
|
|
396
|
+
getSVMChains(): string[] {
|
|
397
|
+
return Array.from(this.svmManagers.keys());
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Clear a specific pocket across all chains
|
|
402
|
+
*
|
|
403
|
+
* @param pocketIndex - Pocket index to clear
|
|
404
|
+
*/
|
|
405
|
+
clearPocket(pocketIndex: number): void {
|
|
406
|
+
for (const manager of this.evmManagers.values()) {
|
|
407
|
+
manager.clearPocket(pocketIndex);
|
|
408
|
+
}
|
|
409
|
+
for (const manager of this.svmManagers.values()) {
|
|
410
|
+
manager.clearPocket(pocketIndex);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Clear all pockets across all chains
|
|
416
|
+
*/
|
|
417
|
+
clearAllPockets(): void {
|
|
418
|
+
for (const manager of this.evmManagers.values()) {
|
|
419
|
+
manager.clearAllPockets();
|
|
420
|
+
}
|
|
421
|
+
for (const manager of this.svmManagers.values()) {
|
|
422
|
+
manager.clearAllPockets();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Clear all RPC clients across all chains
|
|
428
|
+
*/
|
|
429
|
+
clearAllClients(): void {
|
|
430
|
+
for (const manager of this.evmManagers.values()) {
|
|
431
|
+
manager.clearClient();
|
|
432
|
+
}
|
|
433
|
+
for (const manager of this.svmManagers.values()) {
|
|
434
|
+
manager.clearClient();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Dispose all managers and clear all sensitive data
|
|
440
|
+
*
|
|
441
|
+
* @remarks
|
|
442
|
+
* After calling dispose(), this manager instance should not be used.
|
|
443
|
+
*/
|
|
444
|
+
dispose(): void {
|
|
445
|
+
for (const manager of this.evmManagers.values()) {
|
|
446
|
+
manager.dispose();
|
|
447
|
+
}
|
|
448
|
+
for (const manager of this.svmManagers.values()) {
|
|
449
|
+
manager.dispose();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.evmManagers.clear();
|
|
453
|
+
this.svmManagers.clear();
|
|
454
|
+
this.chainConfigs.clear();
|
|
455
|
+
|
|
456
|
+
(this as any).mnemonic = '';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract Base Savings Manager
|
|
3
|
+
*
|
|
4
|
+
* Base class for managing multi-pocket savings accounts across different chains.
|
|
5
|
+
* Similar to the VM<AddressType, PrivateKeyType, ConnectionType> pattern.
|
|
6
|
+
*
|
|
7
|
+
* @template AddressType - Address format (Hex for EVM, PublicKey for SVM)
|
|
8
|
+
* @template ClientType - RPC client type (PublicClient for EVM, Connection for SVM)
|
|
9
|
+
* @template WalletClientType - Wallet client type (WalletClient for EVM, Keypair for SVM)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { SavingsValidation } from "./validation";
|
|
13
|
+
|
|
14
|
+
export interface Pocket<AddressType> {
|
|
15
|
+
privateKey: any;
|
|
16
|
+
address: AddressType;
|
|
17
|
+
derivationPath: string;
|
|
18
|
+
index: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Abstract base class for chain-specific savings managers
|
|
23
|
+
*
|
|
24
|
+
* Follows the same pattern as VM class:
|
|
25
|
+
* - Abstract base with generic types
|
|
26
|
+
* - Concrete implementations for each chain type (EVM, SVM)
|
|
27
|
+
* - Shared memory management and disposal patterns
|
|
28
|
+
*/
|
|
29
|
+
export abstract class SavingsManager<
|
|
30
|
+
AddressType,
|
|
31
|
+
ClientType,
|
|
32
|
+
WalletClientType
|
|
33
|
+
> {
|
|
34
|
+
protected mnemonic: string;
|
|
35
|
+
protected walletIndex: number;
|
|
36
|
+
protected disposed: boolean = false;
|
|
37
|
+
|
|
38
|
+
// Abstract properties (must be implemented by subclasses)
|
|
39
|
+
abstract coinType: number;
|
|
40
|
+
abstract derivationPathBase: string;
|
|
41
|
+
|
|
42
|
+
// Pocket cache: Map<pocketIndex, Pocket>
|
|
43
|
+
protected pockets: Map<number, Pocket<AddressType>> = new Map();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a new SavingsManager
|
|
47
|
+
*
|
|
48
|
+
* @param mnemonic - BIP-39 mnemonic phrase
|
|
49
|
+
* @param walletIndex - Wallet index in derivation path (default: 0)
|
|
50
|
+
*/
|
|
51
|
+
constructor(mnemonic: string, walletIndex: number = 0) {
|
|
52
|
+
SavingsValidation.validateMnemonic(mnemonic);
|
|
53
|
+
SavingsValidation.validateWalletIndex(walletIndex);
|
|
54
|
+
|
|
55
|
+
this.mnemonic = mnemonic;
|
|
56
|
+
this.walletIndex = walletIndex;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Abstract methods (must be implemented by subclasses)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive a savings pocket at the specified account index
|
|
63
|
+
*
|
|
64
|
+
* @param accountIndex - Account index for the pocket (0-based)
|
|
65
|
+
* @returns Pocket object with privateKey, address, derivationPath, and index
|
|
66
|
+
*/
|
|
67
|
+
abstract derivePocket(accountIndex: number): Pocket<AddressType>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the main wallet credentials
|
|
71
|
+
*
|
|
72
|
+
* @returns Main wallet object with privateKey, address, and derivationPath
|
|
73
|
+
*/
|
|
74
|
+
abstract getMainWallet(): {
|
|
75
|
+
privateKey: any;
|
|
76
|
+
address: AddressType;
|
|
77
|
+
derivationPath: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create an RPC client for this chain
|
|
82
|
+
*
|
|
83
|
+
* @param rpcUrl - RPC endpoint URL
|
|
84
|
+
* @returns Chain-specific RPC client
|
|
85
|
+
*/
|
|
86
|
+
abstract createClient(rpcUrl: string): ClientType;
|
|
87
|
+
|
|
88
|
+
// Shared methods (implemented in base class)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get or create a savings pocket at the specified index
|
|
92
|
+
*
|
|
93
|
+
* @param accountIndex - The pocket index (0-based)
|
|
94
|
+
* @returns Pocket object
|
|
95
|
+
* @throws Error if validation fails or VM is disposed
|
|
96
|
+
*/
|
|
97
|
+
getPocket(accountIndex: number): Pocket<AddressType> {
|
|
98
|
+
this.checkNotDisposed();
|
|
99
|
+
SavingsValidation.validateAccountIndex(accountIndex);
|
|
100
|
+
|
|
101
|
+
if (!this.pockets.has(accountIndex)) {
|
|
102
|
+
return this.derivePocket(accountIndex);
|
|
103
|
+
}
|
|
104
|
+
return this.pockets.get(accountIndex)!;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear a specific pocket's cached private key from memory
|
|
109
|
+
*
|
|
110
|
+
* @param accountIndex - Index of the pocket to clear
|
|
111
|
+
*/
|
|
112
|
+
clearPocket(accountIndex: number): void {
|
|
113
|
+
SavingsValidation.validateAccountIndex(accountIndex);
|
|
114
|
+
|
|
115
|
+
if (this.pockets.has(accountIndex)) {
|
|
116
|
+
const pocket = this.pockets.get(accountIndex)!;
|
|
117
|
+
|
|
118
|
+
// Attempt to clear the private key
|
|
119
|
+
// Note: JavaScript strings are immutable, so this only clears our reference
|
|
120
|
+
(pocket as any).privateKey = '';
|
|
121
|
+
|
|
122
|
+
// Remove from cache
|
|
123
|
+
this.pockets.delete(accountIndex);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clear all cached pocket private keys from memory
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* Call this method when:
|
|
132
|
+
* - User locks the wallet
|
|
133
|
+
* - Application goes to background (mobile)
|
|
134
|
+
* - Extension popup closes (browser extension)
|
|
135
|
+
* - Session ends
|
|
136
|
+
*/
|
|
137
|
+
clearAllPockets(): void {
|
|
138
|
+
for (const [_, pocket] of this.pockets.entries()) {
|
|
139
|
+
// Attempt to clear the private key
|
|
140
|
+
(pocket as any).privateKey = '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Clear the map
|
|
144
|
+
this.pockets.clear();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Clear all sensitive data from memory
|
|
149
|
+
*
|
|
150
|
+
* @remarks
|
|
151
|
+
* IMPORTANT: After calling dispose(), the manager instance should not be used.
|
|
152
|
+
* JavaScript strings are immutable, so this method can only clear references.
|
|
153
|
+
* The actual memory will be cleared by garbage collection.
|
|
154
|
+
*/
|
|
155
|
+
dispose(): void {
|
|
156
|
+
if (this.disposed) {
|
|
157
|
+
return; // Already disposed
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Clear all cached pockets
|
|
161
|
+
this.clearAllPockets();
|
|
162
|
+
|
|
163
|
+
// Attempt to clear mnemonic
|
|
164
|
+
(this as any).mnemonic = '';
|
|
165
|
+
|
|
166
|
+
this.disposed = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if manager has been disposed
|
|
171
|
+
*
|
|
172
|
+
* @returns true if dispose() has been called
|
|
173
|
+
*/
|
|
174
|
+
isDisposed(): boolean {
|
|
175
|
+
return this.disposed || !this.mnemonic || this.mnemonic === '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Throw error if manager has been disposed
|
|
180
|
+
*
|
|
181
|
+
* @throws Error if manager is disposed
|
|
182
|
+
* @protected
|
|
183
|
+
*/
|
|
184
|
+
protected checkNotDisposed(): void {
|
|
185
|
+
if (this.isDisposed()) {
|
|
186
|
+
throw new Error('SavingsManager has been disposed. Create a new instance to perform operations.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|