@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,394 @@
1
+ /**
2
+ * SVM (Solana) Savings Manager
3
+ *
4
+ * Manages savings pockets on Solana blockchain
5
+ * Uses BIP-44 coin type 501 (different addresses from EVM)
6
+ */
7
+
8
+ import { SavingsManager, Pocket } from "./savings-manager";
9
+ import { Connection, PublicKey, Keypair, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
10
+ import { SVMDeriveChildPrivateKey, mnemonicToSeed } from "../walletBip32";
11
+ import { Balance, TransactionResult } from "../types";
12
+ import {
13
+ getSvmNativeBalance,
14
+ getTokenBalance as getSvmTokenBalance,
15
+ signAndSendTransaction,
16
+ getTransferNativeTransaction,
17
+ getTransferTokenTransaction
18
+ } from "../svm";
19
+ import { SavingsValidation } from "./validation";
20
+ import BN from "bn.js";
21
+
22
+ /**
23
+ * Solana Savings Manager
24
+ *
25
+ * Provides savings pocket functionality for Solana blockchain.
26
+ * Uses BIP-44 derivation path: m/44'/501'/{pocketIndex}'/0/{walletIndex}
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const manager = new SVMSavingsManager(
31
+ * mnemonic,
32
+ * 'https://api.mainnet-beta.solana.com',
33
+ * 0 // wallet index
34
+ * );
35
+ *
36
+ * // Get pocket
37
+ * const pocket = manager.getPocket(0);
38
+ * console.log(pocket.address.toBase58()); // Solana address
39
+ *
40
+ * // Get balances
41
+ * const balances = await manager.getPocketBalance(0, [usdcMint]);
42
+ * ```
43
+ */
44
+ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Keypair> {
45
+ coinType = 501;
46
+ derivationPathBase = "m/44'/501'/";
47
+
48
+ private rpcUrl: string;
49
+ private _client?: Connection;
50
+
51
+ /**
52
+ * Create a new SVMSavingsManager
53
+ *
54
+ * @param mnemonic - BIP-39 mnemonic phrase
55
+ * @param rpcUrl - Solana RPC endpoint URL
56
+ * @param walletIndex - Wallet index in derivation path (default: 0)
57
+ */
58
+ constructor(
59
+ mnemonic: string,
60
+ rpcUrl: string,
61
+ walletIndex: number = 0
62
+ ) {
63
+ super(mnemonic, walletIndex);
64
+
65
+ if (!rpcUrl || typeof rpcUrl !== 'string') {
66
+ throw new Error('RPC URL must be a non-empty string');
67
+ }
68
+
69
+ this.rpcUrl = rpcUrl;
70
+ }
71
+
72
+ /**
73
+ * Get or create the RPC client on-demand
74
+ *
75
+ * Lazy initialization allows the client to be garbage collected between operations.
76
+ */
77
+ get client(): Connection {
78
+ if (!this._client) {
79
+ this._client = this.createClient(this.rpcUrl);
80
+ }
81
+ return this._client;
82
+ }
83
+
84
+ /**
85
+ * Create a Connection to Solana
86
+ *
87
+ * @param rpcUrl - RPC endpoint URL
88
+ * @returns Connection instance
89
+ */
90
+ createClient(rpcUrl: string): Connection {
91
+ return new Connection(rpcUrl, 'confirmed');
92
+ }
93
+
94
+ /**
95
+ * Clear the cached RPC client
96
+ */
97
+ clearClient(): void {
98
+ this._client = undefined;
99
+ }
100
+
101
+ /**
102
+ * Derive a savings pocket at the specified account index
103
+ *
104
+ * @param accountIndex - Account index (0-based)
105
+ * @returns Pocket object with privateKey (Keypair), address (PublicKey), derivationPath, and index
106
+ */
107
+ derivePocket(accountIndex: number): Pocket<PublicKey> {
108
+ this.checkNotDisposed();
109
+ SavingsValidation.validateAccountIndex(accountIndex);
110
+
111
+ // Add 1 to preserve index 0 for main wallet
112
+ const pocketIndex = accountIndex + 1;
113
+ const derivationPath = `${this.derivationPathBase}${pocketIndex}'/0/${this.walletIndex}`;
114
+ const seed = mnemonicToSeed(this.mnemonic);
115
+ const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
116
+
117
+ const pocket: Pocket<PublicKey> = {
118
+ privateKey: keypair,
119
+ address: keypair.publicKey,
120
+ derivationPath,
121
+ index: pocketIndex
122
+ };
123
+
124
+ this.pockets.set(accountIndex, pocket);
125
+ return pocket;
126
+ }
127
+
128
+ /**
129
+ * Get the main wallet credentials
130
+ *
131
+ * @returns Main wallet object with privateKey (Keypair), address (PublicKey), and derivationPath
132
+ */
133
+ getMainWallet() {
134
+ this.checkNotDisposed();
135
+ const derivationPath = `${this.derivationPathBase}0'/0/${this.walletIndex}`;
136
+ const seed = mnemonicToSeed(this.mnemonic);
137
+ const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
138
+
139
+ return {
140
+ privateKey: keypair,
141
+ address: keypair.publicKey,
142
+ derivationPath
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Get the main wallet address
148
+ *
149
+ * @returns Main wallet PublicKey
150
+ */
151
+ getMainWalletAddress(): PublicKey {
152
+ return this.getMainWallet().address;
153
+ }
154
+
155
+ /**
156
+ * Get token balances for a specific pocket
157
+ *
158
+ * @param pocketIndex - Pocket index to check
159
+ * @param tokens - Array of SPL token mint addresses
160
+ * @returns Array of balance objects
161
+ */
162
+ async getPocketBalance(pocketIndex: number, tokens: string[]): Promise<{
163
+ address: PublicKey | 'native';
164
+ balance: Balance;
165
+ }[]> {
166
+ SavingsValidation.validateAccountIndex(pocketIndex);
167
+
168
+ if (!Array.isArray(tokens)) {
169
+ throw new Error('Tokens must be an array');
170
+ }
171
+
172
+ const pocket = this.getPocket(pocketIndex);
173
+ const balances: { address: PublicKey | 'native'; balance: Balance }[] = [];
174
+
175
+ // Get native SOL balance
176
+ const nativeBalance = await getSvmNativeBalance(pocket.address, this.client);
177
+ balances.push({ address: 'native', balance: nativeBalance });
178
+
179
+ // Get SPL token balances
180
+ await Promise.all(tokens.map(async (token) => {
181
+ try {
182
+ const tokenPubkey = new PublicKey(token);
183
+ const tokenBalanceData = await getSvmTokenBalance(pocket.address, tokenPubkey, this.client);
184
+
185
+ // Handle the case where getTokenBalance returns 0 or TokenAmount
186
+ if (tokenBalanceData === 0) {
187
+ balances.push({
188
+ address: tokenPubkey,
189
+ balance: { balance: new BN(0), formatted: 0, decimal: 0 }
190
+ });
191
+ } else {
192
+ const balance: Balance = {
193
+ balance: new BN(tokenBalanceData.amount),
194
+ formatted: tokenBalanceData.uiAmount || 0,
195
+ decimal: tokenBalanceData.decimals
196
+ };
197
+ balances.push({ address: tokenPubkey, balance });
198
+ }
199
+ } catch (error) {
200
+ // Token account might not exist, push zero balance
201
+ balances.push({
202
+ address: new PublicKey(token),
203
+ balance: { balance: new BN(0), formatted: 0, decimal: 0 }
204
+ });
205
+ }
206
+ }));
207
+
208
+ return balances;
209
+ }
210
+
211
+ /**
212
+ * Get balances for multiple pockets
213
+ *
214
+ * @param tokens - Array of token mint addresses
215
+ * @param pockets - Array of pocket indices
216
+ * @returns Array of balance arrays per pocket
217
+ */
218
+ async getTotalTokenBalanceOfAllPockets(
219
+ tokens: string[],
220
+ pockets: number[]
221
+ ): Promise<Array<{ address: PublicKey | 'native'; balance: Balance; }[]>> {
222
+ if (!Array.isArray(tokens) || tokens.length === 0) {
223
+ throw new Error('Tokens array must be non-empty');
224
+ }
225
+ if (!Array.isArray(pockets) || pockets.length === 0) {
226
+ throw new Error('Pockets array must be non-empty');
227
+ }
228
+
229
+ // Validate all pocket indices
230
+ pockets.forEach((pocket) => {
231
+ SavingsValidation.validateAccountIndex(pocket);
232
+ });
233
+
234
+ // Fetch balances for all pockets in parallel
235
+ const allBalances = await Promise.all(
236
+ pockets.map((p: number) => this.getPocketBalance(p, tokens))
237
+ );
238
+
239
+ return allBalances;
240
+ }
241
+
242
+ /**
243
+ * Transfer SOL from main wallet to a pocket
244
+ *
245
+ * @param mainWallet - Keypair for the main wallet
246
+ * @param pocketIndex - Destination pocket index
247
+ * @param amount - Amount to transfer in lamports
248
+ * @returns Transaction result
249
+ */
250
+ async transferToPocket(
251
+ mainWallet: Keypair,
252
+ pocketIndex: number,
253
+ amount: bigint
254
+ ): Promise<TransactionResult> {
255
+ SavingsValidation.validateAccountIndex(pocketIndex);
256
+
257
+ if (typeof amount !== 'bigint' || amount <= 0n) {
258
+ throw new Error(`Amount must be a positive bigint, got: ${amount}`);
259
+ }
260
+
261
+ const pocket = this.getPocket(pocketIndex);
262
+ const tx = await getTransferNativeTransaction(
263
+ mainWallet,
264
+ pocket.address,
265
+ Number(amount),
266
+ this.client
267
+ );
268
+
269
+ const hash = await signAndSendTransaction(tx, this.client, mainWallet);
270
+ return { success: true, hash };
271
+ }
272
+
273
+ /**
274
+ * Transfer SPL tokens from main wallet to a pocket
275
+ *
276
+ * @param mainWallet - Keypair for the main wallet
277
+ * @param tokenInfo - SPL token information (address, decimals, etc.)
278
+ * @param pocketIndex - Destination pocket index
279
+ * @param amount - Amount to transfer (in token base units)
280
+ * @returns Transaction result
281
+ */
282
+ async transferTokenToPocket(
283
+ mainWallet: Keypair,
284
+ tokenInfo: { address: string; decimals: number },
285
+ pocketIndex: number,
286
+ amount: bigint
287
+ ): Promise<TransactionResult> {
288
+ SavingsValidation.validateAccountIndex(pocketIndex);
289
+
290
+ if (typeof amount !== 'bigint' || amount <= 0n) {
291
+ throw new Error(`Amount must be a positive bigint, got: ${amount}`);
292
+ }
293
+
294
+ const pocket = this.getPocket(pocketIndex);
295
+
296
+ const tx = await getTransferTokenTransaction(
297
+ mainWallet,
298
+ pocket.address,
299
+ tokenInfo as any, // TokenInfo type
300
+ Number(amount),
301
+ this.client
302
+ );
303
+
304
+ const hash = await signAndSendTransaction(tx, this.client, mainWallet);
305
+ return { success: true, hash };
306
+ }
307
+
308
+ /**
309
+ * Get Keypair from pocket (for signing transactions)
310
+ *
311
+ * @param pocketIndex - Pocket index
312
+ * @returns Keypair instance
313
+ */
314
+ accountFromPocketId(pocketIndex: number): Keypair {
315
+ const pocket = this.getPocket(pocketIndex);
316
+ return pocket.privateKey;
317
+ }
318
+
319
+ /**
320
+ * Verify that a stored pocket address matches the derived address
321
+ *
322
+ * @param accountIndex - Pocket index
323
+ * @param storedAddress - Address to verify (base58 string)
324
+ * @returns true if addresses match
325
+ */
326
+ verifyPocketAddress(accountIndex: number, storedAddress: string): boolean {
327
+ SavingsValidation.validateAccountIndex(accountIndex);
328
+
329
+ if (!storedAddress || typeof storedAddress !== 'string') {
330
+ throw new Error('Stored address must be a non-empty string');
331
+ }
332
+
333
+ try {
334
+ const pocket = this.getPocket(accountIndex);
335
+ const storedPubkey = new PublicKey(storedAddress);
336
+ return pocket.address.equals(storedPubkey);
337
+ } catch (error) {
338
+ return false;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Send tokens from a pocket back to the main wallet
344
+ *
345
+ * @param pocketIndex - Source pocket index
346
+ * @param amount - Amount to send (in base units)
347
+ * @param token - Token info object or "native" for SOL
348
+ * @returns Transaction result
349
+ */
350
+ async sendToMainWallet(
351
+ pocketIndex: number,
352
+ amount: bigint,
353
+ token: { address: string; decimals: number } | "native"
354
+ ): Promise<TransactionResult> {
355
+ SavingsValidation.validateAccountIndex(pocketIndex);
356
+
357
+ if (typeof amount !== 'bigint' || amount <= 0n) {
358
+ throw new Error(`Amount must be a positive bigint, got: ${amount}`);
359
+ }
360
+
361
+ const pocket = this.getPocket(pocketIndex);
362
+ const mainWalletAddress = this.getMainWalletAddress();
363
+
364
+ if (token === "native") {
365
+ const tx = await getTransferNativeTransaction(
366
+ pocket.privateKey,
367
+ mainWalletAddress,
368
+ Number(amount),
369
+ this.client
370
+ );
371
+ const hash = await signAndSendTransaction(tx, this.client, pocket.privateKey);
372
+ return { success: true, hash };
373
+ }
374
+
375
+ const tx = await getTransferTokenTransaction(
376
+ pocket.privateKey,
377
+ mainWalletAddress,
378
+ token as any, // TokenInfo type
379
+ Number(amount),
380
+ this.client
381
+ );
382
+
383
+ const hash = await signAndSendTransaction(tx, this.client, pocket.privateKey);
384
+ return { success: true, hash };
385
+ }
386
+
387
+ /**
388
+ * Dispose and clear all resources
389
+ */
390
+ dispose(): void {
391
+ super.dispose();
392
+ this.clearClient();
393
+ }
394
+ }
@@ -1 +1,2 @@
1
- export * from "./svm"
1
+ export * from "./svm"
2
+ export * from "./utils"
package/utils/test.ts CHANGED
@@ -22,7 +22,7 @@ import { entryPoint07Address } from "viem/account-abstraction";
22
22
  import { KERNEL_V3_3, KernelVersionToAddressesMap } from '@zerodev/sdk/constants';
23
23
  import { deserializeSessionKey } from "./evm/aa-service";
24
24
  import { toSpendingLimitHook } from "@zerodev/hooks"
25
- import { SavingsManager } from "./savings";
25
+ import { EVMSavingsManager, SVMSavingsManager, MultiChainSavingsManager } from "./savings";
26
26
  // const mnemonic = GenerateNewMnemonic()
27
27
 
28
28
 
@@ -212,27 +212,186 @@ const testPrice = async () => {
212
212
  walletA.getPrices(['0x98d0baa52b2D063E780DE12F615f963Fe8537553']).then(console.log)
213
213
  }
214
214
 
215
+ // ============================================
216
+ // EVM Savings Pocket Test
217
+ // ============================================
215
218
  const testSavingsPocket = async () => {
219
+ console.log('\n========== EVM Savings Test ==========');
220
+ const mnemonic = EVMVM.generateMnemonicFromPrivateKey(evmPrivateKeyExposed)
221
+ console.log('Mnemonic:', mnemonic);
222
+
223
+ // Using new EVMSavingsManager (multi-chain compatible)
224
+ const savingsManager = new EVMSavingsManager(mnemonic, evmChainConfig, wallet.index)
216
225
 
226
+ const pocket0 = savingsManager.getPocket(0)
227
+ console.log('\nPocket 0 Info:');
228
+ console.log(' Address:', pocket0.address);
229
+ console.log(' Derivation path:', pocket0.derivationPath);
230
+ console.log(' Index:', pocket0.index);
231
+
232
+ // Uncomment to test transfer
233
+ // const deposited = await savingsManager.transferToPocket(walletA.wallet, 0, "0.00001")
234
+ // console.log('deposited: ', deposited);
217
235
 
236
+ // Get balance (updated API: getPocketBalance(pocketIndex, tokens[]))
237
+ const balance = await savingsManager.getPocketBalance(0, [])
238
+ console.log('\nPocket 0 Balance:');
239
+ console.log(' Native balance:', balance[0].balance.formatted, 'ETH');
240
+
241
+ // Get multiple pockets
242
+ const pocket1 = savingsManager.getPocket(1)
243
+ const pocket2 = savingsManager.getPocket(2)
244
+ console.log('\nAdditional Pockets:');
245
+ console.log(' Pocket 1:', pocket1.address);
246
+ console.log(' Pocket 2:', pocket2.address);
247
+
248
+ // Cleanup
249
+ savingsManager.dispose();
250
+ console.log('\nāœ… EVM Savings Test Complete\n');
251
+ }
252
+
253
+ // ============================================
254
+ // SVM (Solana) Savings Pocket Test
255
+ // ============================================
256
+ const testSavingsPocketSVM = async () => {
257
+ console.log('\n========== SVM (Solana) Savings Test ==========');
218
258
  const mnemonic = EVMVM.generateMnemonicFromPrivateKey(evmPrivateKeyExposed)
219
- console.log('mnemonic: ', mnemonic);
259
+ console.log('Mnemonic:', mnemonic);
220
260
 
221
- const savingsManager = new SavingsManager(mnemonic, evmChainConfig, wallet.index)
261
+ // Using SVMSavingsManager for Solana
262
+ const savingsManager = new SVMSavingsManager(
263
+ mnemonic,
264
+ chainConfig.rpcUrl,
265
+ 0 // wallet index
266
+ )
222
267
 
223
268
  const pocket0 = savingsManager.getPocket(0)
269
+ console.log('\nPocket 0 Info:');
270
+ console.log(' Address:', pocket0.address.toBase58());
271
+ console.log(' Derivation path:', pocket0.derivationPath);
272
+ console.log(' Index:', pocket0.index);
273
+
274
+ // Get balance
275
+ const balance = await savingsManager.getPocketBalance(0, [])
276
+ console.log('\nPocket 0 Balance:');
277
+ console.log(' Native balance:', balance[0].balance.formatted, 'SOL');
278
+
279
+ // Get multiple pockets
280
+ const pocket1 = savingsManager.getPocket(1)
281
+ const pocket2 = savingsManager.getPocket(2)
282
+ console.log('\nAdditional Pockets:');
283
+ console.log(' Pocket 1:', pocket1.address.toBase58());
284
+ console.log(' Pocket 2:', pocket2.address.toBase58());
285
+
286
+ // Note: Solana addresses are DIFFERENT from EVM addresses!
287
+ console.log('\nšŸ’” Note: Solana uses coin type 501, so addresses differ from EVM');
288
+
289
+ // Cleanup
290
+ savingsManager.dispose();
291
+ console.log('\nāœ… SVM Savings Test Complete\n');
292
+ }
224
293
 
225
- // const deposited = await savingsManager.transferToPocket(walletA.wallet, pocket0.index, "0.00001")
226
-
227
- const balance = await savingsManager.getPocketTokenBalance([], 0)
228
- console.log('balance: ', balance);
294
+ // ============================================
295
+ // Multi-Chain Savings Test
296
+ // ============================================
297
+ const testSavingsPocketMultiChain = async () => {
298
+ console.log('\n========== Multi-Chain Savings Test ==========');
299
+ const mnemonic = EVMVM.generateMnemonicFromPrivateKey(evmPrivateKeyExposed)
300
+ console.log('Mnemonic:', mnemonic);
229
301
 
230
- // console.log('deposited: ', deposited);
302
+ // Create multi-chain manager with EVM and Solana
303
+ const multiChainManager = new MultiChainSavingsManager(
304
+ mnemonic,
305
+ [
306
+ {
307
+ id: 'base',
308
+ type: 'EVM',
309
+ config: evmChainConfig
310
+ },
311
+ {
312
+ id: 'ethereum',
313
+ type: 'EVM',
314
+ config: {
315
+ chainId: 1,
316
+ name: 'Ethereum',
317
+ rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/demo',
318
+ explorerUrl: 'https://etherscan.io',
319
+ nativeToken: { name: 'Ethereum', symbol: 'ETH', decimals: 18 },
320
+ confirmationNo: 1,
321
+ vmType: 'EVM'
322
+ }
323
+ },
324
+ {
325
+ id: 'solana',
326
+ type: 'SVM',
327
+ config: {
328
+ rpcUrl: chainConfig.rpcUrl
329
+ }
330
+ }
331
+ ],
332
+ 0 // wallet index
333
+ );
334
+
335
+ console.log('\nAvailable chains:', multiChainManager.getChains());
336
+ console.log('EVM chains:', multiChainManager.getEVMChains());
337
+ console.log('SVM chains:', multiChainManager.getSVMChains());
338
+
339
+ // Get pocket 0 address on different chains
340
+ console.log('\nPocket 0 Addresses Across Chains:');
341
+ const baseAddress = multiChainManager.getPocketAddress('base', 0);
342
+ const ethAddress = multiChainManager.getPocketAddress('ethereum', 0);
343
+ const solAddress = multiChainManager.getPocketAddress('solana', 0);
344
+
345
+ console.log(' Base:', baseAddress);
346
+ console.log(' Ethereum:', ethAddress);
347
+ console.log(' Solana:', solAddress);
348
+
349
+ // Verify EVM chains share addresses
350
+ console.log('\nšŸ” EVM Address Check:');
351
+ console.log(' Base === Ethereum?', baseAddress === ethAddress, 'āœ…');
352
+ console.log(' Base === Solana?', baseAddress === solAddress, 'āŒ (different chain type)');
353
+
354
+ // Get balances across chains
355
+ console.log('\nšŸ“Š Getting balances across all chains...');
356
+ try {
357
+ const balances = await multiChainManager.getPocketBalanceAcrossChains(
358
+ 0, // pocket index
359
+ new Map([
360
+ ['base', []], // No tokens, just native
361
+ ['ethereum', []],
362
+ ['solana', []]
363
+ ])
364
+ );
365
+
366
+ console.log('\nBalances for Pocket 0:');
367
+ balances.forEach(chainBalance => {
368
+ console.log(`\n ${chainBalance.chainId.toUpperCase()} (${chainBalance.chainType}):`);
369
+ console.log(` Address: ${chainBalance.address}`);
370
+ chainBalance.balances.forEach(bal => {
371
+ const tokenName = bal.token === 'native' ? 'Native' : bal.token;
372
+ console.log(` ${tokenName}: ${bal.balance.formatted}`);
373
+ });
374
+ });
375
+ } catch (error) {
376
+ console.log(' āš ļø Balance fetch error (expected for demo RPCs):', (error as Error).message);
377
+ }
231
378
 
379
+ // Get specific chain managers for advanced operations
380
+ console.log('\nšŸ”§ Advanced: Direct chain manager access');
381
+ const baseManager = multiChainManager.getEVMManager('base');
382
+ const solanaManager = multiChainManager.getSVMManager('solana');
383
+ console.log(' Base manager type:', baseManager.constructor.name);
384
+ console.log(' Solana manager type:', solanaManager.constructor.name);
232
385
 
386
+ // Cleanup
387
+ multiChainManager.dispose();
388
+ console.log('\nāœ… Multi-Chain Savings Test Complete\n');
233
389
  }
234
390
 
391
+ // Uncomment to run tests
235
392
  // testSavingsPocket()
393
+ // testSavingsPocketSVM()
394
+ // testSavingsPocketMultiChain()
236
395
 
237
396
  // testPrice()
238
397