@deserialize/multi-vm-wallet 1.5.35 → 1.6.1

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.
@@ -44,7 +44,7 @@ import { EVMDeriveChildPrivateKey, mnemonicToSeed } from "../walletBip32";
44
44
  export interface TransferFromPocketOptions {
45
45
  /** Wallet index in derivation path (default: 0) */
46
46
  walletIndex?: number;
47
- /** Account/pocket index to transfer from (1-based, 0 is main wallet) */
47
+ /** Pocket index to transfer from (0-based) */
48
48
  accountIndex: number;
49
49
  /** Destination address */
50
50
  to: string;
@@ -76,7 +76,7 @@ export interface TransferTokenFromPocketOptions extends Omit<TransferFromPocketO
76
76
  export interface GetPocketBalanceOptions {
77
77
  /** Wallet index in derivation path (default: 0) */
78
78
  walletIndex?: number;
79
- /** Account/pocket index (1-based, 0 is main wallet) */
79
+ /** Pocket index (0-based) */
80
80
  accountIndex: number;
81
81
  }
82
82
 
@@ -129,7 +129,7 @@ export class SavingsOperations {
129
129
  * Derives a savings pocket wallet from mnemonic
130
130
  *
131
131
  * @param mnemonic - BIP-39 mnemonic phrase
132
- * @param accountIndex - Account/pocket index (1-based, 0 is main wallet)
132
+ * @param accountIndex - Pocket index (0-based)
133
133
  * @param walletIndex - Wallet index (default: 0)
134
134
  * @param provider - RPC provider to connect wallet to
135
135
  * @returns Derived wallet, address, and cleanup function
@@ -137,7 +137,7 @@ export class SavingsOperations {
137
137
  * @remarks
138
138
  * IMPORTANT: Always call cleanup() when done with the wallet to zero out private key
139
139
  *
140
- * Derivation path: m/44'/60'/{accountIndex}'/0/{walletIndex}'
140
+ * Derivation path: m/44'/60'/{accountIndex + 1}'/0/{walletIndex}'
141
141
  *
142
142
  * @throws Error if validation fails
143
143
  *
@@ -154,9 +154,10 @@ export class SavingsOperations {
154
154
  SavingsValidation.validateAccountIndex(accountIndex);
155
155
  SavingsValidation.validateWalletIndex(walletIndex);
156
156
 
157
- // Derive pocket private key
157
+ // Derive pocket private key (account 0 is main wallet, so pockets start at +1)
158
+ const pocketIndex = accountIndex + 1;
158
159
  const seed = mnemonicToSeed(mnemonic);
159
- const { privateKey } = EVMDeriveChildPrivateKey(seed, walletIndex, `m/44'/60'/${accountIndex}'/`);
160
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, walletIndex, `m/44'/60'/${pocketIndex}'/0/`);
160
161
 
161
162
  // Create wallet
162
163
  const wallet = new Wallet(privateKey, provider);
@@ -473,7 +474,7 @@ export class SavingsOperations {
473
474
  * Get the address of a savings pocket without storing credentials
474
475
  *
475
476
  * @param mnemonic - BIP-39 mnemonic phrase
476
- * @param accountIndex - Account/pocket index
477
+ * @param accountIndex - Pocket index (0-based)
477
478
  * @param walletIndex - Wallet index (default: 0)
478
479
  * @returns Pocket address
479
480
  *
@@ -495,9 +496,10 @@ export class SavingsOperations {
495
496
  SavingsValidation.validateAccountIndex(accountIndex);
496
497
  SavingsValidation.validateWalletIndex(walletIndex);
497
498
 
498
- // Derive address
499
+ // Derive address (account 0 is main wallet, so pockets start at +1)
500
+ const pocketIndex = accountIndex + 1;
499
501
  const seed = mnemonicToSeed(mnemonic);
500
- const { privateKey } = EVMDeriveChildPrivateKey(seed, walletIndex, `m/44'/60'/${accountIndex}'/`);
502
+ const { privateKey } = EVMDeriveChildPrivateKey(seed, walletIndex, `m/44'/60'/${pocketIndex}'/0/`);
501
503
  const wallet = new Wallet(privateKey);
502
504
  const address = wallet.address;
503
505
 
@@ -98,6 +98,13 @@ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Key
98
98
  this._client = undefined;
99
99
  }
100
100
 
101
+ private toSafeNumberAmount(amount: bigint, label: string): number {
102
+ if (amount > BigInt(Number.MAX_SAFE_INTEGER)) {
103
+ throw new Error(`${label} exceeds Number.MAX_SAFE_INTEGER and cannot be represented safely: ${amount}`);
104
+ }
105
+ return Number(amount);
106
+ }
107
+
101
108
  /**
102
109
  * Derive a savings pocket at the specified account index
103
110
  *
@@ -110,9 +117,10 @@ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Key
110
117
 
111
118
  // Add 1 to preserve index 0 for main wallet
112
119
  const pocketIndex = accountIndex + 1;
113
- const derivationPath = `${this.derivationPathBase}${pocketIndex}'/0/${this.walletIndex}`;
120
+ const derivationPathBase = `${this.derivationPathBase}${pocketIndex}'/0/`;
121
+ const derivationPath = `${derivationPathBase}${this.walletIndex}'`;
114
122
  const seed = mnemonicToSeed(this.mnemonic);
115
- const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
123
+ const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPathBase);
116
124
 
117
125
  const pocket: Pocket<PublicKey> = {
118
126
  privateKey: keypair,
@@ -132,9 +140,10 @@ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Key
132
140
  */
133
141
  getMainWallet() {
134
142
  this.checkNotDisposed();
135
- const derivationPath = `${this.derivationPathBase}0'/0/${this.walletIndex}`;
143
+ const derivationPathBase = `${this.derivationPathBase}0'/0/`;
144
+ const derivationPath = `${derivationPathBase}${this.walletIndex}'`;
136
145
  const seed = mnemonicToSeed(this.mnemonic);
137
- const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPath);
146
+ const keypair = SVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPathBase);
138
147
 
139
148
  return {
140
149
  privateKey: keypair,
@@ -262,7 +271,7 @@ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Key
262
271
  const tx = await getTransferNativeTransaction(
263
272
  mainWallet,
264
273
  pocket.address,
265
- Number(amount),
274
+ this.toSafeNumberAmount(amount, 'Native transfer amount'),
266
275
  this.client
267
276
  );
268
277
 
@@ -297,7 +306,7 @@ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Key
297
306
  mainWallet,
298
307
  pocket.address,
299
308
  tokenInfo as any, // TokenInfo type
300
- Number(amount),
309
+ this.toSafeNumberAmount(amount, 'Token transfer amount'),
301
310
  this.client
302
311
  );
303
312
 
@@ -365,7 +374,7 @@ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Key
365
374
  const tx = await getTransferNativeTransaction(
366
375
  pocket.privateKey,
367
376
  mainWalletAddress,
368
- Number(amount),
377
+ this.toSafeNumberAmount(amount, 'Native withdrawal amount'),
369
378
  this.client
370
379
  );
371
380
  const hash = await signAndSendTransaction(tx, this.client, pocket.privateKey);
@@ -376,7 +385,7 @@ export class SVMSavingsManager extends SavingsManager<PublicKey, Connection, Key
376
385
  pocket.privateKey,
377
386
  mainWalletAddress,
378
387
  token as any, // TokenInfo type
379
- Number(amount),
388
+ this.toSafeNumberAmount(amount, 'Token withdrawal amount'),
380
389
  this.client
381
390
  );
382
391
 
package/utils/test.ts CHANGED
@@ -399,7 +399,7 @@ const testAddressClass = async () => {
399
399
  const evmAddressClass = new EVMChainAddress(evmChainConfig, "0xC9C1D854b82BA9b4FB6f6D58E9EF3d1fAEd601AA", 0)
400
400
  const res = await evmAddressClass.getNativeBalance()
401
401
  console.log('res: ', res);
402
-
402
+ SVMVM.generateMnemonicFromPrivateKey
403
403
  // const svmAddressClass = new SVMChainAddress( )
404
404
  }
405
405
  testAddressClass()
package/utils/types.ts CHANGED
@@ -25,7 +25,8 @@ export interface ChainWalletConfig {
25
25
  }
26
26
 
27
27
  export interface ChainSavingConfig {
28
- tokens: string[]; // List of token addresses to track for savings
28
+ supported: boolean; // Whether savings tracking is supported on this chain
29
+ tokens: TokenInfo[]; // List of token addresses to track for savings
29
30
 
30
31
  }
31
32
 
package/utils/utils.ts CHANGED
@@ -1,7 +1,13 @@
1
- import { EVMVM, getNativeBalance } from "./evm";
1
+ import { EVMVM } from "./evm";
2
2
  import { SVMVM } from "./svm";
3
3
  import { ChainWalletConfig, vmTypes } from "./types";
4
4
  import { sha256 } from "@noble/hashes/sha2";
5
+ import { Connection, PublicKey } from "@solana/web3.js";
6
+ import { createPublicClientFromChainConfig, discoverTokens as discoverEvmTokens, getNativeBalance as getEvmNativeBalance, getTokenBalance as getEvmTokenBalance } from "./evm/utils";
7
+ import { discoverTokens as discoverSvmTokens, getSvmNativeBalance } from "./svm/utils";
8
+ import { Hex } from "viem";
9
+ import BN from "bn.js";
10
+ import { fetchPrices } from "./price";
5
11
 
6
12
 
7
13
  export const getPrivateKeyFromAnother = (privateKey: any, fromVm: vmTypes, toVm: vmTypes) => {
@@ -20,8 +26,248 @@ export const getPrivateKeyFromAnother = (privateKey: any, fromVm: vmTypes, toVm:
20
26
  }
21
27
 
22
28
 
23
- //TODO:
29
+ export interface AddressPortfolioItem {
30
+ tokenAddress: string | "native";
31
+ symbol: string;
32
+ decimals: number;
33
+ balanceRaw: string;
34
+ balanceFormatted: number;
35
+ priceUsd: number | null;
36
+ valueUsd: number | null;
37
+ }
38
+
39
+ export interface AddressPortfolioResult {
40
+ address: string;
41
+ vmType: vmTypes;
42
+ chainId: number;
43
+ items: AddressPortfolioItem[];
44
+ totals: {
45
+ valueUsd: number;
46
+ pricedItems: number;
47
+ unpricedItems: number;
48
+ };
49
+ }
50
+
51
+ export interface AddressPortfolioParams {
52
+ chain: ChainWalletConfig;
53
+ address: string;
54
+ vmType?: vmTypes;
55
+ includeNative?: boolean;
56
+ tokenAddresses?: string[];
57
+ }
58
+
59
+ const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
60
+
61
+ export const detectVmTypeFromAddress = (address: string): vmTypes => {
62
+ if (EVM_ADDRESS_REGEX.test(address)) {
63
+ return "EVM";
64
+ }
65
+
66
+ try {
67
+ new PublicKey(address);
68
+ return "SVM";
69
+ } catch {
70
+ throw new Error(`Could not infer VM type from address: ${address}`);
71
+ }
72
+ };
73
+
74
+ const normalizePriceMap = (prices: Array<{ tokenAddress: string; price: number }>) => {
75
+ const priceMap = new Map<string, number>();
76
+ for (const item of prices) {
77
+ if (!item?.tokenAddress || typeof item.price !== "number") continue;
78
+ priceMap.set(item.tokenAddress.toLowerCase(), item.price);
79
+ }
80
+ return priceMap;
81
+ };
82
+
83
+ const enrichWithUsdValues = (
84
+ items: Omit<AddressPortfolioItem, "priceUsd" | "valueUsd">[],
85
+ priceMap: Map<string, number>
86
+ ): AddressPortfolioResult["items"] => {
87
+ return items.map((item) => {
88
+ const key = String(item.tokenAddress).toLowerCase();
89
+ const priceUsd = priceMap.get(key) ?? null;
90
+ const valueUsd = priceUsd === null ? null : item.balanceFormatted * priceUsd;
91
+
92
+ return {
93
+ ...item,
94
+ priceUsd,
95
+ valueUsd,
96
+ };
97
+ });
98
+ };
99
+
100
+ const buildTotals = (items: AddressPortfolioItem[]) => {
101
+ let valueUsd = 0;
102
+ let pricedItems = 0;
103
+ let unpricedItems = 0;
104
+
105
+ for (const item of items) {
106
+ if (item.valueUsd === null) {
107
+ unpricedItems += 1;
108
+ continue;
109
+ }
110
+ pricedItems += 1;
111
+ valueUsd += item.valueUsd;
112
+ }
113
+
114
+ return { valueUsd, pricedItems, unpricedItems };
115
+ };
116
+
117
+ export const getAddressPortfolioValue = async (params: AddressPortfolioParams): Promise<AddressPortfolioResult> => {
118
+ const { chain, address, includeNative = true, tokenAddresses } = params;
119
+ const vmType = params.vmType ?? detectVmTypeFromAddress(address);
120
+
121
+ if (vmType === "EVM" && !EVM_ADDRESS_REGEX.test(address)) {
122
+ throw new Error(`Invalid EVM address: ${address}`);
123
+ }
124
+ if (vmType === "SVM") {
125
+ // Throws if invalid
126
+ new PublicKey(address);
127
+ }
128
+
129
+ if (vmType === "EVM") {
130
+ const client = createPublicClientFromChainConfig(chain);
131
+
132
+ const items: Omit<AddressPortfolioItem, "priceUsd" | "valueUsd">[] = [];
133
+ const priceTargets = new Set<string>();
134
+
135
+ if (includeNative) {
136
+ const nativeBalance = await getEvmNativeBalance(address as Hex, client);
137
+ const nativeRaw = nativeBalance.balance.toString();
138
+ items.push({
139
+ tokenAddress: "native",
140
+ symbol: chain.nativeToken.symbol,
141
+ decimals: chain.nativeToken.decimals,
142
+ balanceRaw: nativeRaw,
143
+ balanceFormatted: nativeBalance.formatted,
144
+ });
145
+ priceTargets.add("native");
146
+ }
147
+
148
+ if (tokenAddresses && tokenAddresses.length > 0) {
149
+ const tokenResults = await Promise.all(
150
+ tokenAddresses.map(async (tokenAddress) => {
151
+ const balance = await getEvmTokenBalance(tokenAddress as Hex, address as Hex, client);
152
+ const raw = balance.balance.toString();
153
+ return {
154
+ tokenAddress,
155
+ symbol: tokenAddress,
156
+ decimals: balance.decimal,
157
+ balanceRaw: raw,
158
+ balanceFormatted: balance.formatted,
159
+ };
160
+ })
161
+ );
162
+ for (const token of tokenResults) {
163
+ items.push(token);
164
+ priceTargets.add(token.tokenAddress);
165
+ }
166
+ } else {
167
+ const discovered = await discoverEvmTokens(address, chain);
168
+ for (const token of discovered) {
169
+ const raw = String(token.balance);
170
+ const decimals = token.decimals ?? 0;
171
+ const rawBn = new BN(raw);
172
+ const divisor = Math.pow(10, decimals);
173
+ const formatted = divisor === 0 ? 0 : Number(rawBn.toString()) / divisor;
174
+
175
+ items.push({
176
+ tokenAddress: token.address,
177
+ symbol: token.symbol,
178
+ decimals,
179
+ balanceRaw: raw,
180
+ balanceFormatted: formatted,
181
+ });
182
+ priceTargets.add(token.address);
183
+ }
184
+ }
185
+
186
+ const priceResult = await fetchPrices({
187
+ vm: "EVM",
188
+ chainId: chain.chainId,
189
+ tokenAddresses: Array.from(priceTargets),
190
+ });
191
+ const priceMap = normalizePriceMap(priceResult.data?.prices ?? []);
192
+ const enrichedItems = enrichWithUsdValues(items, priceMap);
193
+
194
+ return {
195
+ address,
196
+ vmType,
197
+ chainId: chain.chainId,
198
+ items: enrichedItems,
199
+ totals: buildTotals(enrichedItems),
200
+ };
201
+ }
202
+
203
+ const svmAddress = new PublicKey(address);
204
+ const connection = new Connection(chain.rpcUrl);
205
+ const items: Omit<AddressPortfolioItem, "priceUsd" | "valueUsd">[] = [];
206
+ const priceTargets = new Set<string>();
207
+
208
+ if (includeNative) {
209
+ const nativeBalance = await getSvmNativeBalance(svmAddress, connection);
210
+ items.push({
211
+ tokenAddress: "native",
212
+ symbol: chain.nativeToken.symbol,
213
+ decimals: chain.nativeToken.decimals,
214
+ balanceRaw: nativeBalance.balance.toString(),
215
+ balanceFormatted: nativeBalance.formatted,
216
+ });
217
+ priceTargets.add("native");
218
+ }
219
+
220
+ if (tokenAddresses && tokenAddresses.length > 0) {
221
+ const tokenResults = await Promise.all(
222
+ tokenAddresses.map(async (tokenAddress) => {
223
+ const pubkey = new PublicKey(tokenAddress);
224
+ const tokenBalance = await SVMVM.getTokenBalance(svmAddress, pubkey, connection);
225
+ return {
226
+ tokenAddress,
227
+ symbol: tokenAddress,
228
+ decimals: tokenBalance.decimal,
229
+ balanceRaw: tokenBalance.balance.toString(),
230
+ balanceFormatted: tokenBalance.formatted,
231
+ };
232
+ })
233
+ );
234
+ for (const token of tokenResults) {
235
+ items.push(token);
236
+ priceTargets.add(token.tokenAddress);
237
+ }
238
+ } else {
239
+ const discovered = await discoverSvmTokens(svmAddress, connection);
240
+ for (const token of discovered) {
241
+ const raw = String(token.balance);
242
+ const decimals = token.decimals ?? 0;
243
+ const rawBn = new BN(raw);
244
+ const divisor = Math.pow(10, decimals);
245
+ const formatted = divisor === 0 ? 0 : Number(rawBn.toString()) / divisor;
246
+
247
+ items.push({
248
+ tokenAddress: token.address,
249
+ symbol: token.symbol,
250
+ decimals,
251
+ balanceRaw: raw,
252
+ balanceFormatted: formatted,
253
+ });
254
+ priceTargets.add(token.address);
255
+ }
256
+ }
24
257
 
25
- // create a unified evm svm balan e querying function
26
- //create a chain wallet class that takes address and chain config and probably vm
258
+ const priceResult = await fetchPrices({
259
+ vm: "SVM",
260
+ chainId: chain.chainId,
261
+ tokenAddresses: Array.from(priceTargets),
262
+ });
263
+ const priceMap = normalizePriceMap(priceResult.data?.prices ?? []);
264
+ const enrichedItems = enrichWithUsdValues(items, priceMap);
27
265
 
266
+ return {
267
+ address,
268
+ vmType,
269
+ chainId: chain.chainId,
270
+ items: enrichedItems,
271
+ totals: buildTotals(enrichedItems),
272
+ };
273
+ };
@@ -137,7 +137,10 @@ export function EVMDeriveChildPrivateKey(seed: string, index: number, derivation
137
137
  VMValidation.validateIndex(index, 'Wallet index');
138
138
  // VMValidation.validateDerivationPath(derivationPath + index + "'", 'EVM');
139
139
 
140
- const path = `${derivationPath}${index}'`;
140
+ // MetaMask-compatible EVM path uses unhardened address index:
141
+ // m/44'/60'/account'/change/address_index
142
+ // If a full path is passed (no trailing slash), use it as-is.
143
+ const path = derivationPath.endsWith("/") ? `${derivationPath}${index}` : derivationPath;
141
144
  const scureNode = HDKey.fromMasterSeed(Buffer.from(seed, "hex"));
142
145
  const child = scureNode.derive(path);
143
146
 
@@ -218,4 +221,4 @@ function hardenedDerivation(
218
221
  key: hmacResult.slice(0, 32), // Left 32 bytes
219
222
  chainCode: hmacResult.slice(32, 64) // Right 32 bytes
220
223
  };
221
- }
224
+ }