@deserialize/multi-vm-wallet 1.5.0 → 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.
- 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 +0 -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/svm/svm.js +0 -1
- package/dist/test.js +111 -4
- package/dist/vm-validation.d.ts +0 -1
- package/dist/vm-validation.js +0 -12
- package/dist/walletBip32.js +0 -2
- package/package.json +1 -1
- package/utils/evm/evm.ts +5 -5
- 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/svm/svm.ts +1 -1
- package/utils/test.ts +167 -8
- package/utils/vm-validation.ts +17 -17
- package/utils/walletBip32.ts +2 -2
|
@@ -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!
|