@deserialize/multi-vm-wallet 1.5.1 → 1.5.2

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,1028 @@
1
+ # Multi-Chain Savings Manager - Implementation Plan
2
+
3
+ **Created**: 2026-02-02
4
+ **Status**: 📋 Planning Phase
5
+ **Pattern**: Following EVM/SVM VM architecture
6
+
7
+ ---
8
+
9
+ ## 🎯 Architecture Pattern (Based on VM Structure)
10
+
11
+ ### Current VM Pattern
12
+ ```typescript
13
+ // Base abstract VM class
14
+ abstract class VM<AddressType, PrivateKeyType, ConnectionType> { }
15
+
16
+ // EVM implementation
17
+ class EVMVM extends VM<string, string, PublicClient> {
18
+ derivationPath = "m/44'/60'/0'/0/";
19
+ }
20
+
21
+ // SVM implementation
22
+ class SVMVM extends VM<PublicKey, Keypair, Connection> {
23
+ derivationPath = "m/44'/501'/";
24
+ }
25
+ ```
26
+
27
+ ### New Savings Pattern (Same Structure)
28
+ ```typescript
29
+ // Base abstract savings manager
30
+ abstract class SavingsManager<AddressType, ClientType, WalletClientType> { }
31
+
32
+ // EVM savings implementation
33
+ class EVMSavingsManager extends SavingsManager<Hex, PublicClient, WalletClient> {
34
+ coinType = 60;
35
+ }
36
+
37
+ // SVM savings implementation
38
+ class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Keypair> {
39
+ coinType = 501;
40
+ }
41
+
42
+ // Multi-chain orchestrator
43
+ class MultiChainSavingsManager {
44
+ private evmManagers: Map<string, EVMSavingsManager>;
45
+ private svmManagers: Map<string, SVMSavingsManager>;
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 📁 File Structure (Following EVM/SVM Pattern)
52
+
53
+ ```
54
+ utils/savings/
55
+ ├── index.ts // Export all savings modules
56
+ ├── types.ts // Shared types (existing, extend)
57
+ ├── validation.ts // Validation utilities (existing, extend)
58
+
59
+ ├── savings-manager.ts // Base abstract SavingsManager class
60
+ ├── evm-savings.ts // EVMSavingsManager implementation
61
+ ├── svm-savings.ts // SVMSavingsManager implementation
62
+ ├── multi-chain-savings.ts // MultiChainSavingsManager orchestrator
63
+
64
+ └── utils/
65
+ ├── chain-config.ts // Chain configuration helpers
66
+ └── balance-aggregator.ts // Balance aggregation utilities
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 🏗️ Implementation Plan
72
+
73
+ ### Phase 1: Base Abstract Class ⚡
74
+
75
+ **File**: `utils/savings/savings-manager.ts`
76
+
77
+ ```typescript
78
+ import { Balance, TransactionResult } from "../types";
79
+
80
+ /**
81
+ * Abstract base class for savings managers (similar to VM)
82
+ *
83
+ * @template AddressType - Address format (Hex for EVM, PublicKey for SVM)
84
+ * @template ClientType - RPC client type (PublicClient for EVM, Connection for SVM)
85
+ * @template WalletClientType - Wallet client type (WalletClient for EVM, Keypair for SVM)
86
+ */
87
+ export abstract class SavingsManager<
88
+ AddressType,
89
+ ClientType,
90
+ WalletClientType
91
+ > {
92
+ protected mnemonic: string;
93
+ protected walletIndex: number;
94
+ protected disposed: boolean = false;
95
+
96
+ // Abstract properties (implemented by subclasses)
97
+ abstract coinType: number;
98
+ abstract derivationPathBase: string;
99
+
100
+ // Pocket cache: Map<pocketIndex, Pocket>
101
+ protected pockets: Map<number, {
102
+ privateKey: any;
103
+ address: AddressType;
104
+ derivationPath: string;
105
+ index: number;
106
+ }> = new Map();
107
+
108
+ constructor(mnemonic: string, walletIndex: number = 0) {
109
+ this.mnemonic = mnemonic;
110
+ this.walletIndex = walletIndex;
111
+ }
112
+
113
+ // Abstract methods (must be implemented by subclasses)
114
+ abstract derivePocket(accountIndex: number): {
115
+ privateKey: any;
116
+ address: AddressType;
117
+ derivationPath: string;
118
+ index: number;
119
+ };
120
+
121
+ abstract getMainWallet(): {
122
+ privateKey: any;
123
+ address: AddressType;
124
+ derivationPath: string;
125
+ };
126
+
127
+ abstract createClient(rpcUrl: string): ClientType;
128
+
129
+ // Shared methods (implemented in base class)
130
+ getPocket(accountIndex: number) {
131
+ if (!this.pockets.has(accountIndex)) {
132
+ return this.derivePocket(accountIndex);
133
+ }
134
+ return this.pockets.get(accountIndex)!;
135
+ }
136
+
137
+ clearPocket(accountIndex: number): void {
138
+ if (this.pockets.has(accountIndex)) {
139
+ const pocket = this.pockets.get(accountIndex)!;
140
+ (pocket as any).privateKey = '';
141
+ this.pockets.delete(accountIndex);
142
+ }
143
+ }
144
+
145
+ clearAllPockets(): void {
146
+ for (const [_, pocket] of this.pockets.entries()) {
147
+ (pocket as any).privateKey = '';
148
+ }
149
+ this.pockets.clear();
150
+ }
151
+
152
+ dispose(): void {
153
+ if (this.disposed) return;
154
+ this.clearAllPockets();
155
+ (this as any).mnemonic = '';
156
+ this.disposed = true;
157
+ }
158
+
159
+ isDisposed(): boolean {
160
+ return this.disposed || !this.mnemonic;
161
+ }
162
+
163
+ protected checkNotDisposed(): void {
164
+ if (this.isDisposed()) {
165
+ throw new Error('SavingsManager has been disposed');
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ ---
172
+
173
+ ### Phase 2: EVM Implementation ⚡
174
+
175
+ **File**: `utils/savings/evm-savings.ts`
176
+
177
+ ```typescript
178
+ import { SavingsManager } from "./savings-manager";
179
+ import { Hex, PublicClient, WalletClient, createPublicClient, createWalletClient, http } from "viem";
180
+ import { EVMDeriveChildPrivateKey, mnemonicToSeed } from "../walletBip32";
181
+ import { ChainWalletConfig, Balance, TransactionResult } from "../types";
182
+ import { ethers } from "ethers";
183
+ import {
184
+ fromChainToViemChain,
185
+ getNativeBalance,
186
+ getTokenBalance,
187
+ sendNativeToken,
188
+ sendERC20Token
189
+ } from "../evm";
190
+ import { SavingsValidation } from "./validation";
191
+ import { privateKeyToAccount } from "viem/accounts";
192
+
193
+ /**
194
+ * EVM Savings Manager
195
+ *
196
+ * Manages savings pockets across EVM-compatible chains (Ethereum, Polygon, BSC, etc.)
197
+ * All EVM chains use the same addresses (coin type 60)
198
+ */
199
+ export class EVMSavingsManager extends SavingsManager<Hex, PublicClient, WalletClient> {
200
+ coinType = 60;
201
+ derivationPathBase = "m/44'/60'/";
202
+
203
+ private chain: ChainWalletConfig;
204
+ private _client?: PublicClient;
205
+
206
+ constructor(
207
+ mnemonic: string,
208
+ chain: ChainWalletConfig,
209
+ walletIndex: number = 0
210
+ ) {
211
+ super(mnemonic, walletIndex);
212
+
213
+ SavingsValidation.validateMnemonic(mnemonic);
214
+ SavingsValidation.validateWalletIndex(walletIndex);
215
+ SavingsValidation.validateChainId(chain.chainId);
216
+
217
+ this.chain = chain;
218
+ }
219
+
220
+ // Lazy client creation (like VM pattern)
221
+ get client(): PublicClient {
222
+ if (!this._client) {
223
+ this._client = this.createClient(this.chain.rpcUrl);
224
+ }
225
+ return this._client;
226
+ }
227
+
228
+ createClient(rpcUrl: string): PublicClient {
229
+ return createPublicClient({
230
+ chain: fromChainToViemChain(this.chain),
231
+ transport: http(rpcUrl)
232
+ });
233
+ }
234
+
235
+ clearClient(): void {
236
+ this._client = undefined;
237
+ }
238
+
239
+ derivePocket(accountIndex: number) {
240
+ this.checkNotDisposed();
241
+ SavingsValidation.validateAccountIndex(accountIndex);
242
+
243
+ const pocketIndex = accountIndex + 1; // Preserve index 0 for main wallet
244
+ const derivationPath = `${this.derivationPathBase}${pocketIndex}'/0/${this.walletIndex}`;
245
+ const seed = mnemonicToSeed(this.mnemonic);
246
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
247
+ const wallet = new ethers.Wallet(privateKey);
248
+
249
+ const pocket = {
250
+ privateKey,
251
+ address: wallet.address as Hex,
252
+ derivationPath,
253
+ index: pocketIndex
254
+ };
255
+
256
+ this.pockets.set(accountIndex, pocket);
257
+ return pocket;
258
+ }
259
+
260
+ getMainWallet() {
261
+ this.checkNotDisposed();
262
+ const derivationPath = `${this.derivationPathBase}0'/0/${this.walletIndex}`;
263
+ const seed = mnemonicToSeed(this.mnemonic);
264
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
265
+ const wallet = new ethers.Wallet(privateKey);
266
+
267
+ return {
268
+ privateKey,
269
+ address: wallet.address as Hex,
270
+ derivationPath
271
+ };
272
+ }
273
+
274
+ getMainWalletAddress(): Hex {
275
+ return this.getMainWallet().address;
276
+ }
277
+
278
+ // Balance operations
279
+ async getPocketBalance(pocketIndex: number, tokens: string[]): Promise<{
280
+ address: Hex | 'native';
281
+ balance: Balance;
282
+ }[]> {
283
+ SavingsValidation.validateAccountIndex(pocketIndex);
284
+
285
+ const pocket = this.getPocket(pocketIndex);
286
+ const balances: { address: Hex | 'native'; balance: Balance }[] = [];
287
+
288
+ // Get native balance
289
+ const nativeBalance = await getNativeBalance(pocket.address, this.client);
290
+ balances.push({ address: 'native', balance: nativeBalance });
291
+
292
+ // Get token balances
293
+ await Promise.all(tokens.map(async (token) => {
294
+ SavingsValidation.validateAddress(token, 'Token address');
295
+ const tokenBalance = await getTokenBalance(token as Hex, pocket.address, this.client);
296
+ balances.push({ address: token as Hex, balance: tokenBalance });
297
+ }));
298
+
299
+ return balances;
300
+ }
301
+
302
+ // Transfer operations
303
+ async transferToPocket(
304
+ mainWallet: WalletClient,
305
+ pocketIndex: number,
306
+ amount: string
307
+ ): Promise<TransactionResult> {
308
+ SavingsValidation.validateAccountIndex(pocketIndex);
309
+ SavingsValidation.validateAmountString(amount, 'Transfer amount');
310
+
311
+ const pocket = this.getPocket(pocketIndex);
312
+ return await sendNativeToken(mainWallet, this.client, pocket.address, amount, 5);
313
+ }
314
+
315
+ async transferTokenToPocket(
316
+ mainWallet: WalletClient,
317
+ tokenAddress: string,
318
+ pocketIndex: number,
319
+ amount: bigint
320
+ ): Promise<TransactionResult> {
321
+ SavingsValidation.validateAddress(tokenAddress, 'Token address');
322
+ SavingsValidation.validateAccountIndex(pocketIndex);
323
+ SavingsValidation.validateAmount(amount, 'Transfer amount');
324
+
325
+ const pocket = this.getPocket(pocketIndex);
326
+ return await sendERC20Token(mainWallet, this.client, tokenAddress as Hex, pocket.address, amount, 5);
327
+ }
328
+
329
+ async sendToMainWallet(
330
+ pocketIndex: number,
331
+ amount: bigint,
332
+ token: Hex | "native"
333
+ ): Promise<TransactionResult> {
334
+ SavingsValidation.validateAccountIndex(pocketIndex);
335
+
336
+ const pocket = this.getPocket(pocketIndex);
337
+ const account = privateKeyToAccount(`0x${pocket.privateKey}`);
338
+ const mainWalletAddress = this.getMainWalletAddress();
339
+
340
+ const walletClient = createWalletClient({
341
+ account,
342
+ transport: http(this.chain.rpcUrl),
343
+ chain: fromChainToViemChain(this.chain)
344
+ });
345
+
346
+ if (token === "native") {
347
+ return await sendNativeToken(walletClient, this.client, mainWalletAddress, amount);
348
+ }
349
+
350
+ return await sendERC20Token(walletClient, this.client, token, mainWalletAddress, amount);
351
+ }
352
+
353
+ dispose(): void {
354
+ super.dispose();
355
+ this.clearClient();
356
+ }
357
+ }
358
+ ```
359
+
360
+ ---
361
+
362
+ ### Phase 3: SVM (Solana) Implementation ⚡
363
+
364
+ **File**: `utils/savings/svm-savings.ts`
365
+
366
+ ```typescript
367
+ import { SavingsManager } from "./savings-manager";
368
+ import { Connection, PublicKey, Keypair } from "@solana/web3.js";
369
+ import { SVMDeriveChildPrivateKey, mnemonicToSeed } from "../walletBip32";
370
+ import { Balance, TransactionResult } from "../types";
371
+ import {
372
+ getSvmNativeBalance,
373
+ getTokenBalance as getSvmTokenBalance,
374
+ signAndSendTransaction,
375
+ getTransferNativeTransaction,
376
+ getTransferTokenTransaction
377
+ } from "../svm";
378
+ import { SavingsValidation } from "./validation";
379
+ import BN from "bn.js";
380
+
381
+ /**
382
+ * Solana (SVM) Savings Manager
383
+ *
384
+ * Manages savings pockets on Solana
385
+ * Uses coin type 501 (different addresses than EVM)
386
+ */
387
+ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Keypair> {
388
+ coinType = 501;
389
+ derivationPathBase = "m/44'/501'/";
390
+
391
+ private rpcUrl: string;
392
+ private _client?: Connection;
393
+
394
+ constructor(
395
+ mnemonic: string,
396
+ rpcUrl: string,
397
+ walletIndex: number = 0
398
+ ) {
399
+ super(mnemonic, walletIndex);
400
+
401
+ SavingsValidation.validateMnemonic(mnemonic);
402
+ SavingsValidation.validateWalletIndex(walletIndex);
403
+
404
+ this.rpcUrl = rpcUrl;
405
+ }
406
+
407
+ // Lazy client creation
408
+ get client(): Connection {
409
+ if (!this._client) {
410
+ this._client = this.createClient(this.rpcUrl);
411
+ }
412
+ return this._client;
413
+ }
414
+
415
+ createClient(rpcUrl: string): Connection {
416
+ return new Connection(rpcUrl, 'confirmed');
417
+ }
418
+
419
+ clearClient(): void {
420
+ this._client = undefined;
421
+ }
422
+
423
+ derivePocket(accountIndex: number) {
424
+ this.checkNotDisposed();
425
+ SavingsValidation.validateAccountIndex(accountIndex);
426
+
427
+ const pocketIndex = accountIndex + 1;
428
+ const derivationPath = `${this.derivationPathBase}${pocketIndex}'/0/${this.walletIndex}`;
429
+ const seed = mnemonicToSeed(this.mnemonic);
430
+ const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
431
+
432
+ const pocket = {
433
+ privateKey: keypair,
434
+ address: keypair.publicKey,
435
+ derivationPath,
436
+ index: pocketIndex
437
+ };
438
+
439
+ this.pockets.set(accountIndex, pocket);
440
+ return pocket;
441
+ }
442
+
443
+ getMainWallet() {
444
+ this.checkNotDisposed();
445
+ const derivationPath = `${this.derivationPathBase}0'/0/${this.walletIndex}`;
446
+ const seed = mnemonicToSeed(this.mnemonic);
447
+ const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
448
+
449
+ return {
450
+ privateKey: keypair,
451
+ address: keypair.publicKey,
452
+ derivationPath
453
+ };
454
+ }
455
+
456
+ getMainWalletAddress(): PublicKey {
457
+ return this.getMainWallet().address;
458
+ }
459
+
460
+ // Balance operations
461
+ async getPocketBalance(pocketIndex: number, tokens: string[]): Promise<{
462
+ address: PublicKey | 'native';
463
+ balance: Balance;
464
+ }[]> {
465
+ SavingsValidation.validateAccountIndex(pocketIndex);
466
+
467
+ const pocket = this.getPocket(pocketIndex);
468
+ const balances: { address: PublicKey | 'native'; balance: Balance }[] = [];
469
+
470
+ // Get native SOL balance
471
+ const nativeBalance = await getSvmNativeBalance(pocket.address, this.client);
472
+ balances.push({ address: 'native', balance: nativeBalance });
473
+
474
+ // Get SPL token balances
475
+ await Promise.all(tokens.map(async (token) => {
476
+ const tokenPubkey = new PublicKey(token);
477
+ const tokenBalanceData = await getSvmTokenBalance(pocket.address, tokenPubkey, this.client);
478
+ const balance: Balance = {
479
+ balance: new BN(tokenBalanceData.amount),
480
+ formatted: tokenBalanceData.uiAmount || 0,
481
+ decimal: tokenBalanceData.decimals
482
+ };
483
+ balances.push({ address: tokenPubkey, balance });
484
+ }));
485
+
486
+ return balances;
487
+ }
488
+
489
+ // Transfer operations
490
+ async transferToPocket(
491
+ mainWallet: Keypair,
492
+ pocketIndex: number,
493
+ amount: bigint
494
+ ): Promise<TransactionResult> {
495
+ SavingsValidation.validateAccountIndex(pocketIndex);
496
+
497
+ const pocket = this.getPocket(pocketIndex);
498
+ const tx = await getTransferNativeTransaction(
499
+ mainWallet.publicKey,
500
+ pocket.address,
501
+ amount,
502
+ this.client
503
+ );
504
+
505
+ return await signAndSendTransaction(tx, [mainWallet], this.client);
506
+ }
507
+
508
+ async transferTokenToPocket(
509
+ mainWallet: Keypair,
510
+ tokenMint: string,
511
+ pocketIndex: number,
512
+ amount: bigint
513
+ ): Promise<TransactionResult> {
514
+ SavingsValidation.validateAccountIndex(pocketIndex);
515
+
516
+ const pocket = this.getPocket(pocketIndex);
517
+ const tx = await getTransferTokenTransaction(
518
+ mainWallet.publicKey,
519
+ pocket.address,
520
+ new PublicKey(tokenMint),
521
+ amount,
522
+ this.client
523
+ );
524
+
525
+ return await signAndSendTransaction(tx, [mainWallet], this.client);
526
+ }
527
+
528
+ async sendToMainWallet(
529
+ pocketIndex: number,
530
+ amount: bigint,
531
+ token: PublicKey | "native"
532
+ ): Promise<TransactionResult> {
533
+ SavingsValidation.validateAccountIndex(pocketIndex);
534
+
535
+ const pocket = this.getPocket(pocketIndex);
536
+ const mainWalletAddress = this.getMainWalletAddress();
537
+
538
+ if (token === "native") {
539
+ const tx = await getTransferNativeTransaction(
540
+ pocket.address,
541
+ mainWalletAddress,
542
+ amount,
543
+ this.client
544
+ );
545
+ return await signAndSendTransaction(tx, [pocket.privateKey], this.client);
546
+ }
547
+
548
+ const tx = await getTransferTokenTransaction(
549
+ pocket.address,
550
+ mainWalletAddress,
551
+ token,
552
+ amount,
553
+ this.client
554
+ );
555
+
556
+ return await signAndSendTransaction(tx, [pocket.privateKey], this.client);
557
+ }
558
+
559
+ dispose(): void {
560
+ super.dispose();
561
+ this.clearClient();
562
+ }
563
+ }
564
+ ```
565
+
566
+ ---
567
+
568
+ ### Phase 4: Multi-Chain Orchestrator 🚀
569
+
570
+ **File**: `utils/savings/multi-chain-savings.ts`
571
+
572
+ ```typescript
573
+ import { EVMSavingsManager } from "./evm-savings";
574
+ import { SVMSavingsManager } from "./svm-savings";
575
+ import { ChainWalletConfig, Balance } from "../types";
576
+ import { Hex, PublicClient, WalletClient } from "viem";
577
+ import { Connection, PublicKey, Keypair } from "@solana/web3.js";
578
+
579
+ export type ChainType = 'EVM' | 'SVM';
580
+
581
+ export interface ChainConfig {
582
+ id: string; // e.g., "ethereum", "polygon", "solana"
583
+ type: ChainType;
584
+ config: ChainWalletConfig | { rpcUrl: string };
585
+ }
586
+
587
+ export interface PocketBalance {
588
+ chainId: string;
589
+ chainType: ChainType;
590
+ pocketIndex: number;
591
+ address: string;
592
+ balances: {
593
+ token: string; // address or 'native'
594
+ balance: Balance;
595
+ }[];
596
+ }
597
+
598
+ /**
599
+ * Multi-Chain Savings Manager
600
+ *
601
+ * Orchestrates savings across multiple chains (EVM and Solana)
602
+ * Similar to how you might have a multi-chain wallet manager
603
+ */
604
+ export class MultiChainSavingsManager {
605
+ private mnemonic: string;
606
+ private walletIndex: number;
607
+
608
+ // Separate managers by chain type
609
+ private evmManagers: Map<string, EVMSavingsManager> = new Map();
610
+ private svmManagers: Map<string, SVMSavingsManager> = new Map();
611
+
612
+ // Track chain configs
613
+ private chainConfigs: Map<string, ChainConfig> = new Map();
614
+
615
+ constructor(
616
+ mnemonic: string,
617
+ chains: ChainConfig[],
618
+ walletIndex: number = 0
619
+ ) {
620
+ this.mnemonic = mnemonic;
621
+ this.walletIndex = walletIndex;
622
+
623
+ // Initialize managers for each chain
624
+ for (const chain of chains) {
625
+ this.addChain(chain);
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Add a new chain
631
+ */
632
+ addChain(chain: ChainConfig): void {
633
+ this.chainConfigs.set(chain.id, chain);
634
+
635
+ if (chain.type === 'EVM') {
636
+ const manager = new EVMSavingsManager(
637
+ this.mnemonic,
638
+ chain.config as ChainWalletConfig,
639
+ this.walletIndex
640
+ );
641
+ this.evmManagers.set(chain.id, manager);
642
+ } else if (chain.type === 'SVM') {
643
+ const config = chain.config as { rpcUrl: string };
644
+ const manager = new SVMSavingsManager(
645
+ this.mnemonic,
646
+ config.rpcUrl,
647
+ this.walletIndex
648
+ );
649
+ this.svmManagers.set(chain.id, manager);
650
+ }
651
+ }
652
+
653
+ /**
654
+ * Remove a chain
655
+ */
656
+ removeChain(chainId: string): void {
657
+ if (this.evmManagers.has(chainId)) {
658
+ this.evmManagers.get(chainId)?.dispose();
659
+ this.evmManagers.delete(chainId);
660
+ }
661
+ if (this.svmManagers.has(chainId)) {
662
+ this.svmManagers.get(chainId)?.dispose();
663
+ this.svmManagers.delete(chainId);
664
+ }
665
+ this.chainConfigs.delete(chainId);
666
+ }
667
+
668
+ /**
669
+ * Get list of all chain IDs
670
+ */
671
+ getChains(): string[] {
672
+ return Array.from(this.chainConfigs.keys());
673
+ }
674
+
675
+ /**
676
+ * Get pocket address for a specific chain
677
+ */
678
+ getPocketAddress(chainId: string, pocketIndex: number): string {
679
+ const chain = this.chainConfigs.get(chainId);
680
+ if (!chain) throw new Error(`Chain not found: ${chainId}`);
681
+
682
+ if (chain.type === 'EVM') {
683
+ const manager = this.evmManagers.get(chainId)!;
684
+ return manager.getPocket(pocketIndex).address;
685
+ } else {
686
+ const manager = this.svmManagers.get(chainId)!;
687
+ return manager.getPocket(pocketIndex).address.toBase58();
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Get pocket balance for a specific chain
693
+ */
694
+ async getPocketBalance(
695
+ chainId: string,
696
+ pocketIndex: number,
697
+ tokens: string[]
698
+ ): Promise<PocketBalance> {
699
+ const chain = this.chainConfigs.get(chainId);
700
+ if (!chain) throw new Error(`Chain not found: ${chainId}`);
701
+
702
+ if (chain.type === 'EVM') {
703
+ const manager = this.evmManagers.get(chainId)!;
704
+ const balances = await manager.getPocketBalance(pocketIndex, tokens);
705
+ const pocket = manager.getPocket(pocketIndex);
706
+
707
+ return {
708
+ chainId,
709
+ chainType: 'EVM',
710
+ pocketIndex,
711
+ address: pocket.address,
712
+ balances: balances.map(b => ({
713
+ token: b.address,
714
+ balance: b.balance
715
+ }))
716
+ };
717
+ } else {
718
+ const manager = this.svmManagers.get(chainId)!;
719
+ const balances = await manager.getPocketBalance(pocketIndex, tokens);
720
+ const pocket = manager.getPocket(pocketIndex);
721
+
722
+ return {
723
+ chainId,
724
+ chainType: 'SVM',
725
+ pocketIndex,
726
+ address: pocket.address.toBase58(),
727
+ balances: balances.map(b => ({
728
+ token: b.address === 'native' ? 'native' : b.address.toBase58(),
729
+ balance: b.balance
730
+ }))
731
+ };
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Get pocket balance across multiple chains
737
+ */
738
+ async getPocketBalanceAcrossChains(
739
+ pocketIndex: number,
740
+ tokensByChain: Map<string, string[]>
741
+ ): Promise<PocketBalance[]> {
742
+ const promises: Promise<PocketBalance>[] = [];
743
+
744
+ for (const [chainId, tokens] of tokensByChain) {
745
+ promises.push(this.getPocketBalance(chainId, pocketIndex, tokens));
746
+ }
747
+
748
+ return await Promise.all(promises);
749
+ }
750
+
751
+ /**
752
+ * Get balances for multiple pockets across multiple chains
753
+ */
754
+ async getAllPocketsBalanceAcrossChains(
755
+ pocketIndices: number[],
756
+ tokensByChain: Map<string, string[]>
757
+ ): Promise<Map<number, PocketBalance[]>> {
758
+ const results = new Map<number, PocketBalance[]>();
759
+
760
+ for (const pocketIndex of pocketIndices) {
761
+ const balances = await this.getPocketBalanceAcrossChains(
762
+ pocketIndex,
763
+ tokensByChain
764
+ );
765
+ results.set(pocketIndex, balances);
766
+ }
767
+
768
+ return results;
769
+ }
770
+
771
+ /**
772
+ * Get EVM manager for a chain (for advanced operations)
773
+ */
774
+ getEVMManager(chainId: string): EVMSavingsManager {
775
+ const manager = this.evmManagers.get(chainId);
776
+ if (!manager) throw new Error(`EVM chain not found: ${chainId}`);
777
+ return manager;
778
+ }
779
+
780
+ /**
781
+ * Get SVM manager for a chain (for advanced operations)
782
+ */
783
+ getSVMManager(chainId: string): SVMSavingsManager {
784
+ const manager = this.svmManagers.get(chainId);
785
+ if (!manager) throw new Error(`SVM chain not found: ${chainId}`);
786
+ return manager;
787
+ }
788
+
789
+ /**
790
+ * Clear all pockets across all chains
791
+ */
792
+ clearAllPockets(): void {
793
+ for (const manager of this.evmManagers.values()) {
794
+ manager.clearAllPockets();
795
+ }
796
+ for (const manager of this.svmManagers.values()) {
797
+ manager.clearAllPockets();
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Dispose all managers
803
+ */
804
+ dispose(): void {
805
+ for (const manager of this.evmManagers.values()) {
806
+ manager.dispose();
807
+ }
808
+ for (const manager of this.svmManagers.values()) {
809
+ manager.dispose();
810
+ }
811
+ this.evmManagers.clear();
812
+ this.svmManagers.clear();
813
+ this.chainConfigs.clear();
814
+ (this as any).mnemonic = '';
815
+ }
816
+ }
817
+ ```
818
+
819
+ ---
820
+
821
+ ### Phase 5: Update Exports 📦
822
+
823
+ **File**: `utils/savings/index.ts`
824
+
825
+ ```typescript
826
+ // Base classes
827
+ export * from "./savings-manager";
828
+ export * from "./evm-savings";
829
+ export * from "./svm-savings";
830
+ export * from "./multi-chain-savings";
831
+
832
+ // Types and utilities
833
+ export * from "./types";
834
+ export * from "./validation";
835
+
836
+ // Backward compatibility (if needed)
837
+ export { EVMSavingsManager as SavingsManager } from "./evm-savings";
838
+ ```
839
+
840
+ ---
841
+
842
+ ## 📊 Usage Examples
843
+
844
+ ### Example 1: Single EVM Chain (Simple)
845
+
846
+ ```typescript
847
+ import { EVMSavingsManager } from './savings/evm-savings';
848
+
849
+ const manager = new EVMSavingsManager(
850
+ mnemonic,
851
+ { chainId: 1, name: 'ethereum', rpcUrl: 'https://...' },
852
+ 0 // wallet index
853
+ );
854
+
855
+ // Get pocket
856
+ const pocket = manager.getPocket(0);
857
+ console.log(pocket.address); // 0x...
858
+
859
+ // Get balances
860
+ const balances = await manager.getPocketBalance(0, [usdcAddress]);
861
+
862
+ // Transfer to pocket
863
+ await manager.transferToPocket(walletClient, 0, '0.1');
864
+ ```
865
+
866
+ ---
867
+
868
+ ### Example 2: Solana (Simple)
869
+
870
+ ```typescript
871
+ import { SVMSavingsManager } from './savings/svm-savings';
872
+
873
+ const manager = new SVMSavingsManager(
874
+ mnemonic,
875
+ 'https://api.mainnet-beta.solana.com',
876
+ 0 // wallet index
877
+ );
878
+
879
+ // Get pocket
880
+ const pocket = manager.getPocket(0);
881
+ console.log(pocket.address.toBase58()); // Solana address
882
+
883
+ // Get balances
884
+ const balances = await manager.getPocketBalance(0, [usdcMint]);
885
+
886
+ // Transfer to pocket
887
+ await manager.transferToPocket(mainWalletKeypair, 0, 1000000n);
888
+ ```
889
+
890
+ ---
891
+
892
+ ### Example 3: Multi-Chain (Advanced)
893
+
894
+ ```typescript
895
+ import { MultiChainSavingsManager } from './savings/multi-chain-savings';
896
+
897
+ const manager = new MultiChainSavingsManager(
898
+ mnemonic,
899
+ [
900
+ {
901
+ id: 'ethereum',
902
+ type: 'EVM',
903
+ config: { chainId: 1, name: 'ethereum', rpcUrl: 'https://...' }
904
+ },
905
+ {
906
+ id: 'polygon',
907
+ type: 'EVM',
908
+ config: { chainId: 137, name: 'polygon', rpcUrl: 'https://...' }
909
+ },
910
+ {
911
+ id: 'solana',
912
+ type: 'SVM',
913
+ config: { rpcUrl: 'https://api.mainnet-beta.solana.com' }
914
+ },
915
+ ],
916
+ 0 // wallet index
917
+ );
918
+
919
+ // Get pocket address on Ethereum
920
+ const ethAddress = manager.getPocketAddress('ethereum', 0);
921
+
922
+ // Get pocket address on Polygon (same as Ethereum!)
923
+ const polyAddress = manager.getPocketAddress('polygon', 0);
924
+ console.log(ethAddress === polyAddress); // true! (same EVM address)
925
+
926
+ // Get pocket address on Solana (different!)
927
+ const solAddress = manager.getPocketAddress('solana', 0);
928
+ console.log(ethAddress !== solAddress); // true (different chain, different address)
929
+
930
+ // Get balances across all chains for pocket 0
931
+ const balances = await manager.getPocketBalanceAcrossChains(
932
+ 0,
933
+ new Map([
934
+ ['ethereum', [usdcEth]],
935
+ ['polygon', [usdcPoly]],
936
+ ['solana', [usdcSol]]
937
+ ])
938
+ );
939
+
940
+ // Results grouped by chain
941
+ balances.forEach(balance => {
942
+ console.log(`${balance.chainId}:`, balance.balances);
943
+ });
944
+
945
+ // Get specific manager for advanced operations
946
+ const ethManager = manager.getEVMManager('ethereum');
947
+ await ethManager.transferToPocket(walletClient, 0, '0.1');
948
+
949
+ const solManager = manager.getSVMManager('solana');
950
+ await solManager.transferToPocket(mainKeypair, 0, 1000000n);
951
+ ```
952
+
953
+ ---
954
+
955
+ ## ✅ Implementation Checklist
956
+
957
+ ### Phase 1: Base Infrastructure
958
+ - [ ] Create `savings-manager.ts` with abstract base class
959
+ - [ ] Add generic types for AddressType, ClientType, WalletClientType
960
+ - [ ] Implement shared methods (getPocket, clearPocket, dispose)
961
+ - [ ] Add validation checks
962
+
963
+ ### Phase 2: EVM Implementation
964
+ - [ ] Create `evm-savings.ts` extending base class
965
+ - [ ] Implement EVM-specific derivation (coin type 60)
966
+ - [ ] Implement balance queries for EVM
967
+ - [ ] Implement transfer operations for EVM
968
+ - [ ] Add tests for EVM savings
969
+
970
+ ### Phase 3: SVM Implementation
971
+ - [ ] Create `svm-savings.ts` extending base class
972
+ - [ ] Implement Solana-specific derivation (coin type 501)
973
+ - [ ] Implement balance queries for Solana
974
+ - [ ] Implement transfer operations for Solana
975
+ - [ ] Add tests for SVM savings
976
+
977
+ ### Phase 4: Multi-Chain Orchestrator
978
+ - [ ] Create `multi-chain-savings.ts`
979
+ - [ ] Implement chain management (add/remove chains)
980
+ - [ ] Implement cross-chain balance queries
981
+ - [ ] Add chain type detection
982
+ - [ ] Add manager access methods
983
+
984
+ ### Phase 5: Integration & Testing
985
+ - [ ] Update exports in `index.ts`
986
+ - [ ] Write unit tests for all classes
987
+ - [ ] Write integration tests for multi-chain
988
+ - [ ] Update documentation
989
+ - [ ] Create usage examples
990
+
991
+ ---
992
+
993
+ ## 🎓 Key Design Decisions
994
+
995
+ ### 1. Same Pattern as VM Classes ✅
996
+ - Abstract base class with generics
997
+ - Concrete implementations for each chain type
998
+ - Shared logic in base, specific logic in subclasses
999
+
1000
+ ### 2. EVM Chains Share Addresses ✅
1001
+ - All EVM chains use coin type 60
1002
+ - Same mnemonic generates same addresses
1003
+ - One `EVMSavingsManager` per EVM chain (for different RPC endpoints)
1004
+
1005
+ ### 3. Solana Has Different Addresses ✅
1006
+ - Uses coin type 501
1007
+ - Different derivation path = different addresses
1008
+ - Separate `SVMSavingsManager`
1009
+
1010
+ ### 4. No Backward Compatibility Needed ✅
1011
+ - Pockets not implemented yet
1012
+ - Clean slate for proper design
1013
+ - Can design optimal structure from start
1014
+
1015
+ ---
1016
+
1017
+ ## 📝 Next Steps
1018
+
1019
+ 1. **Review & Approve** this plan
1020
+ 2. **Start with Phase 1** (Base class)
1021
+ 3. **Implement Phase 2** (EVM)
1022
+ 4. **Implement Phase 3** (Solana)
1023
+ 5. **Add Phase 4** (Multi-chain)
1024
+ 6. **Test & Document**
1025
+
1026
+ ---
1027
+
1028
+ **Ready to implement?** Let me know which phase to start with!