@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.
- package/dist/constant.js +54 -4
- package/dist/evm/evm.js +1 -1
- package/dist/savings/evm-savings.js +6 -4
- package/dist/savings/multi-chain-savings.d.ts +4 -0
- package/dist/savings/multi-chain-savings.js +51 -7
- package/dist/savings/saving-manager.d.ts +1 -1
- package/dist/savings/saving-manager.js +13 -14
- package/dist/savings/savings-operations.js +4 -2
- package/dist/savings/svm-savings.d.ts +1 -0
- package/dist/savings/svm-savings.js +16 -8
- package/dist/test.js +2 -0
- package/dist/types.d.ts +2 -1
- package/dist/utils.d.ts +30 -1
- package/dist/utils.js +199 -1
- package/dist/walletBip32.js +1 -1
- package/package.json +1 -1
- package/utils/constant.ts +58 -4
- package/utils/evm/evm.ts +1 -1
- package/utils/savings/evm-savings.ts +6 -4
- package/utils/savings/multi-chain-savings.ts +71 -8
- package/utils/savings/saving-manager.ts +20 -20
- package/utils/savings/savings-operations.ts +11 -9
- package/utils/savings/svm-savings.ts +17 -8
- package/utils/test.ts +1 -1
- package/utils/types.ts +2 -1
- package/utils/utils.ts +250 -4
- package/utils/walletBip32.ts +5 -2
package/dist/utils.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getPrivateKeyFromAnother = void 0;
|
|
6
|
+
exports.getAddressPortfolioValue = exports.detectVmTypeFromAddress = exports.getPrivateKeyFromAnother = void 0;
|
|
4
7
|
const evm_1 = require("./evm");
|
|
5
8
|
const svm_1 = require("./svm");
|
|
6
9
|
const sha2_1 = require("@noble/hashes/sha2");
|
|
10
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
11
|
+
const utils_1 = require("./evm/utils");
|
|
12
|
+
const utils_2 = require("./svm/utils");
|
|
13
|
+
const bn_js_1 = __importDefault(require("bn.js"));
|
|
14
|
+
const price_1 = require("./price");
|
|
7
15
|
const getPrivateKeyFromAnother = (privateKey, fromVm, toVm) => {
|
|
8
16
|
if (fromVm === "EVM") {
|
|
9
17
|
if (toVm === "SVM") {
|
|
@@ -20,3 +28,193 @@ const getPrivateKeyFromAnother = (privateKey, fromVm, toVm) => {
|
|
|
20
28
|
}
|
|
21
29
|
};
|
|
22
30
|
exports.getPrivateKeyFromAnother = getPrivateKeyFromAnother;
|
|
31
|
+
const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
|
|
32
|
+
const detectVmTypeFromAddress = (address) => {
|
|
33
|
+
if (EVM_ADDRESS_REGEX.test(address)) {
|
|
34
|
+
return "EVM";
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
new web3_js_1.PublicKey(address);
|
|
38
|
+
return "SVM";
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`Could not infer VM type from address: ${address}`);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
exports.detectVmTypeFromAddress = detectVmTypeFromAddress;
|
|
45
|
+
const normalizePriceMap = (prices) => {
|
|
46
|
+
const priceMap = new Map();
|
|
47
|
+
for (const item of prices) {
|
|
48
|
+
if (!item?.tokenAddress || typeof item.price !== "number")
|
|
49
|
+
continue;
|
|
50
|
+
priceMap.set(item.tokenAddress.toLowerCase(), item.price);
|
|
51
|
+
}
|
|
52
|
+
return priceMap;
|
|
53
|
+
};
|
|
54
|
+
const enrichWithUsdValues = (items, priceMap) => {
|
|
55
|
+
return items.map((item) => {
|
|
56
|
+
const key = String(item.tokenAddress).toLowerCase();
|
|
57
|
+
const priceUsd = priceMap.get(key) ?? null;
|
|
58
|
+
const valueUsd = priceUsd === null ? null : item.balanceFormatted * priceUsd;
|
|
59
|
+
return {
|
|
60
|
+
...item,
|
|
61
|
+
priceUsd,
|
|
62
|
+
valueUsd,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const buildTotals = (items) => {
|
|
67
|
+
let valueUsd = 0;
|
|
68
|
+
let pricedItems = 0;
|
|
69
|
+
let unpricedItems = 0;
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (item.valueUsd === null) {
|
|
72
|
+
unpricedItems += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
pricedItems += 1;
|
|
76
|
+
valueUsd += item.valueUsd;
|
|
77
|
+
}
|
|
78
|
+
return { valueUsd, pricedItems, unpricedItems };
|
|
79
|
+
};
|
|
80
|
+
const getAddressPortfolioValue = async (params) => {
|
|
81
|
+
const { chain, address, includeNative = true, tokenAddresses } = params;
|
|
82
|
+
const vmType = params.vmType ?? (0, exports.detectVmTypeFromAddress)(address);
|
|
83
|
+
if (vmType === "EVM" && !EVM_ADDRESS_REGEX.test(address)) {
|
|
84
|
+
throw new Error(`Invalid EVM address: ${address}`);
|
|
85
|
+
}
|
|
86
|
+
if (vmType === "SVM") {
|
|
87
|
+
new web3_js_1.PublicKey(address);
|
|
88
|
+
}
|
|
89
|
+
if (vmType === "EVM") {
|
|
90
|
+
const client = (0, utils_1.createPublicClientFromChainConfig)(chain);
|
|
91
|
+
const items = [];
|
|
92
|
+
const priceTargets = new Set();
|
|
93
|
+
if (includeNative) {
|
|
94
|
+
const nativeBalance = await (0, utils_1.getNativeBalance)(address, client);
|
|
95
|
+
const nativeRaw = nativeBalance.balance.toString();
|
|
96
|
+
items.push({
|
|
97
|
+
tokenAddress: "native",
|
|
98
|
+
symbol: chain.nativeToken.symbol,
|
|
99
|
+
decimals: chain.nativeToken.decimals,
|
|
100
|
+
balanceRaw: nativeRaw,
|
|
101
|
+
balanceFormatted: nativeBalance.formatted,
|
|
102
|
+
});
|
|
103
|
+
priceTargets.add("native");
|
|
104
|
+
}
|
|
105
|
+
if (tokenAddresses && tokenAddresses.length > 0) {
|
|
106
|
+
const tokenResults = await Promise.all(tokenAddresses.map(async (tokenAddress) => {
|
|
107
|
+
const balance = await (0, utils_1.getTokenBalance)(tokenAddress, address, client);
|
|
108
|
+
const raw = balance.balance.toString();
|
|
109
|
+
return {
|
|
110
|
+
tokenAddress,
|
|
111
|
+
symbol: tokenAddress,
|
|
112
|
+
decimals: balance.decimal,
|
|
113
|
+
balanceRaw: raw,
|
|
114
|
+
balanceFormatted: balance.formatted,
|
|
115
|
+
};
|
|
116
|
+
}));
|
|
117
|
+
for (const token of tokenResults) {
|
|
118
|
+
items.push(token);
|
|
119
|
+
priceTargets.add(token.tokenAddress);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const discovered = await (0, utils_1.discoverTokens)(address, chain);
|
|
124
|
+
for (const token of discovered) {
|
|
125
|
+
const raw = String(token.balance);
|
|
126
|
+
const decimals = token.decimals ?? 0;
|
|
127
|
+
const rawBn = new bn_js_1.default(raw);
|
|
128
|
+
const divisor = Math.pow(10, decimals);
|
|
129
|
+
const formatted = divisor === 0 ? 0 : Number(rawBn.toString()) / divisor;
|
|
130
|
+
items.push({
|
|
131
|
+
tokenAddress: token.address,
|
|
132
|
+
symbol: token.symbol,
|
|
133
|
+
decimals,
|
|
134
|
+
balanceRaw: raw,
|
|
135
|
+
balanceFormatted: formatted,
|
|
136
|
+
});
|
|
137
|
+
priceTargets.add(token.address);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const priceResult = await (0, price_1.fetchPrices)({
|
|
141
|
+
vm: "EVM",
|
|
142
|
+
chainId: chain.chainId,
|
|
143
|
+
tokenAddresses: Array.from(priceTargets),
|
|
144
|
+
});
|
|
145
|
+
const priceMap = normalizePriceMap(priceResult.data?.prices ?? []);
|
|
146
|
+
const enrichedItems = enrichWithUsdValues(items, priceMap);
|
|
147
|
+
return {
|
|
148
|
+
address,
|
|
149
|
+
vmType,
|
|
150
|
+
chainId: chain.chainId,
|
|
151
|
+
items: enrichedItems,
|
|
152
|
+
totals: buildTotals(enrichedItems),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const svmAddress = new web3_js_1.PublicKey(address);
|
|
156
|
+
const connection = new web3_js_1.Connection(chain.rpcUrl);
|
|
157
|
+
const items = [];
|
|
158
|
+
const priceTargets = new Set();
|
|
159
|
+
if (includeNative) {
|
|
160
|
+
const nativeBalance = await (0, utils_2.getSvmNativeBalance)(svmAddress, connection);
|
|
161
|
+
items.push({
|
|
162
|
+
tokenAddress: "native",
|
|
163
|
+
symbol: chain.nativeToken.symbol,
|
|
164
|
+
decimals: chain.nativeToken.decimals,
|
|
165
|
+
balanceRaw: nativeBalance.balance.toString(),
|
|
166
|
+
balanceFormatted: nativeBalance.formatted,
|
|
167
|
+
});
|
|
168
|
+
priceTargets.add("native");
|
|
169
|
+
}
|
|
170
|
+
if (tokenAddresses && tokenAddresses.length > 0) {
|
|
171
|
+
const tokenResults = await Promise.all(tokenAddresses.map(async (tokenAddress) => {
|
|
172
|
+
const pubkey = new web3_js_1.PublicKey(tokenAddress);
|
|
173
|
+
const tokenBalance = await svm_1.SVMVM.getTokenBalance(svmAddress, pubkey, connection);
|
|
174
|
+
return {
|
|
175
|
+
tokenAddress,
|
|
176
|
+
symbol: tokenAddress,
|
|
177
|
+
decimals: tokenBalance.decimal,
|
|
178
|
+
balanceRaw: tokenBalance.balance.toString(),
|
|
179
|
+
balanceFormatted: tokenBalance.formatted,
|
|
180
|
+
};
|
|
181
|
+
}));
|
|
182
|
+
for (const token of tokenResults) {
|
|
183
|
+
items.push(token);
|
|
184
|
+
priceTargets.add(token.tokenAddress);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const discovered = await (0, utils_2.discoverTokens)(svmAddress, connection);
|
|
189
|
+
for (const token of discovered) {
|
|
190
|
+
const raw = String(token.balance);
|
|
191
|
+
const decimals = token.decimals ?? 0;
|
|
192
|
+
const rawBn = new bn_js_1.default(raw);
|
|
193
|
+
const divisor = Math.pow(10, decimals);
|
|
194
|
+
const formatted = divisor === 0 ? 0 : Number(rawBn.toString()) / divisor;
|
|
195
|
+
items.push({
|
|
196
|
+
tokenAddress: token.address,
|
|
197
|
+
symbol: token.symbol,
|
|
198
|
+
decimals,
|
|
199
|
+
balanceRaw: raw,
|
|
200
|
+
balanceFormatted: formatted,
|
|
201
|
+
});
|
|
202
|
+
priceTargets.add(token.address);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const priceResult = await (0, price_1.fetchPrices)({
|
|
206
|
+
vm: "SVM",
|
|
207
|
+
chainId: chain.chainId,
|
|
208
|
+
tokenAddresses: Array.from(priceTargets),
|
|
209
|
+
});
|
|
210
|
+
const priceMap = normalizePriceMap(priceResult.data?.prices ?? []);
|
|
211
|
+
const enrichedItems = enrichWithUsdValues(items, priceMap);
|
|
212
|
+
return {
|
|
213
|
+
address,
|
|
214
|
+
vmType,
|
|
215
|
+
chainId: chain.chainId,
|
|
216
|
+
items: enrichedItems,
|
|
217
|
+
totals: buildTotals(enrichedItems),
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
exports.getAddressPortfolioValue = getAddressPortfolioValue;
|
package/dist/walletBip32.js
CHANGED
|
@@ -87,7 +87,7 @@ function GenerateSeed(_mnemonic) {
|
|
|
87
87
|
function EVMDeriveChildPrivateKey(seed, index, derivationPath) {
|
|
88
88
|
vm_validation_1.VMValidation.validateSeed(seed);
|
|
89
89
|
vm_validation_1.VMValidation.validateIndex(index, 'Wallet index');
|
|
90
|
-
const path = `${derivationPath}${index}
|
|
90
|
+
const path = derivationPath.endsWith("/") ? `${derivationPath}${index}` : derivationPath;
|
|
91
91
|
const scureNode = bip32_1.HDKey.fromMasterSeed(buffer_1.Buffer.from(seed, "hex"));
|
|
92
92
|
const child = scureNode.derive(path);
|
|
93
93
|
if (!child.privateKey) {
|
package/package.json
CHANGED
package/utils/constant.ts
CHANGED
|
@@ -15,7 +15,21 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
15
15
|
logoUrl: "https://solana.com/src/img/branding/solanaLogoMark.svg",
|
|
16
16
|
vmType: "SVM",
|
|
17
17
|
savings: {
|
|
18
|
-
|
|
18
|
+
supported: true,
|
|
19
|
+
tokens: [
|
|
20
|
+
{
|
|
21
|
+
name: "USDC",
|
|
22
|
+
symbol: "USDC",
|
|
23
|
+
decimals: 6,
|
|
24
|
+
address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "USDT",
|
|
28
|
+
symbol: "USDT",
|
|
29
|
+
decimals: 6,
|
|
30
|
+
address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
19
33
|
}
|
|
20
34
|
}
|
|
21
35
|
|
|
@@ -34,6 +48,7 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
34
48
|
logoUrl: "https://push.org/assets/website/favicons/favicon.svg",
|
|
35
49
|
vmType: "EVM",
|
|
36
50
|
savings: {
|
|
51
|
+
supported: false,
|
|
37
52
|
tokens: []
|
|
38
53
|
}
|
|
39
54
|
}
|
|
@@ -53,7 +68,24 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
53
68
|
},
|
|
54
69
|
testnet: false,
|
|
55
70
|
logoUrl: "https://etherscan.io/images/svg/brands/ethereum-original-light.svg",
|
|
56
|
-
vmType: "EVM"
|
|
71
|
+
vmType: "EVM",
|
|
72
|
+
savings: {
|
|
73
|
+
supported: true,
|
|
74
|
+
tokens: [{
|
|
75
|
+
name: "USDC",
|
|
76
|
+
symbol: "USDC",
|
|
77
|
+
decimals: 6,
|
|
78
|
+
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "USDT",
|
|
82
|
+
symbol: "USDT",
|
|
83
|
+
decimals: 6,
|
|
84
|
+
address: "0xdac17f958d2ee523a2206206994597c13d831ec7"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
]
|
|
88
|
+
}
|
|
57
89
|
}, {
|
|
58
90
|
chainId: 56,
|
|
59
91
|
name: "BSC",
|
|
@@ -68,7 +100,18 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
68
100
|
logoUrl: "https://bscscan.com/assets/bsc/images/svg/logos/token-light.svg?v=25.10.5.0",
|
|
69
101
|
vmType: "EVM",
|
|
70
102
|
savings: {
|
|
71
|
-
|
|
103
|
+
supported: true,
|
|
104
|
+
tokens: [
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
name: "BUSD",
|
|
108
|
+
symbol: "BUSD",
|
|
109
|
+
decimals: 6,
|
|
110
|
+
address: "0x55d398326f99059fF775485246999027B3197955"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
]
|
|
72
115
|
}
|
|
73
116
|
},
|
|
74
117
|
|
|
@@ -101,7 +144,15 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
101
144
|
logoUrl: "https://avatars.githubusercontent.com/u/108554348?s=200&v=4",
|
|
102
145
|
vmType: "EVM",
|
|
103
146
|
savings: {
|
|
104
|
-
|
|
147
|
+
supported: true,
|
|
148
|
+
tokens: [
|
|
149
|
+
{
|
|
150
|
+
name: "USDC",
|
|
151
|
+
symbol: "USDC",
|
|
152
|
+
decimals: 6,
|
|
153
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
105
156
|
}
|
|
106
157
|
},
|
|
107
158
|
|
|
@@ -119,6 +170,7 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
119
170
|
logoUrl: "https://avatars.githubusercontent.com/u/119917794?s=280&v=4",
|
|
120
171
|
vmType: "EVM",
|
|
121
172
|
savings: {
|
|
173
|
+
supported: false,
|
|
122
174
|
tokens: []
|
|
123
175
|
}
|
|
124
176
|
},
|
|
@@ -136,6 +188,7 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
136
188
|
logoUrl: "https://avatars.githubusercontent.com/u/58791460?s=280&v=4",
|
|
137
189
|
vmType: "EVM",
|
|
138
190
|
savings: {
|
|
191
|
+
supported: false,
|
|
139
192
|
tokens: []
|
|
140
193
|
}
|
|
141
194
|
},
|
|
@@ -155,6 +208,7 @@ export const DefaultChains: ChainWalletConfig[] = [{
|
|
|
155
208
|
logoUrl: "https://polygonscan.com/images/svg/brands/polygon-light.svg?v=0.0.36",
|
|
156
209
|
vmType: "EVM",
|
|
157
210
|
savings: {
|
|
211
|
+
supported: false,
|
|
158
212
|
tokens: []
|
|
159
213
|
}
|
|
160
214
|
}
|
package/utils/evm/evm.ts
CHANGED
|
@@ -806,7 +806,7 @@ export class EVMVM extends VM<string, string, PublicClient> {
|
|
|
806
806
|
try {
|
|
807
807
|
// Pocket derivation: m/44'/60'/{accountIndex+1}'/0/{walletIndex}
|
|
808
808
|
const pocketIndex = accountIndex + 1;
|
|
809
|
-
const derivationPath = `m/44'/60'/${pocketIndex}'/0/${walletIndex}
|
|
809
|
+
const derivationPath = `m/44'/60'/${pocketIndex}'/0/${walletIndex}`;
|
|
810
810
|
|
|
811
811
|
// Derive pocket
|
|
812
812
|
const { privateKey } = EVMDeriveChildPrivateKey(this.seed, walletIndex, `m/44'/60'/${pocketIndex}'/0/`);
|
|
@@ -119,9 +119,10 @@ export class EVMSavingsManager extends SavingsManager<Hex, PublicClient, WalletC
|
|
|
119
119
|
|
|
120
120
|
// Add 1 to preserve index 0 for main wallet
|
|
121
121
|
const pocketIndex = accountIndex + 1;
|
|
122
|
-
const
|
|
122
|
+
const derivationPathBase = `${this.derivationPathBase}${pocketIndex}'/0/`;
|
|
123
|
+
const derivationPath = `${derivationPathBase}${this.walletIndex}'`;
|
|
123
124
|
const seed = mnemonicToSeed(this.mnemonic);
|
|
124
|
-
const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex,
|
|
125
|
+
const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPathBase);
|
|
125
126
|
const wallet = new ethers.Wallet(privateKey);
|
|
126
127
|
|
|
127
128
|
const pocket: Pocket<Hex> = {
|
|
@@ -142,9 +143,10 @@ export class EVMSavingsManager extends SavingsManager<Hex, PublicClient, WalletC
|
|
|
142
143
|
*/
|
|
143
144
|
getMainWallet() {
|
|
144
145
|
this.checkNotDisposed();
|
|
145
|
-
const
|
|
146
|
+
const derivationPathBase = `${this.derivationPathBase}0'/0/`;
|
|
147
|
+
const derivationPath = `${derivationPathBase}${this.walletIndex}'`;
|
|
146
148
|
const seed = mnemonicToSeed(this.mnemonic);
|
|
147
|
-
const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex,
|
|
149
|
+
const { privateKey } = EVMDeriveChildPrivateKey(seed, this.walletIndex, derivationPathBase);
|
|
148
150
|
const wallet = new ethers.Wallet(privateKey);
|
|
149
151
|
|
|
150
152
|
return {
|
|
@@ -10,6 +10,7 @@ import { SVMSavingsManager } from "./svm-savings";
|
|
|
10
10
|
import { ChainWalletConfig, Balance } from "../types";
|
|
11
11
|
import { Hex } from "viem";
|
|
12
12
|
import { PublicKey } from "@solana/web3.js";
|
|
13
|
+
import { fetchPrices } from "../price";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Chain type identifier
|
|
@@ -46,6 +47,8 @@ export interface PocketBalance {
|
|
|
46
47
|
token: string;
|
|
47
48
|
/** Balance information */
|
|
48
49
|
balance: Balance;
|
|
50
|
+
/** Balance in USD (if unavailable, defaults to 0) */
|
|
51
|
+
balanceInUsd: number;
|
|
49
52
|
}[];
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -90,6 +93,44 @@ export class MultiChainSavingsManager {
|
|
|
90
93
|
// Track chain configs
|
|
91
94
|
private chainConfigs: Map<string, ChainConfig> = new Map();
|
|
92
95
|
|
|
96
|
+
private getTokenKey(tokenAddress: string, chainType: ChainType): string {
|
|
97
|
+
if (chainType === 'EVM') {
|
|
98
|
+
return tokenAddress.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
return tokenAddress;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private normalizePriceMap(
|
|
104
|
+
prices: Array<{ tokenAddress: string; price: number }>,
|
|
105
|
+
chainType: ChainType
|
|
106
|
+
): Map<string, number> {
|
|
107
|
+
const priceMap = new Map<string, number>();
|
|
108
|
+
for (const item of prices) {
|
|
109
|
+
if (!item?.tokenAddress || typeof item.price !== 'number') continue;
|
|
110
|
+
priceMap.set(this.getTokenKey(item.tokenAddress, chainType), item.price);
|
|
111
|
+
}
|
|
112
|
+
return priceMap;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async getPriceMap(
|
|
116
|
+
chain: ChainConfig,
|
|
117
|
+
tokenAddresses: string[]
|
|
118
|
+
): Promise<Map<string, number>> {
|
|
119
|
+
const chainWithId = chain.config as Partial<ChainWalletConfig>;
|
|
120
|
+
if (typeof chainWithId.chainId !== 'number') {
|
|
121
|
+
return new Map();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const uniqueTokenAddresses = Array.from(new Set(tokenAddresses));
|
|
125
|
+
const priceResult = await fetchPrices({
|
|
126
|
+
vm: chain.type,
|
|
127
|
+
chainId: chainWithId.chainId,
|
|
128
|
+
tokenAddresses: uniqueTokenAddresses
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return this.normalizePriceMap(priceResult.data?.prices ?? [], chain.type);
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
/**
|
|
94
135
|
* Create a new MultiChainSavingsManager
|
|
95
136
|
*
|
|
@@ -132,14 +173,13 @@ export class MultiChainSavingsManager {
|
|
|
132
173
|
throw new Error(`Chain with id '${chain.id}' already exists`);
|
|
133
174
|
}
|
|
134
175
|
|
|
135
|
-
this.chainConfigs.set(chain.id, chain);
|
|
136
|
-
|
|
137
176
|
if (chain.type === 'EVM') {
|
|
138
177
|
const manager = new EVMSavingsManager(
|
|
139
178
|
this.mnemonic,
|
|
140
179
|
chain.config as ChainWalletConfig,
|
|
141
180
|
this.walletIndex
|
|
142
181
|
);
|
|
182
|
+
this.chainConfigs.set(chain.id, chain);
|
|
143
183
|
this.evmManagers.set(chain.id, manager);
|
|
144
184
|
} else if (chain.type === 'SVM') {
|
|
145
185
|
const config = chain.config as { rpcUrl: string };
|
|
@@ -151,6 +191,7 @@ export class MultiChainSavingsManager {
|
|
|
151
191
|
config.rpcUrl,
|
|
152
192
|
this.walletIndex
|
|
153
193
|
);
|
|
194
|
+
this.chainConfigs.set(chain.id, chain);
|
|
154
195
|
this.svmManagers.set(chain.id, manager);
|
|
155
196
|
} else {
|
|
156
197
|
throw new Error(`Unknown chain type: ${chain.type}`);
|
|
@@ -253,30 +294,52 @@ export class MultiChainSavingsManager {
|
|
|
253
294
|
const manager = this.evmManagers.get(chainId)!;
|
|
254
295
|
const balances = await manager.getPocketBalance(pocketIndex, tokens);
|
|
255
296
|
const pocket = manager.getPocket(pocketIndex);
|
|
297
|
+
const balanceRows = balances.map(b => ({
|
|
298
|
+
token: b.address === 'native' ? 'native' : b.address,
|
|
299
|
+
balance: b.balance
|
|
300
|
+
}));
|
|
301
|
+
const priceMap = await this.getPriceMap(
|
|
302
|
+
chain,
|
|
303
|
+
balanceRows.map(row => row.token)
|
|
304
|
+
);
|
|
256
305
|
|
|
257
306
|
return {
|
|
258
307
|
chainId,
|
|
259
308
|
chainType: 'EVM',
|
|
260
309
|
pocketIndex,
|
|
261
310
|
address: pocket.address,
|
|
262
|
-
balances:
|
|
263
|
-
token:
|
|
264
|
-
balance:
|
|
311
|
+
balances: balanceRows.map(row => ({
|
|
312
|
+
token: row.token,
|
|
313
|
+
balance: row.balance,
|
|
314
|
+
balanceInUsd:
|
|
315
|
+
row.balance.formatted *
|
|
316
|
+
(priceMap.get(this.getTokenKey(row.token, 'EVM')) ?? 0)
|
|
265
317
|
}))
|
|
266
318
|
};
|
|
267
319
|
} else {
|
|
268
320
|
const manager = this.svmManagers.get(chainId)!;
|
|
269
321
|
const balances = await manager.getPocketBalance(pocketIndex, tokens);
|
|
270
322
|
const pocket = manager.getPocket(pocketIndex);
|
|
323
|
+
const balanceRows = balances.map(b => ({
|
|
324
|
+
token: b.address === 'native' ? 'native' : b.address.toBase58(),
|
|
325
|
+
balance: b.balance
|
|
326
|
+
}));
|
|
327
|
+
const priceMap = await this.getPriceMap(
|
|
328
|
+
chain,
|
|
329
|
+
balanceRows.map(row => row.token)
|
|
330
|
+
);
|
|
271
331
|
|
|
272
332
|
return {
|
|
273
333
|
chainId,
|
|
274
334
|
chainType: 'SVM',
|
|
275
335
|
pocketIndex,
|
|
276
336
|
address: pocket.address.toBase58(),
|
|
277
|
-
balances:
|
|
278
|
-
token:
|
|
279
|
-
balance:
|
|
337
|
+
balances: balanceRows.map(row => ({
|
|
338
|
+
token: row.token,
|
|
339
|
+
balance: row.balance,
|
|
340
|
+
balanceInUsd:
|
|
341
|
+
row.balance.formatted *
|
|
342
|
+
(priceMap.get(this.getTokenKey(row.token, 'SVM')) ?? 0)
|
|
280
343
|
}))
|
|
281
344
|
};
|
|
282
345
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EVMDeriveChildPrivateKey,
|
|
1
|
+
import { EVMDeriveChildPrivateKey, mnemonicToSeed } from "../walletBip32";
|
|
2
2
|
import { ethers } from "ethers";
|
|
3
3
|
import { WalletClient, PublicClient, Hex, Chain, createWalletClient, createPublicClient, http } from "viem";
|
|
4
4
|
import { } from "../utils";
|
|
@@ -141,11 +141,12 @@ export class BaseSavingsManager {
|
|
|
141
141
|
|
|
142
142
|
//? for the sake of derivation we will add one to the index of the pocket that was passed so as to preserve the index 0 as the main wallet index
|
|
143
143
|
const pocketIndex = accountIndex + 1
|
|
144
|
-
const
|
|
145
|
-
const
|
|
144
|
+
const derivationPathBase = `m/44'/60'/${pocketIndex}'/0/`;
|
|
145
|
+
const derivationPath = `${derivationPathBase}${this.walletIndex}'`;
|
|
146
|
+
const { privateKey } = EVMDeriveChildPrivateKey(mnemonicToSeed(this.mnemonic), this.walletIndex, derivationPathBase);
|
|
146
147
|
const wallet = new ethers.Wallet(privateKey);
|
|
147
148
|
const pocket = { privateKey, address: wallet.address, derivationPath, index: pocketIndex };
|
|
148
|
-
this.pockets.set(
|
|
149
|
+
this.pockets.set(accountIndex, pocket);
|
|
149
150
|
return pocket;
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -164,8 +165,13 @@ export class BaseSavingsManager {
|
|
|
164
165
|
* ```
|
|
165
166
|
*/
|
|
166
167
|
getMainWallet() {
|
|
167
|
-
const
|
|
168
|
-
const
|
|
168
|
+
const mainWalletDerivationPathBase = `m/44'/60'/0'/0/`;
|
|
169
|
+
const mainWalletDerivationPath = `${mainWalletDerivationPathBase}${this.walletIndex}'`;
|
|
170
|
+
const { privateKey } = EVMDeriveChildPrivateKey(
|
|
171
|
+
mnemonicToSeed(this.mnemonic),
|
|
172
|
+
this.walletIndex,
|
|
173
|
+
mainWalletDerivationPathBase
|
|
174
|
+
);
|
|
169
175
|
const wallet = new ethers.Wallet(privateKey);
|
|
170
176
|
return { privateKey, address: wallet.address, derivationPath: mainWalletDerivationPath };
|
|
171
177
|
}
|
|
@@ -185,8 +191,7 @@ export class BaseSavingsManager {
|
|
|
185
191
|
*/
|
|
186
192
|
getMainWalletAddress(): Hex {
|
|
187
193
|
if (this.masterAddress) return this.masterAddress;
|
|
188
|
-
|
|
189
|
-
return new ethers.Wallet(privateKey).address as Hex
|
|
194
|
+
return this.getMainWallet().address as Hex
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
/**
|
|
@@ -342,8 +347,8 @@ export class BaseSavingsManager {
|
|
|
342
347
|
* ```
|
|
343
348
|
*/
|
|
344
349
|
accountFromPocketId(p: number): Account {
|
|
345
|
-
// Validation is done in
|
|
346
|
-
return privateKeyToAccount(`0x${this.
|
|
350
|
+
// Validation is done in getPocket
|
|
351
|
+
return privateKeyToAccount(`0x${this.getPocket(p).privateKey}`)
|
|
347
352
|
}
|
|
348
353
|
|
|
349
354
|
/**
|
|
@@ -599,16 +604,13 @@ export class SavingsManager extends BaseSavingsManager {
|
|
|
599
604
|
* Withdraws either native tokens or ERC-20 tokens from a pocket to the main wallet.
|
|
600
605
|
*
|
|
601
606
|
* @param pocketIndex - Index of the pocket to withdraw from
|
|
602
|
-
* @param
|
|
607
|
+
* @param amount - Amount to send in base units
|
|
603
608
|
* @param token - Token address or "native" for native blockchain token
|
|
604
609
|
* @returns Transaction result containing hash and success status
|
|
605
610
|
*
|
|
606
611
|
* @throws Error if validation fails
|
|
607
612
|
*
|
|
608
613
|
* @remarks
|
|
609
|
-
* Despite the parameter name, amountInEther is actually interpreted as a raw bigint value,
|
|
610
|
-
* not converted from ether units. Consider renaming to `amount` for clarity.
|
|
611
|
-
*
|
|
612
614
|
* @example
|
|
613
615
|
* ```typescript
|
|
614
616
|
* // Send native token from pocket to main wallet
|
|
@@ -626,13 +628,11 @@ export class SavingsManager extends BaseSavingsManager {
|
|
|
626
628
|
* );
|
|
627
629
|
* ```
|
|
628
630
|
*/
|
|
629
|
-
async sendToMainWallet(pocketIndex: number,
|
|
631
|
+
async sendToMainWallet(pocketIndex: number, amount: bigint, token: Hex | "native"): Promise<TransactionResult> {
|
|
630
632
|
// Validate inputs
|
|
631
633
|
SavingsValidation.validateAccountIndex(pocketIndex);
|
|
632
634
|
|
|
633
|
-
|
|
634
|
-
throw new Error(`Amount must be a positive number, got: ${amountInEther}`);
|
|
635
|
-
}
|
|
635
|
+
SavingsValidation.validateAmount(amount, 'Transfer amount');
|
|
636
636
|
|
|
637
637
|
if (token !== 'native') {
|
|
638
638
|
SavingsValidation.validateAddress(token, 'Token address');
|
|
@@ -648,9 +648,9 @@ export class SavingsManager extends BaseSavingsManager {
|
|
|
648
648
|
}
|
|
649
649
|
)
|
|
650
650
|
if (token === "native") {
|
|
651
|
-
return await sendNativeToken(walletClient, this.client, mainWalletAddress,
|
|
651
|
+
return await sendNativeToken(walletClient, this.client, mainWalletAddress, amount)
|
|
652
652
|
}
|
|
653
|
-
const res = await sendERC20Token(walletClient, this.client, token, mainWalletAddress as Hex,
|
|
653
|
+
const res = await sendERC20Token(walletClient, this.client, token, mainWalletAddress as Hex, amount)
|
|
654
654
|
return res
|
|
655
655
|
}
|
|
656
656
|
}
|