@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/dist/test.js CHANGED
@@ -119,12 +119,119 @@ const testPrice = async () => {
119
119
  walletA.getPrices(['0x98d0baa52b2D063E780DE12F615f963Fe8537553']).then(console.log);
120
120
  };
121
121
  const testSavingsPocket = async () => {
122
+ console.log('\n========== EVM Savings Test ==========');
122
123
  const mnemonic = evm_1.EVMVM.generateMnemonicFromPrivateKey(evmPrivateKeyExposed);
123
- console.log('mnemonic: ', mnemonic);
124
- const savingsManager = new savings_1.SavingsManager(mnemonic, evmChainConfig, wallet.index);
124
+ console.log('Mnemonic:', mnemonic);
125
+ const savingsManager = new savings_1.EVMSavingsManager(mnemonic, evmChainConfig, wallet.index);
125
126
  const pocket0 = savingsManager.getPocket(0);
126
- const balance = await savingsManager.getPocketTokenBalance([], 0);
127
- console.log('balance: ', balance);
127
+ console.log('\nPocket 0 Info:');
128
+ console.log(' Address:', pocket0.address);
129
+ console.log(' Derivation path:', pocket0.derivationPath);
130
+ console.log(' Index:', pocket0.index);
131
+ const balance = await savingsManager.getPocketBalance(0, []);
132
+ console.log('\nPocket 0 Balance:');
133
+ console.log(' Native balance:', balance[0].balance.formatted, 'ETH');
134
+ const pocket1 = savingsManager.getPocket(1);
135
+ const pocket2 = savingsManager.getPocket(2);
136
+ console.log('\nAdditional Pockets:');
137
+ console.log(' Pocket 1:', pocket1.address);
138
+ console.log(' Pocket 2:', pocket2.address);
139
+ savingsManager.dispose();
140
+ console.log('\nāœ… EVM Savings Test Complete\n');
141
+ };
142
+ const testSavingsPocketSVM = async () => {
143
+ console.log('\n========== SVM (Solana) Savings Test ==========');
144
+ const mnemonic = evm_1.EVMVM.generateMnemonicFromPrivateKey(evmPrivateKeyExposed);
145
+ console.log('Mnemonic:', mnemonic);
146
+ const savingsManager = new savings_1.SVMSavingsManager(mnemonic, chainConfig.rpcUrl, 0);
147
+ const pocket0 = savingsManager.getPocket(0);
148
+ console.log('\nPocket 0 Info:');
149
+ console.log(' Address:', pocket0.address.toBase58());
150
+ console.log(' Derivation path:', pocket0.derivationPath);
151
+ console.log(' Index:', pocket0.index);
152
+ const balance = await savingsManager.getPocketBalance(0, []);
153
+ console.log('\nPocket 0 Balance:');
154
+ console.log(' Native balance:', balance[0].balance.formatted, 'SOL');
155
+ const pocket1 = savingsManager.getPocket(1);
156
+ const pocket2 = savingsManager.getPocket(2);
157
+ console.log('\nAdditional Pockets:');
158
+ console.log(' Pocket 1:', pocket1.address.toBase58());
159
+ console.log(' Pocket 2:', pocket2.address.toBase58());
160
+ console.log('\nšŸ’” Note: Solana uses coin type 501, so addresses differ from EVM');
161
+ savingsManager.dispose();
162
+ console.log('\nāœ… SVM Savings Test Complete\n');
163
+ };
164
+ const testSavingsPocketMultiChain = async () => {
165
+ console.log('\n========== Multi-Chain Savings Test ==========');
166
+ const mnemonic = evm_1.EVMVM.generateMnemonicFromPrivateKey(evmPrivateKeyExposed);
167
+ console.log('Mnemonic:', mnemonic);
168
+ const multiChainManager = new savings_1.MultiChainSavingsManager(mnemonic, [
169
+ {
170
+ id: 'base',
171
+ type: 'EVM',
172
+ config: evmChainConfig
173
+ },
174
+ {
175
+ id: 'ethereum',
176
+ type: 'EVM',
177
+ config: {
178
+ chainId: 1,
179
+ name: 'Ethereum',
180
+ rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/demo',
181
+ explorerUrl: 'https://etherscan.io',
182
+ nativeToken: { name: 'Ethereum', symbol: 'ETH', decimals: 18 },
183
+ confirmationNo: 1,
184
+ vmType: 'EVM'
185
+ }
186
+ },
187
+ {
188
+ id: 'solana',
189
+ type: 'SVM',
190
+ config: {
191
+ rpcUrl: chainConfig.rpcUrl
192
+ }
193
+ }
194
+ ], 0);
195
+ console.log('\nAvailable chains:', multiChainManager.getChains());
196
+ console.log('EVM chains:', multiChainManager.getEVMChains());
197
+ console.log('SVM chains:', multiChainManager.getSVMChains());
198
+ console.log('\nPocket 0 Addresses Across Chains:');
199
+ const baseAddress = multiChainManager.getPocketAddress('base', 0);
200
+ const ethAddress = multiChainManager.getPocketAddress('ethereum', 0);
201
+ const solAddress = multiChainManager.getPocketAddress('solana', 0);
202
+ console.log(' Base:', baseAddress);
203
+ console.log(' Ethereum:', ethAddress);
204
+ console.log(' Solana:', solAddress);
205
+ console.log('\nšŸ” EVM Address Check:');
206
+ console.log(' Base === Ethereum?', baseAddress === ethAddress, 'āœ…');
207
+ console.log(' Base === Solana?', baseAddress === solAddress, 'āŒ (different chain type)');
208
+ console.log('\nšŸ“Š Getting balances across all chains...');
209
+ try {
210
+ const balances = await multiChainManager.getPocketBalanceAcrossChains(0, new Map([
211
+ ['base', []],
212
+ ['ethereum', []],
213
+ ['solana', []]
214
+ ]));
215
+ console.log('\nBalances for Pocket 0:');
216
+ balances.forEach(chainBalance => {
217
+ console.log(`\n ${chainBalance.chainId.toUpperCase()} (${chainBalance.chainType}):`);
218
+ console.log(` Address: ${chainBalance.address}`);
219
+ chainBalance.balances.forEach(bal => {
220
+ const tokenName = bal.token === 'native' ? 'Native' : bal.token;
221
+ console.log(` ${tokenName}: ${bal.balance.formatted}`);
222
+ });
223
+ });
224
+ }
225
+ catch (error) {
226
+ console.log(' āš ļø Balance fetch error (expected for demo RPCs):', error.message);
227
+ }
228
+ console.log('\nšŸ”§ Advanced: Direct chain manager access');
229
+ const baseManager = multiChainManager.getEVMManager('base');
230
+ const solanaManager = multiChainManager.getSVMManager('solana');
231
+ console.log(' Base manager type:', baseManager.constructor.name);
232
+ console.log(' Solana manager type:', solanaManager.constructor.name);
233
+ multiChainManager.dispose();
234
+ console.log('\nāœ… Multi-Chain Savings Test Complete\n');
128
235
  };
