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.
Files changed (189) hide show
  1. package/README.md +757 -0
  2. package/dist/__mocks__/@sodax/sdk.d.ts +24 -0
  3. package/dist/__mocks__/@sodax/sdk.d.ts.map +1 -0
  4. package/dist/__mocks__/@sodax/sdk.js +24 -0
  5. package/dist/__mocks__/@sodax/sdk.js.map +1 -0
  6. package/dist/__tests__/setup.d.ts +4 -0
  7. package/dist/__tests__/setup.d.ts.map +1 -0
  8. package/dist/__tests__/setup.js +32 -0
  9. package/dist/__tests__/setup.js.map +1 -0
  10. package/dist/index.d.ts +66 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +281 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/policy/policyEngine.d.ts +119 -0
  15. package/dist/policy/policyEngine.d.ts.map +1 -0
  16. package/dist/policy/policyEngine.js +322 -0
  17. package/dist/policy/policyEngine.js.map +1 -0
  18. package/dist/providers/spokeProviderFactory.d.ts +38 -0
  19. package/dist/providers/spokeProviderFactory.d.ts.map +1 -0
  20. package/dist/providers/spokeProviderFactory.js +212 -0
  21. package/dist/providers/spokeProviderFactory.js.map +1 -0
  22. package/dist/sodax/client.d.ts +34 -0
  23. package/dist/sodax/client.d.ts.map +1 -0
  24. package/dist/sodax/client.js +99 -0
  25. package/dist/sodax/client.js.map +1 -0
  26. package/dist/tools/bridge.d.ts +105 -0
  27. package/dist/tools/bridge.d.ts.map +1 -0
  28. package/dist/tools/bridge.js +334 -0
  29. package/dist/tools/bridge.js.map +1 -0
  30. package/dist/tools/discovery.d.ts +141 -0
  31. package/dist/tools/discovery.d.ts.map +1 -0
  32. package/dist/tools/discovery.js +777 -0
  33. package/dist/tools/discovery.js.map +1 -0
  34. package/dist/tools/moneyMarket.d.ts +227 -0
  35. package/dist/tools/moneyMarket.d.ts.map +1 -0
  36. package/dist/tools/moneyMarket.js +867 -0
  37. package/dist/tools/moneyMarket.js.map +1 -0
  38. package/dist/tools/portfolio.d.ts +43 -0
  39. package/dist/tools/portfolio.d.ts.map +1 -0
  40. package/dist/tools/portfolio.js +538 -0
  41. package/dist/tools/portfolio.js.map +1 -0
  42. package/dist/tools/swap.d.ts +71 -0
  43. package/dist/tools/swap.d.ts.map +1 -0
  44. package/dist/tools/swap.js +762 -0
  45. package/dist/tools/swap.js.map +1 -0
  46. package/dist/tools/walletManagement.d.ts +80 -0
  47. package/dist/tools/walletManagement.d.ts.map +1 -0
  48. package/dist/tools/walletManagement.js +289 -0
  49. package/dist/tools/walletManagement.js.map +1 -0
  50. package/dist/types.d.ts +205 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +5 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/utils/errorUtils.d.ts +2 -0
  55. package/dist/utils/errorUtils.d.ts.map +1 -0
  56. package/dist/utils/errorUtils.js +19 -0
  57. package/dist/utils/errorUtils.js.map +1 -0
  58. package/dist/utils/errors.d.ts +144 -0
  59. package/dist/utils/errors.d.ts.map +1 -0
  60. package/dist/utils/errors.js +310 -0
  61. package/dist/utils/errors.js.map +1 -0
  62. package/dist/utils/positionAggregator.d.ts +122 -0
  63. package/dist/utils/positionAggregator.d.ts.map +1 -0
  64. package/dist/utils/positionAggregator.js +377 -0
  65. package/dist/utils/positionAggregator.js.map +1 -0
  66. package/dist/utils/priceService.d.ts +45 -0
  67. package/dist/utils/priceService.d.ts.map +1 -0
  68. package/dist/utils/priceService.js +108 -0
  69. package/dist/utils/priceService.js.map +1 -0
  70. package/dist/utils/sodaxApi.d.ts +92 -0
  71. package/dist/utils/sodaxApi.d.ts.map +1 -0
  72. package/dist/utils/sodaxApi.js +143 -0
  73. package/dist/utils/sodaxApi.js.map +1 -0
  74. package/dist/utils/tokenResolver.d.ts +54 -0
  75. package/dist/utils/tokenResolver.d.ts.map +1 -0
  76. package/dist/utils/tokenResolver.js +252 -0
  77. package/dist/utils/tokenResolver.js.map +1 -0
  78. package/dist/wallet/backendConfig.d.ts +37 -0
  79. package/dist/wallet/backendConfig.d.ts.map +1 -0
  80. package/dist/wallet/backendConfig.js +125 -0
  81. package/dist/wallet/backendConfig.js.map +1 -0
  82. package/dist/wallet/backends/BankrBackend.d.ts +73 -0
  83. package/dist/wallet/backends/BankrBackend.d.ts.map +1 -0
  84. package/dist/wallet/backends/BankrBackend.js +315 -0
  85. package/dist/wallet/backends/BankrBackend.js.map +1 -0
  86. package/dist/wallet/backends/BankrWalletProvider.d.ts +75 -0
  87. package/dist/wallet/backends/BankrWalletProvider.d.ts.map +1 -0
  88. package/dist/wallet/backends/BankrWalletProvider.js +243 -0
  89. package/dist/wallet/backends/BankrWalletProvider.js.map +1 -0
  90. package/dist/wallet/backends/EnvBackend.d.ts +50 -0
  91. package/dist/wallet/backends/EnvBackend.d.ts.map +1 -0
  92. package/dist/wallet/backends/EnvBackend.js +114 -0
  93. package/dist/wallet/backends/EnvBackend.js.map +1 -0
  94. package/dist/wallet/backends/EvmWalletSkillBackend.d.ts +40 -0
  95. package/dist/wallet/backends/EvmWalletSkillBackend.d.ts.map +1 -0
  96. package/dist/wallet/backends/EvmWalletSkillBackend.js +81 -0
  97. package/dist/wallet/backends/EvmWalletSkillBackend.js.map +1 -0
  98. package/dist/wallet/backends/index.d.ts +10 -0
  99. package/dist/wallet/backends/index.d.ts.map +1 -0
  100. package/dist/wallet/backends/index.js +10 -0
  101. package/dist/wallet/backends/index.js.map +1 -0
  102. package/dist/wallet/index.d.ts +9 -0
  103. package/dist/wallet/index.d.ts.map +1 -0
  104. package/dist/wallet/index.js +12 -0
  105. package/dist/wallet/index.js.map +1 -0
  106. package/dist/wallet/providers/AmpedWalletProvider.d.ts +107 -0
  107. package/dist/wallet/providers/AmpedWalletProvider.d.ts.map +1 -0
  108. package/dist/wallet/providers/AmpedWalletProvider.js +208 -0
  109. package/dist/wallet/providers/AmpedWalletProvider.js.map +1 -0
  110. package/dist/wallet/providers/BankrBackend.d.ts +105 -0
  111. package/dist/wallet/providers/BankrBackend.d.ts.map +1 -0
  112. package/dist/wallet/providers/BankrBackend.js +327 -0
  113. package/dist/wallet/providers/BankrBackend.js.map +1 -0
  114. package/dist/wallet/providers/LocalKeyBackend.d.ts +62 -0
  115. package/dist/wallet/providers/LocalKeyBackend.d.ts.map +1 -0
  116. package/dist/wallet/providers/LocalKeyBackend.js +152 -0
  117. package/dist/wallet/providers/LocalKeyBackend.js.map +1 -0
  118. package/dist/wallet/providers/chainConfig.d.ts +209 -0
  119. package/dist/wallet/providers/chainConfig.d.ts.map +1 -0
  120. package/dist/wallet/providers/chainConfig.js +175 -0
  121. package/dist/wallet/providers/chainConfig.js.map +1 -0
  122. package/dist/wallet/providers/index.d.ts +30 -0
  123. package/dist/wallet/providers/index.d.ts.map +1 -0
  124. package/dist/wallet/providers/index.js +32 -0
  125. package/dist/wallet/providers/index.js.map +1 -0
  126. package/dist/wallet/providers/types.d.ts +156 -0
  127. package/dist/wallet/providers/types.d.ts.map +1 -0
  128. package/dist/wallet/providers/types.js +11 -0
  129. package/dist/wallet/providers/types.js.map +1 -0
  130. package/dist/wallet/skillWalletAdapter.d.ts +96 -0
  131. package/dist/wallet/skillWalletAdapter.d.ts.map +1 -0
  132. package/dist/wallet/skillWalletAdapter.js +280 -0
  133. package/dist/wallet/skillWalletAdapter.js.map +1 -0
  134. package/dist/wallet/types.d.ts +134 -0
  135. package/dist/wallet/types.d.ts.map +1 -0
  136. package/dist/wallet/types.js +138 -0
  137. package/dist/wallet/types.js.map +1 -0
  138. package/dist/wallet/walletManager.d.ts +111 -0
  139. package/dist/wallet/walletManager.d.ts.map +1 -0
  140. package/dist/wallet/walletManager.js +476 -0
  141. package/dist/wallet/walletManager.js.map +1 -0
  142. package/dist/wallet/walletRegistry.d.ts +95 -0
  143. package/dist/wallet/walletRegistry.d.ts.map +1 -0
  144. package/dist/wallet/walletRegistry.js +184 -0
  145. package/dist/wallet/walletRegistry.js.map +1 -0
  146. package/index.js +2 -0
  147. package/openclaw.plugin.json +37 -0
  148. package/package.json +69 -0
  149. package/src/__mocks__/@sodax/sdk.ts +28 -0
  150. package/src/__tests__/errors.test.ts +238 -0
  151. package/src/__tests__/policyEngine.test.ts +354 -0
  152. package/src/__tests__/positionAggregator.test.ts +271 -0
  153. package/src/__tests__/setup.ts +35 -0
  154. package/src/__tests__/sodaxApi.test.ts +203 -0
  155. package/src/__tests__/walletRegistry.test.ts +155 -0
  156. package/src/index.ts +376 -0
  157. package/src/policy/policyEngine.ts +389 -0
  158. package/src/providers/spokeProviderFactory.ts +283 -0
  159. package/src/sodax/client.ts +113 -0
  160. package/src/tools/bridge.ts +425 -0
  161. package/src/tools/discovery.ts +989 -0
  162. package/src/tools/moneyMarket.ts +1265 -0
  163. package/src/tools/portfolio.ts +697 -0
  164. package/src/tools/swap.ts +926 -0
  165. package/src/tools/walletManagement.ts +359 -0
  166. package/src/types.ts +228 -0
  167. package/src/utils/errorUtils.ts +16 -0
  168. package/src/utils/errors.ts +396 -0
  169. package/src/utils/positionAggregator.ts +559 -0
  170. package/src/utils/priceService.ts +153 -0
  171. package/src/utils/sodaxApi.ts +261 -0
  172. package/src/utils/tokenResolver.ts +286 -0
  173. package/src/wallet/backendConfig.ts +151 -0
  174. package/src/wallet/backends/BankrBackend.ts +399 -0
  175. package/src/wallet/backends/BankrWalletProvider.ts +329 -0
  176. package/src/wallet/backends/EnvBackend.ts +149 -0
  177. package/src/wallet/backends/EvmWalletSkillBackend.ts +110 -0
  178. package/src/wallet/backends/index.ts +10 -0
  179. package/src/wallet/index.ts +14 -0
  180. package/src/wallet/providers/AmpedWalletProvider.ts +267 -0
  181. package/src/wallet/providers/BankrBackend.ts +407 -0
  182. package/src/wallet/providers/LocalKeyBackend.ts +184 -0
  183. package/src/wallet/providers/chainConfig.ts +194 -0
  184. package/src/wallet/providers/index.ts +62 -0
  185. package/src/wallet/providers/types.ts +186 -0
  186. package/src/wallet/skillWalletAdapter.ts +335 -0
  187. package/src/wallet/types.ts +248 -0
  188. package/src/wallet/walletManager.ts +561 -0
  189. 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
+ }