amped-defi 1.0.0
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/README.md +757 -0
- package/dist/__mocks__/@sodax/sdk.d.ts +24 -0
- package/dist/__mocks__/@sodax/sdk.d.ts.map +1 -0
- package/dist/__mocks__/@sodax/sdk.js +24 -0
- package/dist/__mocks__/@sodax/sdk.js.map +1 -0
- package/dist/__tests__/setup.d.ts +4 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +32 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +1 -0
- package/dist/policy/policyEngine.d.ts +119 -0
- package/dist/policy/policyEngine.d.ts.map +1 -0
- package/dist/policy/policyEngine.js +322 -0
- package/dist/policy/policyEngine.js.map +1 -0
- package/dist/providers/spokeProviderFactory.d.ts +38 -0
- package/dist/providers/spokeProviderFactory.d.ts.map +1 -0
- package/dist/providers/spokeProviderFactory.js +212 -0
- package/dist/providers/spokeProviderFactory.js.map +1 -0
- package/dist/sodax/client.d.ts +34 -0
- package/dist/sodax/client.d.ts.map +1 -0
- package/dist/sodax/client.js +99 -0
- package/dist/sodax/client.js.map +1 -0
- package/dist/tools/bridge.d.ts +105 -0
- package/dist/tools/bridge.d.ts.map +1 -0
- package/dist/tools/bridge.js +334 -0
- package/dist/tools/bridge.js.map +1 -0
- package/dist/tools/discovery.d.ts +141 -0
- package/dist/tools/discovery.d.ts.map +1 -0
- package/dist/tools/discovery.js +777 -0
- package/dist/tools/discovery.js.map +1 -0
- package/dist/tools/moneyMarket.d.ts +227 -0
- package/dist/tools/moneyMarket.d.ts.map +1 -0
- package/dist/tools/moneyMarket.js +867 -0
- package/dist/tools/moneyMarket.js.map +1 -0
- package/dist/tools/portfolio.d.ts +43 -0
- package/dist/tools/portfolio.d.ts.map +1 -0
- package/dist/tools/portfolio.js +538 -0
- package/dist/tools/portfolio.js.map +1 -0
- package/dist/tools/swap.d.ts +71 -0
- package/dist/tools/swap.d.ts.map +1 -0
- package/dist/tools/swap.js +762 -0
- package/dist/tools/swap.js.map +1 -0
- package/dist/tools/walletManagement.d.ts +80 -0
- package/dist/tools/walletManagement.d.ts.map +1 -0
- package/dist/tools/walletManagement.js +289 -0
- package/dist/tools/walletManagement.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/errorUtils.d.ts +2 -0
- package/dist/utils/errorUtils.d.ts.map +1 -0
- package/dist/utils/errorUtils.js +19 -0
- package/dist/utils/errorUtils.js.map +1 -0
- package/dist/utils/errors.d.ts +144 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +310 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/positionAggregator.d.ts +122 -0
- package/dist/utils/positionAggregator.d.ts.map +1 -0
- package/dist/utils/positionAggregator.js +377 -0
- package/dist/utils/positionAggregator.js.map +1 -0
- package/dist/utils/priceService.d.ts +45 -0
- package/dist/utils/priceService.d.ts.map +1 -0
- package/dist/utils/priceService.js +108 -0
- package/dist/utils/priceService.js.map +1 -0
- package/dist/utils/sodaxApi.d.ts +92 -0
- package/dist/utils/sodaxApi.d.ts.map +1 -0
- package/dist/utils/sodaxApi.js +143 -0
- package/dist/utils/sodaxApi.js.map +1 -0
- package/dist/utils/tokenResolver.d.ts +54 -0
- package/dist/utils/tokenResolver.d.ts.map +1 -0
- package/dist/utils/tokenResolver.js +252 -0
- package/dist/utils/tokenResolver.js.map +1 -0
- package/dist/wallet/backendConfig.d.ts +37 -0
- package/dist/wallet/backendConfig.d.ts.map +1 -0
- package/dist/wallet/backendConfig.js +125 -0
- package/dist/wallet/backendConfig.js.map +1 -0
- package/dist/wallet/backends/BankrBackend.d.ts +73 -0
- package/dist/wallet/backends/BankrBackend.d.ts.map +1 -0
- package/dist/wallet/backends/BankrBackend.js +315 -0
- package/dist/wallet/backends/BankrBackend.js.map +1 -0
- package/dist/wallet/backends/BankrWalletProvider.d.ts +75 -0
- package/dist/wallet/backends/BankrWalletProvider.d.ts.map +1 -0
- package/dist/wallet/backends/BankrWalletProvider.js +243 -0
- package/dist/wallet/backends/BankrWalletProvider.js.map +1 -0
- package/dist/wallet/backends/EnvBackend.d.ts +50 -0
- package/dist/wallet/backends/EnvBackend.d.ts.map +1 -0
- package/dist/wallet/backends/EnvBackend.js +114 -0
- package/dist/wallet/backends/EnvBackend.js.map +1 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.d.ts +40 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.d.ts.map +1 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.js +81 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.js.map +1 -0
- package/dist/wallet/backends/index.d.ts +10 -0
- package/dist/wallet/backends/index.d.ts.map +1 -0
- package/dist/wallet/backends/index.js +10 -0
- package/dist/wallet/backends/index.js.map +1 -0
- package/dist/wallet/index.d.ts +9 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +12 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/providers/AmpedWalletProvider.d.ts +107 -0
- package/dist/wallet/providers/AmpedWalletProvider.d.ts.map +1 -0
- package/dist/wallet/providers/AmpedWalletProvider.js +208 -0
- package/dist/wallet/providers/AmpedWalletProvider.js.map +1 -0
- package/dist/wallet/providers/BankrBackend.d.ts +105 -0
- package/dist/wallet/providers/BankrBackend.d.ts.map +1 -0
- package/dist/wallet/providers/BankrBackend.js +327 -0
- package/dist/wallet/providers/BankrBackend.js.map +1 -0
- package/dist/wallet/providers/LocalKeyBackend.d.ts +62 -0
- package/dist/wallet/providers/LocalKeyBackend.d.ts.map +1 -0
- package/dist/wallet/providers/LocalKeyBackend.js +152 -0
- package/dist/wallet/providers/LocalKeyBackend.js.map +1 -0
- package/dist/wallet/providers/chainConfig.d.ts +209 -0
- package/dist/wallet/providers/chainConfig.d.ts.map +1 -0
- package/dist/wallet/providers/chainConfig.js +175 -0
- package/dist/wallet/providers/chainConfig.js.map +1 -0
- package/dist/wallet/providers/index.d.ts +30 -0
- package/dist/wallet/providers/index.d.ts.map +1 -0
- package/dist/wallet/providers/index.js +32 -0
- package/dist/wallet/providers/index.js.map +1 -0
- package/dist/wallet/providers/types.d.ts +156 -0
- package/dist/wallet/providers/types.d.ts.map +1 -0
- package/dist/wallet/providers/types.js +11 -0
- package/dist/wallet/providers/types.js.map +1 -0
- package/dist/wallet/skillWalletAdapter.d.ts +96 -0
- package/dist/wallet/skillWalletAdapter.d.ts.map +1 -0
- package/dist/wallet/skillWalletAdapter.js +280 -0
- package/dist/wallet/skillWalletAdapter.js.map +1 -0
- package/dist/wallet/types.d.ts +134 -0
- package/dist/wallet/types.d.ts.map +1 -0
- package/dist/wallet/types.js +138 -0
- package/dist/wallet/types.js.map +1 -0
- package/dist/wallet/walletManager.d.ts +111 -0
- package/dist/wallet/walletManager.d.ts.map +1 -0
- package/dist/wallet/walletManager.js +476 -0
- package/dist/wallet/walletManager.js.map +1 -0
- package/dist/wallet/walletRegistry.d.ts +95 -0
- package/dist/wallet/walletRegistry.d.ts.map +1 -0
- package/dist/wallet/walletRegistry.js +184 -0
- package/dist/wallet/walletRegistry.js.map +1 -0
- package/index.js +2 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +69 -0
- package/src/__mocks__/@sodax/sdk.ts +28 -0
- package/src/__tests__/errors.test.ts +238 -0
- package/src/__tests__/policyEngine.test.ts +354 -0
- package/src/__tests__/positionAggregator.test.ts +271 -0
- package/src/__tests__/setup.ts +35 -0
- package/src/__tests__/sodaxApi.test.ts +203 -0
- package/src/__tests__/walletRegistry.test.ts +155 -0
- package/src/index.ts +376 -0
- package/src/policy/policyEngine.ts +389 -0
- package/src/providers/spokeProviderFactory.ts +283 -0
- package/src/sodax/client.ts +113 -0
- package/src/tools/bridge.ts +425 -0
- package/src/tools/discovery.ts +989 -0
- package/src/tools/moneyMarket.ts +1265 -0
- package/src/tools/portfolio.ts +697 -0
- package/src/tools/swap.ts +926 -0
- package/src/tools/walletManagement.ts +359 -0
- package/src/types.ts +228 -0
- package/src/utils/errorUtils.ts +16 -0
- package/src/utils/errors.ts +396 -0
- package/src/utils/positionAggregator.ts +559 -0
- package/src/utils/priceService.ts +153 -0
- package/src/utils/sodaxApi.ts +261 -0
- package/src/utils/tokenResolver.ts +286 -0
- package/src/wallet/backendConfig.ts +151 -0
- package/src/wallet/backends/BankrBackend.ts +399 -0
- package/src/wallet/backends/BankrWalletProvider.ts +329 -0
- package/src/wallet/backends/EnvBackend.ts +149 -0
- package/src/wallet/backends/EvmWalletSkillBackend.ts +110 -0
- package/src/wallet/backends/index.ts +10 -0
- package/src/wallet/index.ts +14 -0
- package/src/wallet/providers/AmpedWalletProvider.ts +267 -0
- package/src/wallet/providers/BankrBackend.ts +407 -0
- package/src/wallet/providers/LocalKeyBackend.ts +184 -0
- package/src/wallet/providers/chainConfig.ts +194 -0
- package/src/wallet/providers/index.ts +62 -0
- package/src/wallet/providers/types.ts +186 -0
- package/src/wallet/skillWalletAdapter.ts +335 -0
- package/src/wallet/types.ts +248 -0
- package/src/wallet/walletManager.ts +561 -0
- package/src/wallet/walletRegistry.ts +216 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portfolio Summary Tool
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified view of all wallet balances and positions.
|
|
5
|
+
* Queries native tokens and major stablecoins via RPC, plus money market positions.
|
|
6
|
+
*
|
|
7
|
+
* @module tools/portfolio
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Type, Static } from '@sinclair/typebox';
|
|
11
|
+
import { createPublicClient, http, formatUnits, type PublicClient, type Address } from 'viem';
|
|
12
|
+
import { getWalletManager } from '../wallet';
|
|
13
|
+
import { aggregateCrossChainPositions, formatHealthFactor, getHealthFactorStatus } from '../utils/positionAggregator';
|
|
14
|
+
import { fetchTokenPrices, getTokenPriceBySymbol } from '../utils/priceService';
|
|
15
|
+
import {
|
|
16
|
+
getViemChain,
|
|
17
|
+
getDefaultRpcUrl,
|
|
18
|
+
resolveChainId,
|
|
19
|
+
CHAIN_IDS,
|
|
20
|
+
} from '../wallet/providers/chainConfig';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// TypeBox Schema
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Schema for amped_portfolio_summary
|
|
28
|
+
*/
|
|
29
|
+
export const PortfolioSummarySchema = Type.Object({
|
|
30
|
+
walletId: Type.Optional(Type.String({
|
|
31
|
+
description: 'Specific wallet to query (defaults to all wallets)',
|
|
32
|
+
})),
|
|
33
|
+
chains: Type.Optional(Type.Array(Type.String(), {
|
|
34
|
+
description: 'Specific chains to query (defaults to all supported chains)',
|
|
35
|
+
})),
|
|
36
|
+
includeZeroBalances: Type.Optional(Type.Boolean({
|
|
37
|
+
description: 'Include tokens with zero balance',
|
|
38
|
+
default: false,
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
type PortfolioSummaryParams = Static<typeof PortfolioSummarySchema>;
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Token Configuration
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
interface TokenConfig {
|
|
49
|
+
address: string;
|
|
50
|
+
symbol: string;
|
|
51
|
+
decimals: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Major tokens to check on each chain
|
|
56
|
+
*/
|
|
57
|
+
const MAJOR_TOKENS: Record<number, TokenConfig[]> = {
|
|
58
|
+
[CHAIN_IDS.ETHEREUM]: [
|
|
59
|
+
{ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', decimals: 6 },
|
|
60
|
+
{ address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', symbol: 'USDT', decimals: 6 },
|
|
61
|
+
{ address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH', decimals: 18 },
|
|
62
|
+
],
|
|
63
|
+
[CHAIN_IDS.BASE]: [
|
|
64
|
+
{ address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', symbol: 'USDC', decimals: 6 },
|
|
65
|
+
{ address: '0x4200000000000000000000000000000000000006', symbol: 'WETH', decimals: 18 },
|
|
66
|
+
],
|
|
67
|
+
[CHAIN_IDS.ARBITRUM]: [
|
|
68
|
+
{ address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', symbol: 'USDC', decimals: 6 },
|
|
69
|
+
{ address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', symbol: 'USDT', decimals: 6 },
|
|
70
|
+
{ address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', symbol: 'WETH', decimals: 18 },
|
|
71
|
+
],
|
|
72
|
+
[CHAIN_IDS.OPTIMISM]: [
|
|
73
|
+
{ address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', symbol: 'USDC', decimals: 6 },
|
|
74
|
+
{ address: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58', symbol: 'USDT', decimals: 6 },
|
|
75
|
+
{ address: '0x4200000000000000000000000000000000000006', symbol: 'WETH', decimals: 18 },
|
|
76
|
+
],
|
|
77
|
+
[CHAIN_IDS.POLYGON]: [
|
|
78
|
+
{ address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', symbol: 'USDC', decimals: 6 },
|
|
79
|
+
{ address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', symbol: 'USDT', decimals: 6 },
|
|
80
|
+
{ address: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619', symbol: 'WETH', decimals: 18 },
|
|
81
|
+
],
|
|
82
|
+
[CHAIN_IDS.BSC]: [
|
|
83
|
+
{ address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', symbol: 'USDC', decimals: 18 },
|
|
84
|
+
{ address: '0x55d398326f99059fF775485246999027B3197955', symbol: 'USDT', decimals: 18 },
|
|
85
|
+
{ address: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8', symbol: 'WETH', decimals: 18 },
|
|
86
|
+
],
|
|
87
|
+
[CHAIN_IDS.AVALANCHE]: [
|
|
88
|
+
{ address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', symbol: 'USDC', decimals: 6 },
|
|
89
|
+
{ address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', symbol: 'USDT', decimals: 6 },
|
|
90
|
+
{ address: '0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB', symbol: 'WETH', decimals: 18 },
|
|
91
|
+
],
|
|
92
|
+
[CHAIN_IDS.SONIC]: [
|
|
93
|
+
{ address: '0x29219dd400f2Bf60E5a23d13Be72B486D4038894', symbol: 'USDC', decimals: 6 },
|
|
94
|
+
],
|
|
95
|
+
[CHAIN_IDS.LIGHTLINK]: [
|
|
96
|
+
{ address: '0xbCF8C1B03bBDDA88D579330BDF236B58F8bb2cFd', symbol: 'USDC', decimals: 6 },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Native token symbols by chain
|
|
102
|
+
*/
|
|
103
|
+
const NATIVE_SYMBOLS: Record<number, string> = {
|
|
104
|
+
[CHAIN_IDS.ETHEREUM]: 'ETH',
|
|
105
|
+
[CHAIN_IDS.ARBITRUM]: 'ETH',
|
|
106
|
+
[CHAIN_IDS.OPTIMISM]: 'ETH',
|
|
107
|
+
[CHAIN_IDS.BASE]: 'ETH',
|
|
108
|
+
[CHAIN_IDS.POLYGON]: 'POL',
|
|
109
|
+
[CHAIN_IDS.BSC]: 'BNB',
|
|
110
|
+
[CHAIN_IDS.AVALANCHE]: 'AVAX',
|
|
111
|
+
[CHAIN_IDS.SONIC]: 'S',
|
|
112
|
+
[CHAIN_IDS.LIGHTLINK]: 'ETH',
|
|
113
|
+
[CHAIN_IDS.HYPEREVM]: 'HYPE',
|
|
114
|
+
[CHAIN_IDS.KAIA]: 'KAIA',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Chain ID to name mapping
|
|
119
|
+
*/
|
|
120
|
+
const CHAIN_NAMES: Record<number, string> = {
|
|
121
|
+
[CHAIN_IDS.ETHEREUM]: 'Ethereum',
|
|
122
|
+
[CHAIN_IDS.ARBITRUM]: 'Arbitrum',
|
|
123
|
+
[CHAIN_IDS.OPTIMISM]: 'Optimism',
|
|
124
|
+
[CHAIN_IDS.BASE]: 'Base',
|
|
125
|
+
[CHAIN_IDS.POLYGON]: 'Polygon',
|
|
126
|
+
[CHAIN_IDS.BSC]: 'BSC',
|
|
127
|
+
[CHAIN_IDS.AVALANCHE]: 'Avalanche',
|
|
128
|
+
[CHAIN_IDS.SONIC]: 'Sonic',
|
|
129
|
+
[CHAIN_IDS.LIGHTLINK]: 'LightLink',
|
|
130
|
+
[CHAIN_IDS.HYPEREVM]: 'HyperEVM',
|
|
131
|
+
[CHAIN_IDS.KAIA]: 'Kaia',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Chain name strings for wallet support check
|
|
136
|
+
*/
|
|
137
|
+
const CHAIN_NAME_STRINGS: Record<number, string[]> = {
|
|
138
|
+
[CHAIN_IDS.ETHEREUM]: ['ethereum'],
|
|
139
|
+
[CHAIN_IDS.BASE]: ['base'],
|
|
140
|
+
[CHAIN_IDS.ARBITRUM]: ['arbitrum'],
|
|
141
|
+
[CHAIN_IDS.OPTIMISM]: ['optimism'],
|
|
142
|
+
[CHAIN_IDS.POLYGON]: ['polygon'],
|
|
143
|
+
[CHAIN_IDS.BSC]: ['bsc'],
|
|
144
|
+
[CHAIN_IDS.AVALANCHE]: ['avalanche', 'avax'],
|
|
145
|
+
[CHAIN_IDS.SONIC]: ['sonic'],
|
|
146
|
+
[CHAIN_IDS.LIGHTLINK]: ['lightlink'],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Helper Functions
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a viem public client for a chain
|
|
155
|
+
*/
|
|
156
|
+
function createClient(chainId: number): PublicClient {
|
|
157
|
+
const chain = getViemChain(chainId);
|
|
158
|
+
const rpcUrl = getDefaultRpcUrl(chainId);
|
|
159
|
+
return createPublicClient({
|
|
160
|
+
chain,
|
|
161
|
+
transport: http(rpcUrl, { timeout: 10000 }),
|
|
162
|
+
}) as PublicClient;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get native balance for a wallet on a chain
|
|
167
|
+
*/
|
|
168
|
+
async function getNativeBalance(
|
|
169
|
+
client: PublicClient,
|
|
170
|
+
address: Address,
|
|
171
|
+
chainId: number
|
|
172
|
+
): Promise<{ symbol: string; balance: string; balanceRaw: bigint }> {
|
|
173
|
+
try {
|
|
174
|
+
const balance = await client.getBalance({ address });
|
|
175
|
+
return {
|
|
176
|
+
symbol: NATIVE_SYMBOLS[chainId] || 'ETH',
|
|
177
|
+
balance: formatUnits(balance, 18),
|
|
178
|
+
balanceRaw: balance,
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`[portfolio] Failed to get native balance on chain ${chainId}:`, error);
|
|
182
|
+
return { symbol: NATIVE_SYMBOLS[chainId] || 'ETH', balance: '0', balanceRaw: 0n };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get ERC20 token balance using eth_call directly (avoids viem type issues)
|
|
188
|
+
*/
|
|
189
|
+
async function getTokenBalance(
|
|
190
|
+
rpcUrl: string,
|
|
191
|
+
walletAddress: string,
|
|
192
|
+
tokenAddress: string,
|
|
193
|
+
decimals: number,
|
|
194
|
+
symbol: string
|
|
195
|
+
): Promise<{ symbol: string; balance: string; balanceRaw: bigint; address: string }> {
|
|
196
|
+
try {
|
|
197
|
+
// balanceOf(address) selector: 0x70a08231
|
|
198
|
+
const paddedAddress = walletAddress.slice(2).toLowerCase().padStart(64, '0');
|
|
199
|
+
const callData = `0x70a08231${paddedAddress}`;
|
|
200
|
+
|
|
201
|
+
const response = await fetch(rpcUrl, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
jsonrpc: '2.0',
|
|
206
|
+
method: 'eth_call',
|
|
207
|
+
params: [{ to: tokenAddress, data: callData }, 'latest'],
|
|
208
|
+
id: 1,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const json = await response.json() as { result?: string };
|
|
213
|
+
const result = json.result;
|
|
214
|
+
|
|
215
|
+
if (!result || result === '0x' || result === '0x0') {
|
|
216
|
+
return { symbol, balance: '0', balanceRaw: 0n, address: tokenAddress };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const balanceRaw = BigInt(result);
|
|
220
|
+
const balance = formatUnits(balanceRaw, decimals);
|
|
221
|
+
|
|
222
|
+
return { symbol, balance, balanceRaw, address: tokenAddress };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`[portfolio] Failed to get ${symbol} balance:`, error);
|
|
225
|
+
return { symbol, balance: '0', balanceRaw: 0n, address: tokenAddress };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Query all balances for a wallet on a specific chain
|
|
231
|
+
*/
|
|
232
|
+
async function getChainBalances(
|
|
233
|
+
address: Address,
|
|
234
|
+
chainId: number,
|
|
235
|
+
includeZeroBalances: boolean
|
|
236
|
+
): Promise<{
|
|
237
|
+
chainId: string;
|
|
238
|
+
chainName: string;
|
|
239
|
+
native: { symbol: string; balance: string; usdValue?: string };
|
|
240
|
+
tokens: Array<{ symbol: string; balance: string; address: string; usdValue?: string }>; chainTotalUsd?: string;
|
|
241
|
+
}> {
|
|
242
|
+
const client = createClient(chainId);
|
|
243
|
+
const rpcUrl = getDefaultRpcUrl(chainId);
|
|
244
|
+
const chainName = CHAIN_NAMES[chainId] || `Chain ${chainId}`;
|
|
245
|
+
|
|
246
|
+
// Get native balance
|
|
247
|
+
const native = await getNativeBalance(client, address, chainId);
|
|
248
|
+
|
|
249
|
+
// Get token balances
|
|
250
|
+
const tokenConfigs = MAJOR_TOKENS[chainId] || [];
|
|
251
|
+
const tokenPromises = tokenConfigs.map((t) =>
|
|
252
|
+
getTokenBalance(rpcUrl, address, t.address, t.decimals, t.symbol)
|
|
253
|
+
);
|
|
254
|
+
const tokenResults = await Promise.all(tokenPromises);
|
|
255
|
+
|
|
256
|
+
// Filter zero balances if requested
|
|
257
|
+
const tokens = includeZeroBalances
|
|
258
|
+
? tokenResults.map((t) => ({ symbol: t.symbol, balance: t.balance, address: t.address }))
|
|
259
|
+
: tokenResults
|
|
260
|
+
.filter((t) => t.balanceRaw > 0n)
|
|
261
|
+
.map((t) => ({ symbol: t.symbol, balance: t.balance, address: t.address }));
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
chainId: chainId.toString(),
|
|
265
|
+
chainName,
|
|
266
|
+
native: {
|
|
267
|
+
symbol: native.symbol,
|
|
268
|
+
balance: parseFloat(native.balance).toFixed(6),
|
|
269
|
+
},
|
|
270
|
+
tokens,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Main Handler
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
interface WalletBalanceResult {
|
|
279
|
+
wallet: {
|
|
280
|
+
nickname: string;
|
|
281
|
+
address: string;
|
|
282
|
+
type: string;
|
|
283
|
+
};
|
|
284
|
+
balances: Array<{
|
|
285
|
+
chainId: string;
|
|
286
|
+
chainName: string;
|
|
287
|
+
native: { symbol: string; balance: string; usdValue?: string };
|
|
288
|
+
tokens: Array<{ symbol: string; balance: string; address: string; usdValue?: string }>;
|
|
289
|
+
chainTotalUsd?: string;
|
|
290
|
+
}>;
|
|
291
|
+
moneyMarket?: {
|
|
292
|
+
totalSupplyUsd: string;
|
|
293
|
+
totalBorrowUsd: string;
|
|
294
|
+
netWorthUsd: string;
|
|
295
|
+
healthFactor: string;
|
|
296
|
+
healthStatus: { status: string; color: string };
|
|
297
|
+
/** Per-chain breakdown - CRITICAL: each chain has independent health factor */
|
|
298
|
+
chainBreakdown: Array<{
|
|
299
|
+
chainId: string;
|
|
300
|
+
supplyUsd: string;
|
|
301
|
+
borrowUsd: string;
|
|
302
|
+
healthFactor: string;
|
|
303
|
+
healthStatus: { status: string; color: string };
|
|
304
|
+
}>;
|
|
305
|
+
};
|
|
306
|
+
walletTotalUsd?: string;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Handle portfolio summary request
|
|
311
|
+
*/
|
|
312
|
+
export async function handlePortfolioSummary(
|
|
313
|
+
params: PortfolioSummaryParams
|
|
314
|
+
): Promise<unknown> {
|
|
315
|
+
const { walletId, chains, includeZeroBalances = false } = params;
|
|
316
|
+
|
|
317
|
+
console.log('[portfolio:summary] Fetching portfolio summary', {
|
|
318
|
+
walletId: walletId || 'all',
|
|
319
|
+
chains: chains || 'all',
|
|
320
|
+
includeZeroBalances,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Fetch token prices from SODAX (cached, 1 min TTL)
|
|
324
|
+
let priceMap: Awaited<ReturnType<typeof fetchTokenPrices>> | null = null;
|
|
325
|
+
try {
|
|
326
|
+
priceMap = await fetchTokenPrices();
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.warn('[portfolio] Failed to fetch prices, USD values will be unavailable:', err);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const walletManager = getWalletManager();
|
|
332
|
+
const allWallets = await walletManager.listWallets();
|
|
333
|
+
|
|
334
|
+
// Filter to specific wallet if requested
|
|
335
|
+
const walletsToQuery = walletId
|
|
336
|
+
? allWallets.filter((w) => w.nickname === walletId)
|
|
337
|
+
: allWallets;
|
|
338
|
+
|
|
339
|
+
if (walletsToQuery.length === 0) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
error: walletId ? `Wallet not found: ${walletId}` : 'No wallets configured',
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Determine chains to query
|
|
347
|
+
// Query all chains with configured tokens by default
|
|
348
|
+
const defaultChains = [
|
|
349
|
+
CHAIN_IDS.BASE,
|
|
350
|
+
CHAIN_IDS.ETHEREUM,
|
|
351
|
+
CHAIN_IDS.ARBITRUM,
|
|
352
|
+
CHAIN_IDS.OPTIMISM,
|
|
353
|
+
CHAIN_IDS.POLYGON,
|
|
354
|
+
CHAIN_IDS.SONIC,
|
|
355
|
+
CHAIN_IDS.BSC,
|
|
356
|
+
CHAIN_IDS.AVALANCHE,
|
|
357
|
+
CHAIN_IDS.LIGHTLINK,
|
|
358
|
+
];
|
|
359
|
+
const chainIdsToQuery = chains
|
|
360
|
+
? chains.map((c) => resolveChainId(c))
|
|
361
|
+
: defaultChains;
|
|
362
|
+
|
|
363
|
+
const results: WalletBalanceResult[] = [];
|
|
364
|
+
let totalValueUsd = 0;
|
|
365
|
+
|
|
366
|
+
// Helper to get USD price for a symbol
|
|
367
|
+
const getPrice = (symbol: string): number | null => {
|
|
368
|
+
if (!priceMap) return null;
|
|
369
|
+
const lower = symbol.toLowerCase();
|
|
370
|
+
return priceMap.bySymbol.get(lower) ?? priceMap.bySymbol.get('soda' + lower) ?? null;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
for (const wallet of walletsToQuery) {
|
|
374
|
+
// Skip wallets without known addresses
|
|
375
|
+
if (wallet.address === '0x...') {
|
|
376
|
+
console.log(`[portfolio] Skipping wallet ${wallet.nickname} - address not resolved`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const address = wallet.address as Address;
|
|
381
|
+
|
|
382
|
+
// Filter chains to those the wallet supports
|
|
383
|
+
const supportedChains = wallet.chains || [];
|
|
384
|
+
const chainsForWallet = chainIdsToQuery.filter((cid) => {
|
|
385
|
+
const names = CHAIN_NAME_STRINGS[cid] || [];
|
|
386
|
+
return supportedChains.length === 0 || names.some((n) => supportedChains.includes(n));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Query balances for each chain (in parallel)
|
|
390
|
+
const balancePromises = chainsForWallet.map((cid) =>
|
|
391
|
+
getChainBalances(address, cid, includeZeroBalances).catch((err) => {
|
|
392
|
+
console.error(`[portfolio] Failed to query chain ${cid}:`, err);
|
|
393
|
+
return null;
|
|
394
|
+
})
|
|
395
|
+
);
|
|
396
|
+
const balanceResults = (await Promise.all(balancePromises)).filter(
|
|
397
|
+
(b): b is NonNullable<typeof b> => b !== null
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Filter out chains with no balances if not including zeros
|
|
401
|
+
const filteredBalances = includeZeroBalances
|
|
402
|
+
? balanceResults
|
|
403
|
+
: balanceResults.filter(
|
|
404
|
+
(b) => parseFloat(b.native.balance) > 0 || b.tokens.length > 0
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// Add USD values to balances
|
|
408
|
+
let walletBalanceUsd = 0;
|
|
409
|
+
const balancesWithUsd = filteredBalances.map((chainBalance) => {
|
|
410
|
+
let chainTotalUsd = 0;
|
|
411
|
+
|
|
412
|
+
// Native token USD value
|
|
413
|
+
const nativePrice = getPrice(chainBalance.native.symbol);
|
|
414
|
+
const nativeBalance = parseFloat(chainBalance.native.balance);
|
|
415
|
+
const nativeUsdValue = nativePrice ? nativeBalance * nativePrice : null;
|
|
416
|
+
if (nativeUsdValue) chainTotalUsd += nativeUsdValue;
|
|
417
|
+
|
|
418
|
+
// Token USD values
|
|
419
|
+
const tokensWithUsd = chainBalance.tokens.map((token) => {
|
|
420
|
+
const price = getPrice(token.symbol);
|
|
421
|
+
const balance = parseFloat(token.balance);
|
|
422
|
+
const usdValue = price ? balance * price : null;
|
|
423
|
+
if (usdValue) chainTotalUsd += usdValue;
|
|
424
|
+
return {
|
|
425
|
+
...token,
|
|
426
|
+
usdValue: usdValue ? `$${usdValue.toFixed(2)}` : undefined,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
walletBalanceUsd += chainTotalUsd;
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
chainId: chainBalance.chainId,
|
|
434
|
+
chainName: chainBalance.chainName,
|
|
435
|
+
native: {
|
|
436
|
+
symbol: chainBalance.native.symbol,
|
|
437
|
+
balance: chainBalance.native.balance,
|
|
438
|
+
usdValue: nativeUsdValue ? `$${nativeUsdValue.toFixed(2)}` : undefined,
|
|
439
|
+
},
|
|
440
|
+
tokens: tokensWithUsd,
|
|
441
|
+
chainTotalUsd: chainTotalUsd > 0 ? `$${chainTotalUsd.toFixed(2)}` : undefined,
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Query Solana balances if wallet has a Solana address
|
|
446
|
+
// Bankr wallets have a separate Solana address that can be cached
|
|
447
|
+
const solanaAddress = (wallet as any).solanaAddress;
|
|
448
|
+
if (solanaAddress) {
|
|
449
|
+
try {
|
|
450
|
+
const solanaBalances = await getSolanaWalletBalances(solanaAddress, includeZeroBalances);
|
|
451
|
+
if (solanaBalances) {
|
|
452
|
+
// Add USD values for Solana
|
|
453
|
+
let solanaTotalUsd = 0;
|
|
454
|
+
const solPrice = getPrice('SOL');
|
|
455
|
+
const nativeBalance = parseFloat(solanaBalances.native.balance);
|
|
456
|
+
const nativeUsdValue = solPrice ? nativeBalance * solPrice : null;
|
|
457
|
+
if (nativeUsdValue) solanaTotalUsd += nativeUsdValue;
|
|
458
|
+
|
|
459
|
+
const tokensWithUsd = solanaBalances.tokens.map((token) => {
|
|
460
|
+
const price = getPrice(token.symbol);
|
|
461
|
+
const balance = parseFloat(token.balance);
|
|
462
|
+
const usdValue = price ? balance * price : null;
|
|
463
|
+
if (usdValue) solanaTotalUsd += usdValue;
|
|
464
|
+
return { ...token, usdValue: usdValue ? `${usdValue.toFixed(2)}` : undefined };
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
walletBalanceUsd += solanaTotalUsd;
|
|
468
|
+
balancesWithUsd.push({
|
|
469
|
+
chainId: 'solana',
|
|
470
|
+
chainName: 'Solana',
|
|
471
|
+
native: {
|
|
472
|
+
symbol: solanaBalances.native.symbol,
|
|
473
|
+
balance: solanaBalances.native.balance,
|
|
474
|
+
usdValue: nativeUsdValue ? `${nativeUsdValue.toFixed(2)}` : undefined,
|
|
475
|
+
},
|
|
476
|
+
tokens: tokensWithUsd,
|
|
477
|
+
chainTotalUsd: solanaTotalUsd > 0 ? `${solanaTotalUsd.toFixed(2)}` : undefined,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.error(`[portfolio] Failed to get Solana balances for ${wallet.nickname}:`, err);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Get money market positions (aggregate)
|
|
486
|
+
let mmSummary: WalletBalanceResult['moneyMarket'] | undefined;
|
|
487
|
+
try {
|
|
488
|
+
const positions = await aggregateCrossChainPositions(wallet.nickname);
|
|
489
|
+
if (positions && (positions.summary.totalSupplyUsd > 0 || positions.summary.totalBorrowUsd > 0)) {
|
|
490
|
+
const hfStatus = getHealthFactorStatus(positions.summary.healthFactor);
|
|
491
|
+
// Build per-chain breakdown with individual health factors
|
|
492
|
+
const chainBreakdown = positions.chainSummaries.map(cs => ({
|
|
493
|
+
chainId: cs.chainId,
|
|
494
|
+
supplyUsd: cs.supplyUsd.toFixed(2),
|
|
495
|
+
borrowUsd: cs.borrowUsd.toFixed(2),
|
|
496
|
+
healthFactor: formatHealthFactor(cs.healthFactor),
|
|
497
|
+
healthStatus: getHealthFactorStatus(cs.healthFactor),
|
|
498
|
+
}));
|
|
499
|
+
mmSummary = {
|
|
500
|
+
totalSupplyUsd: positions.summary.totalSupplyUsd.toFixed(2),
|
|
501
|
+
totalBorrowUsd: positions.summary.totalBorrowUsd.toFixed(2),
|
|
502
|
+
netWorthUsd: positions.summary.netWorthUsd.toFixed(2),
|
|
503
|
+
healthFactor: formatHealthFactor(positions.summary.healthFactor),
|
|
504
|
+
healthStatus: hfStatus,
|
|
505
|
+
chainBreakdown,
|
|
506
|
+
};
|
|
507
|
+
// MM net worth is already USD - add to wallet total
|
|
508
|
+
walletBalanceUsd += positions.summary.netWorthUsd;
|
|
509
|
+
}
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.error(`[portfolio] Failed to get MM positions for ${wallet.nickname}:`, err);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
totalValueUsd += walletBalanceUsd;
|
|
515
|
+
|
|
516
|
+
results.push({
|
|
517
|
+
wallet: {
|
|
518
|
+
nickname: wallet.nickname,
|
|
519
|
+
address: wallet.address,
|
|
520
|
+
type: wallet.type,
|
|
521
|
+
},
|
|
522
|
+
balances: balancesWithUsd as WalletBalanceResult['balances'],
|
|
523
|
+
moneyMarket: mmSummary,
|
|
524
|
+
walletTotalUsd: walletBalanceUsd > 0 ? `$${walletBalanceUsd.toFixed(2)}` : undefined,
|
|
525
|
+
} as WalletBalanceResult);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Build summary
|
|
529
|
+
const summary = {
|
|
530
|
+
walletCount: results.length,
|
|
531
|
+
chainsQueried: chainIdsToQuery.length,
|
|
532
|
+
timestamp: new Date().toISOString(),
|
|
533
|
+
estimatedTotalUsd: totalValueUsd > 0 ? `$${totalValueUsd.toFixed(2)}` : 'No positions',
|
|
534
|
+
priceSource: priceMap ? 'SODAX' : 'unavailable',
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
success: true,
|
|
539
|
+
summary,
|
|
540
|
+
wallets: results,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ============================================================================
|
|
545
|
+
// Solana Balance Functions
|
|
546
|
+
// ============================================================================
|
|
547
|
+
|
|
548
|
+
const SOLANA_RPC_URL = 'https://api.mainnet-beta.solana.com';
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Major SPL tokens to check on Solana
|
|
552
|
+
*/
|
|
553
|
+
const SOLANA_TOKENS: Array<{ mint: string; symbol: string; decimals: number }> = [
|
|
554
|
+
{ mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', symbol: 'USDC', decimals: 6 },
|
|
555
|
+
{ mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', symbol: 'USDT', decimals: 6 },
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Get native SOL balance for a Solana wallet
|
|
560
|
+
*/
|
|
561
|
+
async function getSolanaBalance(
|
|
562
|
+
address: string
|
|
563
|
+
): Promise<{ symbol: string; balance: string; balanceRaw: bigint }> {
|
|
564
|
+
try {
|
|
565
|
+
const response = await fetch(SOLANA_RPC_URL, {
|
|
566
|
+
method: 'POST',
|
|
567
|
+
headers: { 'Content-Type': 'application/json' },
|
|
568
|
+
body: JSON.stringify({
|
|
569
|
+
jsonrpc: '2.0',
|
|
570
|
+
id: 1,
|
|
571
|
+
method: 'getBalance',
|
|
572
|
+
params: [address],
|
|
573
|
+
}),
|
|
574
|
+
});
|
|
575
|
+
const json = await response.json() as { result?: { value: number } };
|
|
576
|
+
const lamports = BigInt(json.result?.value || 0);
|
|
577
|
+
// SOL has 9 decimals
|
|
578
|
+
const balance = Number(lamports) / 1e9;
|
|
579
|
+
return {
|
|
580
|
+
symbol: 'SOL',
|
|
581
|
+
balance: balance.toFixed(6),
|
|
582
|
+
balanceRaw: lamports,
|
|
583
|
+
};
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error('[portfolio] Failed to get SOL balance:', error);
|
|
586
|
+
return { symbol: 'SOL', balance: '0', balanceRaw: 0n };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Get SPL token balances for a Solana wallet
|
|
592
|
+
*/
|
|
593
|
+
async function getSolanaTokenBalances(
|
|
594
|
+
address: string
|
|
595
|
+
): Promise<Array<{ symbol: string; balance: string; address: string }>> {
|
|
596
|
+
const results: Array<{ symbol: string; balance: string; address: string }> = [];
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
// Query all token accounts owned by this wallet
|
|
600
|
+
const response = await fetch(SOLANA_RPC_URL, {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: { 'Content-Type': 'application/json' },
|
|
603
|
+
body: JSON.stringify({
|
|
604
|
+
jsonrpc: '2.0',
|
|
605
|
+
id: 1,
|
|
606
|
+
method: 'getTokenAccountsByOwner',
|
|
607
|
+
params: [
|
|
608
|
+
address,
|
|
609
|
+
{ programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' },
|
|
610
|
+
{ encoding: 'jsonParsed' },
|
|
611
|
+
],
|
|
612
|
+
}),
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const json = await response.json() as {
|
|
616
|
+
result?: {
|
|
617
|
+
value: Array<{
|
|
618
|
+
account: {
|
|
619
|
+
data: {
|
|
620
|
+
parsed: {
|
|
621
|
+
info: {
|
|
622
|
+
mint: string;
|
|
623
|
+
tokenAmount: { uiAmount: number; decimals: number };
|
|
624
|
+
};
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
};
|
|
628
|
+
}>;
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const accounts = json.result?.value || [];
|
|
633
|
+
|
|
634
|
+
// Match against known tokens
|
|
635
|
+
for (const tokenConfig of SOLANA_TOKENS) {
|
|
636
|
+
const account = accounts.find(
|
|
637
|
+
(a) => a.account.data.parsed.info.mint === tokenConfig.mint
|
|
638
|
+
);
|
|
639
|
+
if (account) {
|
|
640
|
+
const amount = account.account.data.parsed.info.tokenAmount.uiAmount || 0;
|
|
641
|
+
if (amount > 0) {
|
|
642
|
+
results.push({
|
|
643
|
+
symbol: tokenConfig.symbol,
|
|
644
|
+
balance: amount.toFixed(6),
|
|
645
|
+
address: tokenConfig.mint,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error('[portfolio] Failed to get Solana token balances:', error);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return results;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Get all Solana balances for a wallet
|
|
659
|
+
*/
|
|
660
|
+
export async function getSolanaWalletBalances(
|
|
661
|
+
address: string,
|
|
662
|
+
includeZeroBalances: boolean = false
|
|
663
|
+
): Promise<{
|
|
664
|
+
chainId: string;
|
|
665
|
+
chainName: string;
|
|
666
|
+
native: { symbol: string; balance: string; usdValue?: string };
|
|
667
|
+
tokens: Array<{ symbol: string; balance: string; address: string; usdValue?: string }>;
|
|
668
|
+
chainTotalUsd?: string;
|
|
669
|
+
} | null> {
|
|
670
|
+
// Validate Solana address format (base58, 32-44 chars)
|
|
671
|
+
if (!address || address.startsWith('0x') || address.length < 32 || address.length > 44) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const native = await getSolanaBalance(address);
|
|
677
|
+
const tokens = await getSolanaTokenBalances(address);
|
|
678
|
+
|
|
679
|
+
// Skip if no balances and not including zeros
|
|
680
|
+
if (!includeZeroBalances && native.balanceRaw === 0n && tokens.length === 0) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
chainId: 'solana',
|
|
686
|
+
chainName: 'Solana',
|
|
687
|
+
native: {
|
|
688
|
+
symbol: native.symbol,
|
|
689
|
+
balance: native.balance,
|
|
690
|
+
},
|
|
691
|
+
tokens,
|
|
692
|
+
};
|
|
693
|
+
} catch (error) {
|
|
694
|
+
console.error('[portfolio] Failed to get Solana balances:', error);
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
}
|