129
236
  const RPC_URL = chainConfig.rpcUrl;
130
237
  const connection = new web3_js_1.Connection(RPC_URL);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deserialize/multi-vm-wallet",
3
- "version": "1.5.11",
3
+ "version": "1.5.21",
4
4
  "devDependencies": {
5
5
  "@types/bn.js": "^5.2.0",
6
6
  "@types/crypto-js": "^4.2.2",
package/utils/evm/evm.ts CHANGED
@@ -202,7 +202,7 @@ export class EVMVM extends VM<string, string, PublicClient> {
202
202
  }
203
203
 
204
204
  static convertFromEntropyToPrivateKey = (entropy: string): string => {
205
- return toHex(entropy)
205
+ return toHex(entropy).substring(0, 64)
206
206
  }
207
207
 
208
208
  /**
package/utils/index.ts CHANGED
@@ -1,8 +1,18 @@
1
1
  export * from "./walletBip32"
2
2
  export * from "./types"
3
3
  export * from "./vm"
4
- export * from "./evm"
5
- export * from "./svm"
4
+
5
+ // Export main classes and types from EVM and SVM
6
+ // Note: Utility functions (discoverTokens, getTokenBalance, etc.) have name conflicts
7
+ // Import them directly from './evm' or './svm' submodules when needed
8
+ export { EVMVM, EVMChainWallet, EVMSmartWallet } from "./evm"
9
+ export { SVMVM, SVMChainWallet, } from "./svm"
10
+
11
+ // Re-export all EVM exports as namespace to avoid conflicts
12
+ export * as evm from "./evm"
13
+ // Re-export all SVM exports as namespace to avoid conflicts
14
+ export * as svm from "./svm"
15
+
6
16
  export * from "./constant"
7
17
  export * from "bs58"
8
18
  export * from "@solana/web3.js"
@@ -0,0 +1,354 @@
1
+ /**
2
+ * EVM Savings Manager
3
+ *
4
+ * Manages savings pockets across EVM-compatible chains (Ethereum, Polygon, BSC, Arbitrum, etc.)
5
+ * All EVM chains use the same addresses (coin type 60 in BIP-44)
6
+ */
7
+
8
+ import { SavingsManager, Pocket } from "./savings-manager";
9
+ import { Hex, PublicClient, WalletClient, createPublicClient, createWalletClient, http } from "viem";
10
+ import { EVMDeriveChildPrivateKey, mnemonicToSeed } from "../walletBip32";
11
+ import { ChainWalletConfig, Balance, TransactionResult } from "../types";
12
+ import { ethers } from "ethers";
13
+ import {
14
+ fromChainToViemChain,
15
+ getNativeBalance,
16
+ getTokenBalance,
17
+ sendNativeToken,
18
+ sendERC20Token
19
+ } from "../evm";
20
+ import { SavingsValidation } from "./validation";
21
+ import { privateKeyToAccount } from "viem/accounts";
22
+
23
+ /**
24
+ * EVM Savings Manager
25
+ *
26
+ * Provides savings pocket functionality for EVM-compatible chains.
27
+ * Uses BIP-44 derivation path: m/44'/60'/{pocketIndex}'/0/{walletIndex}
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const manager = new EVMSavingsManager(
32
+ * mnemonic,
33
+ * { chainId: 1, name: 'ethereum', rpcUrl: 'https://...' },
34
+ * 0 // wallet index
35
+ * );
36
+ *
37
+ * // Get pocket
38
+ * const pocket = manager.getPocket(0);
39
+ * console.log(pocket.address); // 0x...
40
+ *
41
+ * // Get balances
42
+ * const balances = await manager.getPocketBalance(0, [usdcAddress]);
43
+ * ```
44
+ */
45
+ export class EVMSavingsManager extends SavingsManager<Hex, PublicClient, WalletClient> {
46
+ coinType = 60;
47
+ derivationPathBase = "m/44'/60'/";
48
+
49
+ private chain: ChainWalletConfig;
50
+ private _client?: PublicClient;
51
+ private masterAddress?: Hex;
52
+
53
+ /**
54
+ * Create a new EVMSavingsManager
55
+ *
56
+ * @param mnemonic - BIP-39 mnemonic phrase
57
+ * @param chain - Chain configuration with RPC URL and chain details
58
+ * @param walletIndex - Wallet index in derivation path (default: 0)
59
+ * @param masterAddress - Optional master wallet address
60
+ */
61
+ constructor(
62
+ mnemonic: string,
63
+ chain: ChainWalletConfig,
64
+ walletIndex: number = 0,
65
+ masterAddress?: Hex
66
+ ) {
67
+ super(mnemonic, walletIndex);
68
+
69
+ SavingsValidation.validateChainId(chain.chainId);
70
+ if (masterAddress) {
71
+ SavingsValidation.validateAddress(masterAddress, 'Master address');
72
+ }
73
+
74
+ this.chain = chain;
75
+ this.masterAddress = masterAddress;
76
+ }
77
+
78
+ /**
79
+ * Get or create the RPC client on-demand
80
+ *
81
+ * Lazy initialization allows the client to be garbage collected between operations.
82
+ */
83
+ get client(): PublicClient {
84
+ if (!this._client) {
85
+ this._client = this.createClient(this.chain.rpcUrl);
86
+ }
87
+ return this._client;
88
+ }
89
+
90
+ /**
91
+ * Create an RPC client for this chain
92
+ *
93
+ * @param rpcUrl - RPC endpoint URL
94
+ * @returns PublicClient instance
95
+ */
96
+ createClient(rpcUrl: string): PublicClient {
97
+ return createPublicClient({
98
+ chain: fromChainToViemChain(this.chain),
99
+ transport: http(rpcUrl)
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Clear the cached RPC client
105
+ */
106
+ clearClient(): void {
107
+ this._client = undefined;
108
+ }
109
+
110
+ /**
111
+ * Derive a savings pocket at the specified account index
112
+ *
113
+ * @param accountIndex - Account index (0-based)
114
+ * @returns Pocket object with privateKey, address, derivationPath, and index
115
+ */
116
+ derivePocket(accountIndex: number): Pocket<Hex> {
117
+ this.checkNotDisposed();
118
+ SavingsValidation.validateAccountIndex(accountIndex);
119
+
120
+ // Add 1 to preserve index 0 for main wallet
121
+ const pocketIndex = accountIndex + 1;
122
+ const derivationPath = `${this.derivationPathBase}${pocketIndex}'/0/${this.walletIndex}`;
123
+ const seed = mnemonicToSeed(this.mnemonic);
124
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
125
+ const wallet = new ethers.Wallet(privateKey);
126
+
127
+ const pocket: Pocket<Hex> = {
128
+ privateKey,
129
+ address: wallet.address as Hex,
130
+ derivationPath,
131
+ index: pocketIndex
132
+ };
133
+
134
+ this.pockets.set(accountIndex, pocket);
135
+ return pocket;
136
+ }
137
+
138
+ /**
139
+ * Get the main wallet credentials
140
+ *
141
+ * @returns Main wallet object with privateKey, address, and derivationPath
142
+ */
143
+ getMainWallet() {
144
+ this.checkNotDisposed();
145
+ const derivationPath = `${this.derivationPathBase}0'/0/${this.walletIndex}`;
146
+ const seed = mnemonicToSeed(this.mnemonic);
147
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
148
+ const wallet = new ethers.Wallet(privateKey);
149
+
150
+ return {
151
+ privateKey,
152
+ address: wallet.address as Hex,
153
+ derivationPath
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Get the main wallet address
159
+ *
160
+ * @returns Main wallet address as Hex
161
+ */
162
+ getMainWalletAddress(): Hex {
163
+ if (this.masterAddress) return this.masterAddress;
164
+ return this.getMainWallet().address;
165
+ }
166
+
167
+ /**
168
+ * Get token balances for a specific pocket
169
+ *
170
+ * @param pocketIndex - Pocket index to check
171
+ * @param tokens - Array of ERC-20 token addresses to query
172
+ * @returns Array of balance objects
173
+ */
174
+ async getPocketBalance(pocketIndex: number, tokens: string[]): Promise<{
175
+ address: Hex | 'native';
176
+ balance: Balance;
177
+ }[]> {
178
+ SavingsValidation.validateAccountIndex(pocketIndex);
179
+
180
+ if (!Array.isArray(tokens)) {
181
+ throw new Error('Tokens must be an array');
182
+ }
183
+
184
+ const pocket = this.getPocket(pocketIndex);
185
+ const balances: { address: Hex | 'native'; balance: Balance }[] = [];
186
+
187
+ // Get native balance
188
+ const nativeBalance = await getNativeBalance(pocket.address, this.client);
189
+ balances.push({ address: 'native', balance: nativeBalance });
190
+
191
+ // Get token balances
192
+ await Promise.all(tokens.map(async (token) => {
193
+ SavingsValidation.validateAddress(token, 'Token address');
194
+ const tokenBalance = await getTokenBalance(token as Hex, pocket.address, this.client);
195
+ balances.push({ address: token as Hex, balance: tokenBalance });
196
+ }));
197
+
198
+ return balances;
199
+ }
200
+
201
+ /**
202
+ * Get balances for multiple pockets
203
+ *
204
+ * @param pocketIndices - Array of pocket indices
205
+ * @param tokens - Array of token addresses to query
206
+ * @returns Array of balance arrays per pocket
207
+ */
208
+ async getTotalTokenBalanceOfAllPockets(
209
+ tokens: string[],
210
+ pockets: number[]
211
+ ): Promise<Array<{ address: Hex | 'native'; balance: Balance; }[]>> {
212
+ if (!Array.isArray(tokens) || tokens.length === 0) {
213
+ throw new Error('Tokens array must be non-empty');
214
+ }
215
+ if (!Array.isArray(pockets) || pockets.length === 0) {
216
+ throw new Error('Pockets array must be non-empty');
217
+ }
218
+
219
+ // Validate all inputs
220
+ tokens.forEach((token, index) => {
221
+ SavingsValidation.validateAddress(token, `Token at index ${index}`);
222
+ });
223
+ pockets.forEach((pocket) => {
224
+ SavingsValidation.validateAccountIndex(pocket);
225
+ });
226
+
227
+ // Fetch balances for all pockets in parallel
228
+ const allBalances = await Promise.all(
229
+ pockets.map((p: number) => this.getPocketBalance(p, tokens))
230
+ );
231
+
232
+ return allBalances;
233
+ }
234
+
235
+ /**
236
+ * Transfer native tokens from main wallet to a pocket
237
+ *
238
+ * @param mainWallet - WalletClient for the main wallet
239
+ * @param pocketIndex - Destination pocket index
240
+ * @param amount - Amount to transfer as string (in ether units)
241
+ * @returns Transaction result
242
+ */
243
+ async transferToPocket(
244
+ mainWallet: WalletClient,
245
+ pocketIndex: number,
246
+ amount: string
247
+ ): Promise<TransactionResult> {
248
+ SavingsValidation.validateAccountIndex(pocketIndex);
249
+ SavingsValidation.validateAmountString(amount, 'Transfer amount');
250
+
251
+ const pocket = this.getPocket(pocketIndex);
252
+ return await sendNativeToken(mainWallet, this.client, pocket.address, amount, 5);
253
+ }
254
+
255
+ /**
256
+ * Transfer ERC-20 tokens from main wallet to a pocket
257
+ *
258
+ * @param mainWallet - WalletClient for the main wallet
259
+ * @param tokenAddress - ERC-20 token contract address
260
+ * @param pocketIndex - Destination pocket index
261
+ * @param amount - Amount to transfer (in token base units)
262
+ * @returns Transaction result
263
+ */
264
+ async transferTokenToPocket(
265
+ mainWallet: WalletClient,
266
+ tokenAddress: string,
267
+ pocketIndex: number,
268
+ amount: bigint
269
+ ): Promise<TransactionResult> {
270
+ SavingsValidation.validateAddress(tokenAddress, 'Token address');
271
+ SavingsValidation.validateAccountIndex(pocketIndex);
272
+ SavingsValidation.validateAmount(amount, 'Transfer amount');
273
+
274
+ const pocket = this.getPocket(pocketIndex);
275
+ return await sendERC20Token(mainWallet, this.client, tokenAddress as Hex, pocket.address, amount, 5);
276
+ }
277
+
278
+ /**
279
+ * Create a Viem Account from a pocket's private key
280
+ *
281
+ * @param pocketIndex - Pocket index
282
+ * @returns Viem Account instance
283
+ */
284
+ accountFromPocketId(pocketIndex: number) {
285
+ const pocket = this.getPocket(pocketIndex);
286
+ return privateKeyToAccount(`0x${pocket.privateKey}`);
287
+ }
288
+
289
+ /**
290
+ * Verify that a stored pocket address matches the derived address
291
+ *
292
+ * @param accountIndex - Pocket index
293
+ * @param storedAddress - Address to verify
294
+ * @returns true if addresses match
295
+ */
296
+ verifyPocketAddress(accountIndex: number, storedAddress: string): boolean {
297
+ SavingsValidation.validateAccountIndex(accountIndex);
298
+ SavingsValidation.validateAddress(storedAddress, 'Stored address');
299
+
300
+ const pocket = this.getPocket(accountIndex);
301
+ return pocket.address.toLowerCase() === storedAddress.toLowerCase();
302
+ }
303
+
304
+ /**
305
+ * Send tokens from a pocket back to the main wallet
306
+ *
307
+ * @param pocketIndex - Source pocket index
308
+ * @param amount - Amount to send (in base units)
309
+ * @param token - Token address or "native"
310
+ * @returns Transaction result
311
+ */
312
+ async sendToMainWallet(
313
+ pocketIndex: number,
314
+ amount: bigint,
315
+ token: Hex | "native"
316
+ ): Promise<TransactionResult> {
317
+ SavingsValidation.validateAccountIndex(pocketIndex);
318
+
319
+ if (typeof amount !== 'bigint' || amount <= 0n) {
320
+ throw new Error(`Amount must be a positive bigint, got: ${amount}`);
321
+ }
322
+
323
+ if (token !== 'native') {
324
+ SavingsValidation.validateAddress(token, 'Token address');
325
+ }
326
+
327
+ const pocket = this.getPocket(pocketIndex);
328
+ const account = this.accountFromPocketId(pocketIndex);
329
+ const mainWalletAddress = this.getMainWalletAddress();
330
+
331
+ const walletClient = createWalletClient({
332
+ account,
333
+ transport: http(this.chain.rpcUrl),
334
+ chain: fromChainToViemChain(this.chain)
335
+ });
336
+
337
+ if (token === "native") {
338
+ return await sendNativeToken(walletClient, this.client, mainWalletAddress, amount);
339
+ }
340
+
341
+ return await sendERC20Token(walletClient, this.client, token, mainWalletAddress, amount);
342
+ }
343
+
344
+ /**
345
+ * Dispose and clear all resources
346
+ */
347
+ dispose(): void {
348
+ super.dispose();
349
+ this.clearClient();
350
+ if (this.masterAddress) {
351
+ (this as any).masterAddress = undefined;
352
+ }
353
+ }
354
+ }
@@ -1 +1,18 @@
1
- export * from "./saving-manager"
1
+ // Base abstract class and types
2
+ export * from "./savings-manager";
3
+
4
+ // Chain-specific implementations
5
+ export * from "./evm-savings";
6
+ export * from "./svm-savings";
7
+
8
+ // Multi-chain orchestrator
9
+ export * from "./multi-chain-savings";
10
+
11
+ // Types and validation
12
+ export * from "./types";
13
+ export * from "./validation";
14
+
15
+ // Legacy exports (old single-chain manager - rename to avoid conflict)
16
+ export { BaseSavingsManager as LegacyBaseSavingsManager, SavingsManager as LegacySavingsManager } from "./saving-manager";
17
+ export * from "./smart-savings";
18
+ export * from "./savings-operations";