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,559 @@
1
+ /**
2
+ * Cross-Chain Money Market Position Aggregator
3
+ *
4
+ * Aggregates user positions across all supported chains to provide a unified view
5
+ * of their money market portfolio, including:
6
+ * - Total supplied/borrowed across all chains
7
+ * - Health factor and liquidation risk
8
+ * - Available borrowing power
9
+ * - Net position (supply - borrow)
10
+ * - Cross-chain collateral utilization
11
+ */
12
+
13
+ import { getSodaxClient } from '../sodax/client';
14
+ import { getSpokeProvider } from '../providers/spokeProviderFactory';
15
+ import { getWalletManager } from '../wallet/walletManager';
16
+ import { normalizeChainId } from '../wallet/types';
17
+
18
+ /**
19
+ * Position data for a single token on a single chain
20
+ */
21
+ export interface TokenPosition {
22
+ chainId: string;
23
+ token: {
24
+ address: string;
25
+ symbol: string;
26
+ name: string;
27
+ decimals: number;
28
+ logoURI?: string;
29
+ };
30
+ supply: {
31
+ balance: string;
32
+ balanceUsd: string;
33
+ balanceRaw: string;
34
+ apy: number;
35
+ isCollateral: boolean;
36
+ };
37
+ borrow: {
38
+ balance: string;
39
+ balanceUsd: string;
40
+ balanceRaw: string;
41
+ apy: number;
42
+ };
43
+ loanToValue: number;
44
+ liquidationThreshold: number;
45
+ }
46
+
47
+ /**
48
+ * Aggregated position summary across all chains
49
+ */
50
+ export interface AggregatedPositionSummary {
51
+ totalSupplyUsd: number;
52
+ totalBorrowUsd: number;
53
+ netWorthUsd: number;
54
+ availableBorrowUsd: number;
55
+ healthFactor: number | null;
56
+ liquidationRisk: 'none' | 'low' | 'medium' | 'high';
57
+ weightedSupplyApy: number;
58
+ weightedBorrowApy: number;
59
+ netApy: number;
60
+ }
61
+
62
+ /**
63
+ * Chain-specific position summary
64
+ */
65
+ export interface ChainPositionSummary {
66
+ chainId: string;
67
+ supplyUsd: number;
68
+ borrowUsd: number;
69
+ netWorthUsd: number;
70
+ healthFactor: number | null;
71
+ positionCount: number;
72
+ }
73
+
74
+ /**
75
+ * Complete cross-chain position view
76
+ */
77
+ export interface CrossChainPositionView {
78
+ walletId: string;
79
+ address: string;
80
+ timestamp: string;
81
+ summary: AggregatedPositionSummary;
82
+ chainSummaries: ChainPositionSummary[];
83
+ positions: TokenPosition[];
84
+ collateralUtilization: {
85
+ totalCollateralUsd: number;
86
+ usedCollateralUsd: number;
87
+ availableCollateralUsd: number;
88
+ utilizationRate: number;
89
+ };
90
+ riskMetrics: {
91
+ maxLtv: number;
92
+ currentLtv: number;
93
+ bufferUntilLiquidation: number;
94
+ safeMaxBorrowUsd: number;
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Options for position aggregation
100
+ */
101
+ export interface AggregationOptions {
102
+ /** Specific chains to query (defaults to all supported chains) */
103
+ chainIds?: string[];
104
+ /** Include zero-balance positions */
105
+ includeZeroBalances?: boolean;
106
+ /** Minimum USD value to include (positions below this are filtered out unless includeZeroBalances is true) */
107
+ minUsdValue?: number;
108
+ }
109
+
110
+ // ============================================================================
111
+ // Position Aggregation Functions
112
+ // ============================================================================
113
+
114
+ /**
115
+ * Aggregate money market positions across all supported chains
116
+ *
117
+ * @param walletId - The wallet identifier
118
+ * @param options - Aggregation options
119
+ * @returns Complete cross-chain position view
120
+ */
121
+ export async function aggregateCrossChainPositions(
122
+ walletId: string,
123
+ options: AggregationOptions = {}
124
+ ): Promise<CrossChainPositionView> {
125
+ const startTime = Date.now();
126
+
127
+ // Get wallet
128
+ const walletManager = getWalletManager();
129
+ const wallet = await walletManager.resolve(walletId);
130
+ const walletAddress = await wallet.getAddress();
131
+
132
+ // Get supported chains from SODAX
133
+ const sodax = getSodaxClient();
134
+ const sodaxChains = sodax.config.getSupportedSpokeChains();
135
+
136
+ // Map SDK chains to string IDs
137
+ const allSodaxChains = sodaxChains.map((c: any) =>
138
+ typeof c === 'string' ? c : c.id
139
+ );
140
+
141
+ // Filter chains by what the wallet supports
142
+ // This is important for Bankr which only supports ethereum/polygon/base
143
+ const walletSupportedChains = wallet.supportedChains;
144
+ const filteredChains = allSodaxChains.filter((chainId: string) =>
145
+ wallet.supportsChain(normalizeChainId(chainId))
146
+ );
147
+
148
+ // Determine which chains to query
149
+ const chainsToQuery = options.chainIds || filteredChains;
150
+
151
+ console.log('[positionAggregator] Wallet chain filter', {
152
+ walletType: wallet.type,
153
+ walletSupports: walletSupportedChains,
154
+ sodaxChains: allSodaxChains,
155
+ filteredChains: filteredChains,
156
+ normalizedFiltered: filteredChains.map(normalizeChainId),
157
+ });
158
+
159
+ console.log('[positionAggregator] Querying positions across chains', {
160
+ walletId,
161
+ address: walletAddress,
162
+ chains: chainsToQuery,
163
+ });
164
+
165
+ // Query positions from all chains in parallel
166
+ const chainResults = await Promise.allSettled(
167
+ chainsToQuery.map(chainId => queryChainPositions(walletId, walletAddress, chainId))
168
+ );
169
+
170
+ // Collect all positions
171
+ const allPositions: TokenPosition[] = [];
172
+ const chainSummaries: ChainPositionSummary[] = [];
173
+
174
+ chainResults.forEach((result, index) => {
175
+ const chainId = chainsToQuery[index];
176
+
177
+ if (result.status === 'fulfilled') {
178
+ const { positions, summary } = result.value;
179
+
180
+ if (positions.length > 0 || options.includeZeroBalances) {
181
+ allPositions.push(...positions);
182
+ chainSummaries.push(summary);
183
+ }
184
+ } else {
185
+ console.warn(`[positionAggregator] Failed to query chain ${chainId}:`, result.reason);
186
+ }
187
+ });
188
+
189
+ // Calculate aggregated summary
190
+ const summary = calculateAggregatedSummary(allPositions);
191
+
192
+ // Calculate collateral utilization
193
+ const collateralUtilization = calculateCollateralUtilization(allPositions, summary);
194
+
195
+ // Calculate risk metrics
196
+ const riskMetrics = calculateRiskMetrics(allPositions, summary);
197
+
198
+ const view: CrossChainPositionView = {
199
+ walletId,
200
+ address: walletAddress,
201
+ timestamp: new Date().toISOString(),
202
+ summary,
203
+ chainSummaries: chainSummaries.sort((a, b) => b.netWorthUsd - a.netWorthUsd),
204
+ positions: allPositions.sort((a, b) =>
205
+ (parseFloat(b.supply.balanceUsd) + parseFloat(b.borrow.balanceUsd)) -
206
+ (parseFloat(a.supply.balanceUsd) + parseFloat(a.borrow.balanceUsd))
207
+ ),
208
+ collateralUtilization,
209
+ riskMetrics,
210
+ };
211
+
212
+ console.log('[positionAggregator] Aggregation complete', {
213
+ durationMs: Date.now() - startTime,
214
+ totalPositions: allPositions.length,
215
+ totalSupplyUsd: summary.totalSupplyUsd,
216
+ totalBorrowUsd: summary.totalBorrowUsd,
217
+ healthFactor: summary.healthFactor,
218
+ });
219
+
220
+ return view;
221
+ }
222
+
223
+ /**
224
+ * Query positions for a single chain
225
+ *
226
+ * IMPORTANT: getUserReservesHumanized() only returns raw balances without token metadata.
227
+ * To get token symbols/names, we must:
228
+ * 1. Fetch getReservesHumanized() for token metadata
229
+ * 2. Format with formatReservesUSD(buildReserveDataWithPrice())
230
+ * 3. Join with formatUserSummary(buildUserSummaryRequest())
231
+ *
232
+ * Reference: sodax-frontend/packages/dapp-kit/src/hooks/mm/useUserFormattedSummary.ts
233
+ */
234
+ async function queryChainPositions(
235
+ walletId: string,
236
+ address: string,
237
+ chainId: string
238
+ ): Promise<{ positions: TokenPosition[]; summary: ChainPositionSummary }> {
239
+ try {
240
+ // Use address for spoke provider lookup
241
+ const spokeProvider = await getSpokeProvider(walletId, chainId);
242
+ const sodax = getSodaxClient();
243
+
244
+ // Step 1: Fetch reserves with token metadata (symbols, names, decimals)
245
+ // This is the key fix - getUserReservesHumanized alone doesn't include token metadata
246
+ const reserves = await sodax.moneyMarket.data.getReservesHumanized();
247
+
248
+ // Step 2: Format reserves with USD prices
249
+ const formattedReserves = sodax.moneyMarket.data.formatReservesUSD(
250
+ sodax.moneyMarket.data.buildReserveDataWithPrice(reserves)
251
+ );
252
+
253
+ // Step 3: Fetch user-specific balances
254
+ const userReserves = await sodax.moneyMarket.data.getUserReservesHumanized(spokeProvider);
255
+
256
+ // Step 4: Join reserves metadata with user balances via formatUserSummary
257
+ const userSummary = sodax.moneyMarket.data.formatUserSummary(
258
+ sodax.moneyMarket.data.buildUserSummaryRequest(reserves, formattedReserves, userReserves)
259
+ );
260
+
261
+ // Extract user reserves from the formatted summary
262
+ // The formatted summary has userReservesData with proper token metadata
263
+ const userReservesData = (userSummary as any).userReservesData || [];
264
+
265
+ // Convert to TokenPosition format
266
+ const positions: TokenPosition[] = userReservesData.map((reserve: any) => {
267
+ // Get supply balance (underlyingBalance is the human-readable supply amount)
268
+ const supplyBalance = reserve.underlyingBalance || '0';
269
+ const supplyBalanceUsd = reserve.underlyingBalanceUSD || '0';
270
+
271
+ // Get borrow balance (variableBorrows is the human-readable borrow amount)
272
+ const borrowBalance = reserve.variableBorrows || reserve.totalBorrows || '0';
273
+ const borrowBalanceUsd = reserve.variableBorrowsUSD || reserve.totalBorrowsUSD || '0';
274
+
275
+ // Get APY values (formatted reserves have these)
276
+ const supplyApy = parseFloat(reserve.reserve?.supplyAPY || '0') * 100;
277
+ const borrowApy = parseFloat(reserve.reserve?.variableBorrowAPY || '0') * 100;
278
+
279
+ return {
280
+ chainId,
281
+ token: {
282
+ address: reserve.underlyingAsset || reserve.reserve?.underlyingAsset || '',
283
+ symbol: reserve.reserve?.symbol || '',
284
+ name: reserve.reserve?.name || '',
285
+ decimals: reserve.reserve?.decimals || 18,
286
+ logoURI: reserve.reserve?.iconSymbol || undefined,
287
+ },
288
+ supply: {
289
+ balance: supplyBalance,
290
+ balanceUsd: supplyBalanceUsd,
291
+ balanceRaw: reserve.scaledATokenBalance || '0',
292
+ apy: supplyApy,
293
+ isCollateral: reserve.usageAsCollateralEnabledOnUser ?? false,
294
+ },
295
+ borrow: {
296
+ balance: borrowBalance,
297
+ balanceUsd: borrowBalanceUsd,
298
+ balanceRaw: reserve.scaledVariableDebt || '0',
299
+ apy: borrowApy,
300
+ },
301
+ loanToValue: parseFloat(reserve.reserve?.baseLTVasCollateral || '0') / 10000,
302
+ liquidationThreshold: parseFloat(reserve.reserve?.reserveLiquidationThreshold || '0') / 10000,
303
+ };
304
+ });
305
+
306
+ // Filter out positions with zero balance (unless explicitly requested)
307
+ const activePositions = positions.filter(p =>
308
+ parseFloat(p.supply.balance) > 0 || parseFloat(p.borrow.balance) > 0
309
+ );
310
+
311
+ // Calculate chain summary
312
+ const supplyUsd = activePositions.reduce((sum, p) => sum + parseFloat(p.supply.balanceUsd || '0'), 0);
313
+ const borrowUsd = activePositions.reduce((sum, p) => sum + parseFloat(p.borrow.balanceUsd || '0'), 0);
314
+
315
+ // Calculate health factor for this chain
316
+ const healthFactor = calculateChainHealthFactor(activePositions);
317
+
318
+ const summary: ChainPositionSummary = {
319
+ chainId,
320
+ supplyUsd,
321
+ borrowUsd,
322
+ netWorthUsd: supplyUsd - borrowUsd,
323
+ healthFactor,
324
+ positionCount: activePositions.length,
325
+ };
326
+
327
+ console.log(`[positionAggregator] Chain ${chainId}: ${activePositions.length} positions, supply=$${supplyUsd.toFixed(2)}, borrow=$${borrowUsd.toFixed(2)}`);
328
+
329
+ return { positions: activePositions, summary };
330
+ } catch (error) {
331
+ console.error(`[positionAggregator] Error querying ${chainId}:`, error);
332
+ throw error;
333
+ }
334
+ }
335
+
336
+ // ============================================================================
337
+ // Calculation Helpers
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Calculate aggregated summary across all positions
342
+ */
343
+ function calculateAggregatedSummary(positions: TokenPosition[]): AggregatedPositionSummary {
344
+ let totalSupplyUsd = 0;
345
+ let totalBorrowUsd = 0;
346
+ let weightedSupplyApy = 0;
347
+ let weightedBorrowApy = 0;
348
+
349
+ positions.forEach(pos => {
350
+ const supplyUsd = parseFloat(pos.supply.balanceUsd || '0');
351
+ const borrowUsd = parseFloat(pos.borrow.balanceUsd || '0');
352
+
353
+ totalSupplyUsd += supplyUsd;
354
+ totalBorrowUsd += borrowUsd;
355
+ weightedSupplyApy += supplyUsd * pos.supply.apy;
356
+ weightedBorrowApy += borrowUsd * pos.borrow.apy;
357
+ });
358
+
359
+ // Calculate weighted average APYs
360
+ const avgSupplyApy = totalSupplyUsd > 0 ? weightedSupplyApy / totalSupplyUsd : 0;
361
+ const avgBorrowApy = totalBorrowUsd > 0 ? weightedBorrowApy / totalBorrowUsd : 0;
362
+
363
+ // Calculate health factor
364
+ const healthFactor = calculateHealthFactor(positions);
365
+
366
+ // Determine liquidation risk
367
+ let liquidationRisk: AggregatedPositionSummary['liquidationRisk'] = 'none';
368
+ if (healthFactor !== null) {
369
+ if (healthFactor < 1.1) liquidationRisk = 'high';
370
+ else if (healthFactor < 1.5) liquidationRisk = 'medium';
371
+ else if (healthFactor < 2) liquidationRisk = 'low';
372
+ }
373
+
374
+ // Calculate available borrow (simplified - would need proper oracle prices)
375
+ // This is a conservative estimate based on average LTV
376
+ const avgLtv = positions.length > 0
377
+ ? positions.reduce((sum, p) => sum + p.loanToValue, 0) / positions.length
378
+ : 0;
379
+ const availableBorrowUsd = totalSupplyUsd * avgLtv - totalBorrowUsd;
380
+
381
+ return {
382
+ totalSupplyUsd,
383
+ totalBorrowUsd,
384
+ netWorthUsd: totalSupplyUsd - totalBorrowUsd,
385
+ availableBorrowUsd: Math.max(0, availableBorrowUsd),
386
+ healthFactor,
387
+ liquidationRisk,
388
+ weightedSupplyApy: avgSupplyApy,
389
+ weightedBorrowApy: avgBorrowApy,
390
+ netApy: totalSupplyUsd > 0
391
+ ? (avgSupplyApy * totalSupplyUsd - avgBorrowApy * totalBorrowUsd) / totalSupplyUsd
392
+ : 0,
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Calculate collateral utilization metrics
398
+ */
399
+ function calculateCollateralUtilization(
400
+ positions: TokenPosition[],
401
+ summary: AggregatedPositionSummary
402
+ ): CrossChainPositionView['collateralUtilization'] {
403
+ // Only count collateral-enabled supplies
404
+ const totalCollateralUsd = positions
405
+ .filter(p => p.supply.isCollateral)
406
+ .reduce((sum, p) => sum + parseFloat(p.supply.balanceUsd || '0'), 0);
407
+
408
+ const usedCollateralUsd = summary.totalBorrowUsd;
409
+ const availableCollateralUsd = Math.max(0, totalCollateralUsd - usedCollateralUsd);
410
+ const utilizationRate = totalCollateralUsd > 0 ? (usedCollateralUsd / totalCollateralUsd) * 100 : 0;
411
+
412
+ return {
413
+ totalCollateralUsd,
414
+ usedCollateralUsd,
415
+ availableCollateralUsd,
416
+ utilizationRate,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Calculate risk metrics
422
+ */
423
+ function calculateRiskMetrics(
424
+ positions: TokenPosition[],
425
+ summary: AggregatedPositionSummary
426
+ ): CrossChainPositionView['riskMetrics'] {
427
+ // Calculate max LTV across all positions (weighted by supply)
428
+ let totalSupply = 0;
429
+ let weightedLtvSum = 0;
430
+ let liquidationThresholdSum = 0;
431
+
432
+ positions.forEach(pos => {
433
+ const supplyUsd = parseFloat(pos.supply.balanceUsd || '0');
434
+ totalSupply += supplyUsd;
435
+ weightedLtvSum += supplyUsd * pos.loanToValue;
436
+ liquidationThresholdSum += supplyUsd * pos.liquidationThreshold;
437
+ });
438
+
439
+ const maxLtv = totalSupply > 0 ? weightedLtvSum / totalSupply : 0;
440
+ const avgLiquidationThreshold = totalSupply > 0 ? liquidationThresholdSum / totalSupply : 0;
441
+
442
+ // Current LTV
443
+ const currentLtv = summary.totalSupplyUsd > 0
444
+ ? summary.totalBorrowUsd / summary.totalSupplyUsd
445
+ : 0;
446
+
447
+ // Buffer until liquidation (percentage points)
448
+ const bufferUntilLiquidation = Math.max(0, avgLiquidationThreshold - currentLtv) * 100;
449
+
450
+ // Safe max borrow (at 80% of liquidation threshold for safety)
451
+ const safeMaxBorrowUsd = summary.totalSupplyUsd * avgLiquidationThreshold * 0.8;
452
+
453
+ return {
454
+ maxLtv,
455
+ currentLtv,
456
+ bufferUntilLiquidation,
457
+ safeMaxBorrowUsd,
458
+ };
459
+ }
460
+
461
+ /**
462
+ * Calculate health factor for a set of positions
463
+ * Health Factor = (Total Collateral in ETH * Liquidation Threshold) / Total Borrow in ETH
464
+ */
465
+ function calculateHealthFactor(positions: TokenPosition[]): number | null {
466
+ let totalCollateralEth = 0;
467
+ let totalBorrowEth = 0;
468
+
469
+ positions.forEach(pos => {
470
+ const supplyUsd = parseFloat(pos.supply.balanceUsd || '0');
471
+ const borrowUsd = parseFloat(pos.borrow.balanceUsd || '0');
472
+
473
+ // Only count collateral-enabled supplies
474
+ if (pos.supply.isCollateral) {
475
+ totalCollateralEth += supplyUsd * pos.liquidationThreshold;
476
+ }
477
+ totalBorrowEth += borrowUsd;
478
+ });
479
+
480
+ if (totalBorrowEth === 0) {
481
+ return totalCollateralEth > 0 ? Infinity : null;
482
+ }
483
+
484
+ return totalCollateralEth / totalBorrowEth;
485
+ }
486
+
487
+ /**
488
+ * Calculate health factor for a single chain
489
+ */
490
+ function calculateChainHealthFactor(positions: TokenPosition[]): number | null {
491
+ return calculateHealthFactor(positions);
492
+ }
493
+
494
+ // ============================================================================
495
+ // Utility Functions
496
+ // ============================================================================
497
+
498
+ /**
499
+ * Format health factor for display
500
+ */
501
+ export function formatHealthFactor(hf: number | null): string {
502
+ if (hf === null) return 'N/A';
503
+ if (hf === Infinity) return '∞';
504
+ return hf.toFixed(2);
505
+ }
506
+
507
+ /**
508
+ * Get health factor color/styling indicator
509
+ */
510
+ export function getHealthFactorStatus(hf: number | null): {
511
+ status: 'healthy' | 'caution' | 'danger' | 'critical';
512
+ color: 'green' | 'yellow' | 'orange' | 'red';
513
+ } {
514
+ if (hf === null) return { status: 'healthy', color: 'green' };
515
+ if (hf === Infinity) return { status: 'healthy', color: 'green' };
516
+ if (hf < 1.1) return { status: 'critical', color: 'red' };
517
+ if (hf < 1.5) return { status: 'danger', color: 'orange' };
518
+ if (hf < 2) return { status: 'caution', color: 'yellow' };
519
+ return { status: 'healthy', color: 'green' };
520
+ }
521
+
522
+ /**
523
+ * Get recommendation based on position health
524
+ */
525
+ export function getPositionRecommendation(view: CrossChainPositionView): string[] {
526
+ const recommendations: string[] = [];
527
+ const { summary } = view;
528
+
529
+ // Health factor recommendations
530
+ if (summary.healthFactor !== null && summary.healthFactor < 1.5) {
531
+ recommendations.push('⚠️ Health factor is low. Consider repaying debt or adding collateral.');
532
+ }
533
+
534
+ // Borrowing capacity recommendations
535
+ if (summary.availableBorrowUsd > 1000 && summary.healthFactor !== null && summary.healthFactor > 2) {
536
+ recommendations.push(`💡 You have $${summary.availableBorrowUsd.toFixed(2)} in available borrowing power.`);
537
+ }
538
+
539
+ // Collateral utilization
540
+ if (view.collateralUtilization.utilizationRate > 80) {
541
+ recommendations.push('⚠️ High collateral utilization. Avoid borrowing more to maintain safety margin.');
542
+ }
543
+
544
+ // Net APY optimization
545
+ if (summary.netApy < 0) {
546
+ recommendations.push('📉 Your borrowing costs exceed supply earnings. Consider reducing debt or finding higher APY supply opportunities.');
547
+ }
548
+
549
+ // Cross-chain opportunities
550
+ const highApyChains = view.chainSummaries
551
+ .filter(cs => cs.supplyUsd > 100)
552
+ .sort((a, b) => (b.healthFactor || Infinity) - (a.healthFactor || Infinity));
553
+
554
+ if (highApyChains.length > 1) {
555
+ recommendations.push(`🌐 You have positions across ${highApyChains.length} chains. Monitor each chain's health factor independently.`);
556
+ }
557
+
558
+ return recommendations;
559
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Price Service - Fetches USD prices from SODAX reserves
3
+ *
4
+ * Uses the money market reserve data to get accurate USD prices
5
+ * for tokens supported by the protocol.
6
+ *
7
+ * @module utils/priceService
8
+ */
9
+
10
+ import { getSodaxClient } from '../sodax/client';
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export interface TokenPrice {
17
+ symbol: string;
18
+ priceUsd: number;
19
+ underlyingAsset: string;
20
+ }
21
+
22
+ export interface PriceMap {
23
+ /** Map of symbol (lowercase) to USD price */
24
+ bySymbol: Map<string, number>;
25
+ /** Map of address (lowercase) to USD price */
26
+ byAddress: Map<string, number>;
27
+ /** Last update timestamp */
28
+ timestamp: number;
29
+ }
30
+
31
+ // ============================================================================
32
+ // Cache
33
+ // ============================================================================
34
+
35
+ let cachedPrices: PriceMap | null = null;
36
+ const CACHE_TTL_MS = 60_000; // 1 minute cache
37
+
38
+ // ============================================================================
39
+ // Price Fetching
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Fetch token prices from SODAX money market reserves
44
+ *
45
+ * The reserves contain `priceInMarketReferenceCurrency` which represents
46
+ * the price in 8 decimal USD (100000000 = $1.00)
47
+ */
48
+ export async function fetchTokenPrices(): Promise<PriceMap> {
49
+ // Return cached if fresh
50
+ if (cachedPrices && Date.now() - cachedPrices.timestamp < CACHE_TTL_MS) {
51
+ return cachedPrices;
52
+ }
53
+
54
+ console.log('[priceService] Fetching token prices from SODAX reserves');
55
+
56
+ const sodax = await getSodaxClient();
57
+ const reserves = await sodax.moneyMarket.data.getReservesHumanized();
58
+
59
+ const bySymbol = new Map<string, number>();
60
+ const byAddress = new Map<string, number>();
61
+
62
+ // Market reference currency decimals (typically 8)
63
+ const PRICE_DECIMALS = 8;
64
+
65
+ for (const reserve of reserves.reservesData) {
66
+ // priceInMarketReferenceCurrency is a string representing the raw value
67
+ const priceRaw = BigInt(reserve.priceInMarketReferenceCurrency);
68
+ const priceUsd = Number(priceRaw) / Math.pow(10, PRICE_DECIMALS);
69
+
70
+ // Use symbol for matching (e.g., "sodaUSDC" -> "USDC")
71
+ const symbol = reserve.symbol.toLowerCase();
72
+ const normalizedSymbol = normalizeSymbol(reserve.symbol);
73
+ const address = reserve.underlyingAsset.toLowerCase();
74
+
75
+ bySymbol.set(symbol, priceUsd);
76
+ bySymbol.set(normalizedSymbol, priceUsd);
77
+ byAddress.set(address, priceUsd);
78
+
79
+ console.log(`[priceService] ${reserve.symbol}: $${priceUsd.toFixed(4)}`);
80
+ }
81
+
82
+ cachedPrices = {
83
+ bySymbol,
84
+ byAddress,
85
+ timestamp: Date.now(),
86
+ };
87
+
88
+ console.log(`[priceService] Cached ${bySymbol.size} token prices`);
89
+
90
+ return cachedPrices;
91
+ }
92
+
93
+ /**
94
+ * Normalize SODAX symbol to standard symbol
95
+ * e.g., "sodaUSDC" -> "usdc", "sodaETH" -> "eth"
96
+ */
97
+ function normalizeSymbol(symbol: string): string {
98
+ const lower = symbol.toLowerCase();
99
+ if (lower.startsWith('soda')) {
100
+ return lower.slice(4); // Remove 'soda' prefix
101
+ }
102
+ return lower;
103
+ }
104
+
105
+ /**
106
+ * Get USD price for a token by symbol
107
+ */
108
+ export async function getTokenPriceBySymbol(symbol: string): Promise<number | null> {
109
+ const prices = await fetchTokenPrices();
110
+ const normalizedSymbol = symbol.toLowerCase();
111
+
112
+ // Try exact match first
113
+ if (prices.bySymbol.has(normalizedSymbol)) {
114
+ return prices.bySymbol.get(normalizedSymbol)!;
115
+ }
116
+
117
+ // Try with 'soda' prefix
118
+ const sodaSymbol = 'soda' + normalizedSymbol;
119
+ if (prices.bySymbol.has(sodaSymbol)) {
120
+ return prices.bySymbol.get(sodaSymbol)!;
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Get USD price for a token by address
128
+ */
129
+ export async function getTokenPriceByAddress(address: string): Promise<number | null> {
130
+ const prices = await fetchTokenPrices();
131
+ return prices.byAddress.get(address.toLowerCase()) ?? null;
132
+ }
133
+
134
+ /**
135
+ * Calculate USD value for a token amount
136
+ */
137
+ export async function calculateUsdValue(
138
+ symbol: string,
139
+ amount: string | number
140
+ ): Promise<number | null> {
141
+ const price = await getTokenPriceBySymbol(symbol);
142
+ if (price === null) return null;
143
+
144
+ const amountNum = typeof amount === 'string' ? parseFloat(amount) : amount;
145
+ return amountNum * price;
146
+ }
147
+
148
+ /**
149
+ * Clear the price cache (useful for testing)
150
+ */
151
+ export function clearPriceCache(): void {
152
+ cachedPrices = null;
153
+ }