@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.
@@ -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
+ }