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,1265 @@
1
+ /**
2
+ * Money Market Tools for Amped DeFi Plugin
3
+ *
4
+ * Provides advanced supply, withdraw, borrow, and repay operations for the SODAX money market.
5
+ * Supports both same-chain and cross-chain operations (e.g., supply on Chain A, borrow to Chain B).
6
+ *
7
+ * Key capabilities:
8
+ * - Supply: Deposit tokens as collateral on any supported chain
9
+ * - Borrow: Borrow tokens to any chain (cross-chain capable)
10
+ * - Withdraw: Withdraw supplied tokens from any chain
11
+ * - Repay: Repay borrowed tokens from any chain
12
+ * - Intent-based operations: Create intents for custom flows
13
+ *
14
+ * Cross-chain flows:
15
+ * 1. Supply on Chain A → Borrow to Chain B (different destination)
16
+ * 2. Supply on Chain A → Borrow on Chain A (same chain)
17
+ * 3. Cross-chain repay: Repay debt from any chain
18
+ * 4. Cross-chain withdraw: Withdraw collateral to any chain
19
+ */
20
+
21
+ import { Type, Static } from "@sinclair/typebox";
22
+ import { getSodaxClient } from "../sodax/client";
23
+ import { getSpokeProvider } from "../providers/spokeProviderFactory";
24
+ import { PolicyEngine } from "../policy/policyEngine";
25
+ import { getWalletManager } from '../wallet/walletManager';
26
+ import { AgentTools } from "../types";
27
+ import { serializeError } from '../utils/errorUtils';
28
+ import { resolveToken, getTokenInfo } from '../utils/tokenResolver';
29
+ import { toSodaxChainId } from '../wallet/types';
30
+
31
+ // ============================================================================
32
+ // TypeBox Schemas
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Base schema for money market operations
37
+ */
38
+ const MoneyMarketBaseSchema = Type.Object({
39
+ walletId: Type.String({
40
+ description: "Unique identifier for the wallet"
41
+ }),
42
+ chainId: Type.String({
43
+ description: "Source SODAX spoke chain ID where the operation originates (e.g., 'ethereum', 'arbitrum', 'sonic')"
44
+ }),
45
+ token: Type.String({
46
+ description: "Token address or symbol to supply/borrow/withdraw/repay",
47
+ }),
48
+ amount: Type.String({
49
+ description: "Amount in human-readable units (e.g., '100.5' for 100.5 USDC). Use '-1' for max repay (repay full debt).",
50
+ }),
51
+ timeoutMs: Type.Optional(
52
+ Type.Number({
53
+ description: "Operation timeout in milliseconds",
54
+ default: 180000,
55
+ })
56
+ ),
57
+ policyId: Type.Optional(
58
+ Type.String({ description: "Optional policy profile identifier for custom limits" })
59
+ ),
60
+ skipSimulation: Type.Optional(
61
+ Type.Boolean({
62
+ description: "Skip transaction simulation (not recommended)",
63
+ default: false,
64
+ })
65
+ ),
66
+ });
67
+
68
+ /**
69
+ * Supply operation schema
70
+ * Supply tokens as collateral to the money market on the specified chain
71
+ */
72
+ const MoneyMarketSupplySchema = Type.Composite([
73
+ MoneyMarketBaseSchema,
74
+ Type.Object({
75
+ useAsCollateral: Type.Optional(
76
+ Type.Boolean({
77
+ description: "Whether to use the supplied tokens as collateral for borrowing (default: true)",
78
+ default: true,
79
+ })
80
+ ),
81
+ // Cross-chain supply options
82
+ dstChainId: Type.Optional(
83
+ Type.String({
84
+ description: "Optional destination chain for the supply operation. If different from chainId, performs cross-chain supply.",
85
+ })
86
+ ),
87
+ recipient: Type.Optional(
88
+ Type.String({
89
+ description: "Optional recipient address for the supplied position (defaults to wallet address)",
90
+ })
91
+ ),
92
+ }),
93
+ ]);
94
+
95
+ /**
96
+ * Withdraw operation schema
97
+ * Withdraw supplied tokens from the money market
98
+ */
99
+ const MoneyMarketWithdrawSchema = Type.Composite([
100
+ MoneyMarketBaseSchema,
101
+ Type.Object({
102
+ withdrawType: Type.Optional(
103
+ Type.Union([
104
+ Type.Literal('default'),
105
+ Type.Literal('collateral'),
106
+ Type.Literal('all'),
107
+ ], {
108
+ description: "Withdraw type: 'default' (standard), 'collateral' (withdraw collateral only), 'all' (withdraw maximum)",
109
+ default: 'default',
110
+ })
111
+ ),
112
+ // Cross-chain withdraw options
113
+ dstChainId: Type.Optional(
114
+ Type.String({
115
+ description: "Optional destination chain to receive withdrawn tokens. If different from chainId, performs cross-chain withdraw.",
116
+ })
117
+ ),
118
+ recipient: Type.Optional(
119
+ Type.String({
120
+ description: "Optional recipient address to receive withdrawn tokens (defaults to wallet address)",
121
+ })
122
+ ),
123
+ }),
124
+ ]);
125
+
126
+ /**
127
+ * Borrow operation schema
128
+ * Borrow tokens from the money market
129
+ *
130
+ * Key feature: Can borrow to a DIFFERENT chain than where collateral is supplied!
131
+ * Example: Supply USDC on Ethereum, borrow USDT to Arbitrum
132
+ */
133
+ const MoneyMarketBorrowSchema = Type.Composite([
134
+ MoneyMarketBaseSchema,
135
+ Type.Object({
136
+ interestRateMode: Type.Optional(
137
+ Type.Union([
138
+ Type.Literal(1, { description: "Stable interest rate" }),
139
+ Type.Literal(2, { description: "Variable interest rate (recommended)" }),
140
+ ], {
141
+ description: "Interest rate mode: 1 = Stable, 2 = Variable",
142
+ default: 2,
143
+ })
144
+ ),
145
+ referralCode: Type.Optional(
146
+ Type.String({
147
+ description: "Optional referral code for the borrow operation",
148
+ })
149
+ ),
150
+ // Cross-chain borrow options (key feature!)
151
+ dstChainId: Type.Optional(
152
+ Type.String({
153
+ description: "Destination chain to receive borrowed tokens. If different from chainId, performs cross-chain borrow (supply on chainId, receive borrowed tokens on dstChainId).",
154
+ })
155
+ ),
156
+ recipient: Type.Optional(
157
+ Type.String({
158
+ description: "Optional recipient address to receive borrowed tokens (defaults to wallet address on dstChainId or chainId)",
159
+ })
160
+ ),
161
+ }),
162
+ ]);
163
+
164
+ /**
165
+ * Repay operation schema
166
+ * Repay borrowed tokens to the money market
167
+ */
168
+ const MoneyMarketRepaySchema = Type.Composite([
169
+ MoneyMarketBaseSchema,
170
+ Type.Object({
171
+ interestRateMode: Type.Optional(
172
+ Type.Union([
173
+ Type.Literal(1, { description: "Stable interest rate" }),
174
+ Type.Literal(2, { description: "Variable interest rate" }),
175
+ ], {
176
+ description: "Interest rate mode of the debt to repay: 1 = Stable, 2 = Variable",
177
+ default: 2,
178
+ })
179
+ ),
180
+ repayAll: Type.Optional(
181
+ Type.Boolean({
182
+ description: "If true, repays the full debt amount (useful for closing position)",
183
+ default: false,
184
+ })
185
+ ),
186
+ // Cross-chain repay options
187
+ collateralChainId: Type.Optional(
188
+ Type.String({
189
+ description: "Optional chain ID where collateral is held (for cross-chain repay scenarios)",
190
+ })
191
+ ),
192
+ }),
193
+ ]);
194
+
195
+ /**
196
+ * Create Intent schemas for advanced users
197
+ * These allow building custom multi-step flows
198
+ */
199
+ const CreateSupplyIntentSchema = Type.Composite([
200
+ MoneyMarketSupplySchema,
201
+ Type.Object({
202
+ raw: Type.Optional(
203
+ Type.Boolean({
204
+ description: "If true, returns raw transaction data instead of executing (for custom signing flows)",
205
+ default: false,
206
+ })
207
+ ),
208
+ }),
209
+ ]);
210
+
211
+ const CreateBorrowIntentSchema = Type.Composite([
212
+ MoneyMarketBorrowSchema,
213
+ Type.Object({
214
+ raw: Type.Optional(
215
+ Type.Boolean({
216
+ description: "If true, returns raw transaction data instead of executing (for custom signing flows)",
217
+ default: false,
218
+ })
219
+ ),
220
+ }),
221
+ ]);
222
+
223
+ const CreateWithdrawIntentSchema = Type.Composite([
224
+ MoneyMarketWithdrawSchema,
225
+ Type.Object({
226
+ raw: Type.Optional(
227
+ Type.Boolean({
228
+ description: "If true, returns raw transaction data instead of executing (for custom signing flows)",
229
+ default: false,
230
+ })
231
+ ),
232
+ }),
233
+ ]);
234
+
235
+ const CreateRepayIntentSchema = Type.Composite([
236
+ MoneyMarketRepaySchema,
237
+ Type.Object({
238
+ raw: Type.Optional(
239
+ Type.Boolean({
240
+ description: "If true, returns raw transaction data instead of executing (for custom signing flows)",
241
+ default: false,
242
+ })
243
+ ),
244
+ }),
245
+ ]);
246
+
247
+ // ============================================================================
248
+ // Output Types
249
+ // ============================================================================
250
+
251
+ interface MoneyMarketOperationResult {
252
+ success: boolean;
253
+ txHash?: string;
254
+ status: "success" | "pending" | "failed";
255
+ spokeTxHash?: string;
256
+ hubTxHash?: string;
257
+ intentHash?: string;
258
+ operation: string;
259
+ chainId: string;
260
+ dstChainId?: string;
261
+ token: string;
262
+ amount: string;
263
+ message?: string;
264
+ warnings?: string[];
265
+ // Cross-chain specific
266
+ isCrossChain?: boolean;
267
+ srcSpokeTxHash?: string;
268
+ dstSpokeTxHash?: string;
269
+ // Raw intent data (for createIntent operations)
270
+ rawIntent?: unknown;
271
+ }
272
+
273
+ interface IntentResult extends MoneyMarketOperationResult {
274
+ intentData: unknown;
275
+ requiresSubmission: boolean;
276
+ }
277
+
278
+ // ============================================================================
279
+ // Helper Functions
280
+ // ============================================================================
281
+
282
+ /**
283
+ * Converts human-readable amount to token units (wei)
284
+ */
285
+ function parseTokenAmount(amount: string, decimals: number = 18): bigint {
286
+ // Handle special case for max repay
287
+ if (amount === '-1') {
288
+ return BigInt(-1);
289
+ }
290
+
291
+ const parsed = parseFloat(amount);
292
+ if (isNaN(parsed)) {
293
+ throw new Error(`Invalid amount: ${amount}`);
294
+ }
295
+
296
+ const multiplier = Math.pow(10, decimals);
297
+ return BigInt(Math.floor(parsed * multiplier));
298
+ }
299
+
300
+ /**
301
+ * Resolves wallet and creates spoke provider for the operation
302
+ */
303
+ async function resolveWalletAndProvider(
304
+ walletId: string,
305
+ chainId: string
306
+ ): Promise<{
307
+ walletAddress: string;
308
+ spokeProvider: any;
309
+ }> {
310
+ const walletManager = getWalletManager();
311
+ const wallet = await walletManager.resolve(walletId);
312
+ const walletAddress = await wallet.getAddress();
313
+
314
+ const spokeProvider = await getSpokeProvider(walletId, chainId);
315
+
316
+ return { walletAddress, spokeProvider };
317
+ }
318
+
319
+ /**
320
+ * Common pre-operation checks and allowance handling
321
+ */
322
+ async function prepareMoneyMarketOperation(
323
+ walletId: string,
324
+ chainId: string,
325
+ token: string,
326
+ amount: string,
327
+ operation: "supply" | "withdraw" | "borrow" | "repay",
328
+ policyId?: string
329
+ ): Promise<{ walletAddress: string; spokeProvider: any; policyResult: any; tokenAddr: string }> {
330
+ // ============================================================================
331
+ // Hub Chain Validation
332
+ // ============================================================================
333
+ // SODAX architecture: Money market operations must be initiated from spoke chains,
334
+ // not the hub chain (Sonic). The hub chain is the settlement layer where contracts
335
+ // live, but users interact via spoke chains that relay operations to the hub.
336
+ // Reference: sodax-tests/tests/crossChainSdk.test.ts explicitly omits SONIC_MAINNET_CHAIN_ID
337
+ const isHubChainSource = chainId.toLowerCase() === 'sonic' || chainId === '146';
338
+ if (isHubChainSource) {
339
+ throw new Error(
340
+ `Money market operations cannot be initiated from the hub chain (Sonic). ` +
341
+ `Please use a spoke chain (base, arbitrum, ethereum, optimism, avalanche, bsc, polygon) as the source chain.`
342
+ );
343
+ }
344
+ // Ensure sodax client is initialized
345
+ const _sodaxClient = getSodaxClient(); // Just verify it's ready
346
+ void _sodaxClient;
347
+
348
+ // Normalize chain ID to SDK format for token resolution
349
+ const sdkChainId = toSodaxChainId(chainId);
350
+
351
+ // Resolve token symbol to address
352
+ const tokenAddr = await resolveToken(sdkChainId, token);
353
+
354
+ // Resolve wallet and create spoke provider
355
+ const { walletAddress, spokeProvider } = await resolveWalletAndProvider(walletId, chainId);
356
+
357
+ // Policy check
358
+ const policyEngine = new PolicyEngine();
359
+ const policyResult = await policyEngine.checkMoneyMarket({
360
+ walletId,
361
+ chainId,
362
+ token,
363
+ amount, // Add required amount parameter
364
+ amountUsd: parseFloat(amount), // Simplified - would need actual price lookup
365
+ operation,
366
+ policyId,
367
+ });
368
+
369
+ if (!policyResult.allowed) {
370
+ throw new Error(
371
+ `Policy check failed: ${policyResult.reason || "Operation not permitted"}.`
372
+ );
373
+ }
374
+
375
+ return { walletAddress, spokeProvider, policyResult, tokenAddr };
376
+ }
377
+ /**
378
+ * Resolves token and returns its decimals
379
+ * Falls back to 18 decimals if token info not found
380
+ */
381
+ async function getTokenDecimals(
382
+ chainId: string,
383
+ token: string
384
+ ): Promise<number> {
385
+ try {
386
+ const sdkChainId = toSodaxChainId(chainId);
387
+ const tokenInfo = await getTokenInfo(sdkChainId, token);
388
+ return tokenInfo?.decimals ?? 18;
389
+ } catch {
390
+ // If token info lookup fails, fall back to 18 decimals
391
+ return 18;
392
+ }
393
+ }
394
+
395
+
396
+ /**
397
+ * Checks and handles token approval if needed
398
+ */
399
+ async function ensureAllowance(
400
+ params: {
401
+ token: string;
402
+ amount: bigint;
403
+ action: 'supply' | 'repay';
404
+ },
405
+ spokeProvider: any,
406
+ raw: boolean = false
407
+ ): Promise<{ approvalTxHash?: string; rawApproval?: unknown }> {
408
+ const sodaxClient = await getSodaxClient();
409
+
410
+ // Check if allowance is sufficient
411
+ const isAllowanceValid = await sodaxClient.moneyMarket.isAllowanceValid(
412
+ params,
413
+ spokeProvider
414
+ );
415
+
416
+ if (!isAllowanceValid.ok || !isAllowanceValid.value) {
417
+ if (raw) {
418
+ // Return raw approval transaction
419
+ const rawApproval = await sodaxClient.moneyMarket.approve(
420
+ params,
421
+ spokeProvider,
422
+ true // raw mode
423
+ );
424
+ return { rawApproval };
425
+ } else {
426
+ // Execute approval transaction
427
+ const approvalResult = await sodaxClient.moneyMarket.approve(
428
+ params,
429
+ spokeProvider,
430
+ false
431
+ );
432
+ // Handle Result type from SDK
433
+ const txHash = (approvalResult as any).ok
434
+ ? (approvalResult as any).value
435
+ : (approvalResult as any).txHash || approvalResult;
436
+ return { approvalTxHash: String(txHash) };
437
+ }
438
+ }
439
+
440
+ return {};
441
+ }
442
+
443
+ /**
444
+ * Determine if operation is cross-chain
445
+ */
446
+ function isCrossChainOperation(srcChainId: string, dstChainId?: string): boolean {
447
+ return !!dstChainId && dstChainId !== srcChainId;
448
+ }
449
+
450
+ // ============================================================================
451
+ // Tool Handlers
452
+ // ============================================================================
453
+
454
+ /**
455
+ * Supply tokens to the money market
456
+ *
457
+ * Supports cross-chain supply: supply tokens on chainId, collateral is recorded on dstChainId (if different)
458
+ */
459
+ async function handleSupply(
460
+ params: Static<typeof MoneyMarketSupplySchema>
461
+ ): Promise<MoneyMarketOperationResult> {
462
+ const {
463
+ walletId,
464
+ chainId,
465
+ token,
466
+ amount,
467
+ timeoutMs = 180000,
468
+ policyId,
469
+ useAsCollateral = true,
470
+ dstChainId,
471
+ recipient,
472
+ skipSimulation = false
473
+ } = params;
474
+
475
+ const crossChain = isCrossChainOperation(chainId, dstChainId);
476
+ const warnings: string[] = [];
477
+
478
+ try {
479
+ // Pre-operation checks
480
+ const { walletAddress, spokeProvider, tokenAddr } = await prepareMoneyMarketOperation(
481
+ walletId,
482
+ chainId,
483
+ token,
484
+ amount,
485
+ "supply",
486
+ policyId
487
+ );
488
+
489
+ // Parse amount with actual token decimals
490
+ const decimals = await getTokenDecimals(chainId, token);
491
+ const amountBigInt = parseTokenAmount(amount, decimals);
492
+
493
+ // Check allowance for supply
494
+ const { approvalTxHash } = await ensureAllowance(
495
+ { token: tokenAddr, amount: amountBigInt, action: 'supply' },
496
+ spokeProvider
497
+ );
498
+
499
+ if (approvalTxHash) {
500
+ warnings.push(`Approval transaction executed: ${approvalTxHash}`);
501
+ }
502
+
503
+ const sodaxClient = await getSodaxClient();
504
+
505
+ // Build supply parameters
506
+ const supplyParams: any = {
507
+ action: 'supply',
508
+ token: tokenAddr,
509
+ amount: amountBigInt,
510
+
511
+ toAddress: recipient || walletAddress,
512
+ };
513
+
514
+ // Add cross-chain parameters if applicable
515
+ if (crossChain && dstChainId) {
516
+ supplyParams.toChainId = toSodaxChainId(dstChainId);
517
+ warnings.push(`Cross-chain supply: tokens supplied on ${chainId}, collateral recorded on ${dstChainId}`);
518
+ }
519
+
520
+ // Check and handle allowance (required for ALL supply operations)
521
+ // Reference: sodax-frontend moneymarket-ops.ts - supply ALWAYS checks allowance
522
+ try {
523
+ const allowanceResult = await (sodaxClient as any).moneyMarket.isAllowanceValid(
524
+ { token: tokenAddr, amount: amountBigInt, action: 'supply' },
525
+ spokeProvider
526
+ );
527
+
528
+ if (allowanceResult.ok && !allowanceResult.value) {
529
+ console.log('[mm:supply] Approval needed, approving...');
530
+ const approveResult = await (sodaxClient as any).moneyMarket.approve(
531
+ { token: tokenAddr, amount: amountBigInt, action: 'supply' },
532
+ spokeProvider
533
+ );
534
+
535
+ if (!approveResult.ok) {
536
+ throw new Error(`Approval failed: ${serializeError(approveResult.error)}`);
537
+ }
538
+
539
+ // Wait for approval confirmation
540
+ const approvalTxHash = approveResult.value;
541
+ if (approvalTxHash && spokeProvider.walletProvider?.waitForTransactionReceipt) {
542
+ await spokeProvider.walletProvider.waitForTransactionReceipt(approvalTxHash);
543
+ console.log('[mm:supply] Approval confirmed');
544
+ }
545
+ }
546
+ } catch (allowanceError) {
547
+ console.warn('[mm:supply] Allowance check failed, proceeding anyway:', allowanceError);
548
+ }
549
+
550
+ // Execute supply
551
+ const supplyResult = await (sodaxClient as any).moneyMarket.supply(
552
+ supplyParams,
553
+ spokeProvider,
554
+ timeoutMs
555
+ );
556
+
557
+ // Handle Result type from SDK
558
+ if (supplyResult.ok === false) {
559
+ throw new Error(`Supply failed: ${serializeError(supplyResult.error)}`);
560
+ }
561
+
562
+ const value = supplyResult.ok ? supplyResult.value : supplyResult;
563
+ // SDK may return [spokeTxHash, hubTxHash] tuple
564
+ const [solverResponse, intent, deliveryInfo] = Array.isArray(value) ? value : [value, undefined, undefined];
565
+
566
+ // Extract tx hashes from deliveryInfo (SDK returns 3-element tuple)
567
+ const spokeTxHash = deliveryInfo?.srcTxHash || (solverResponse as any)?.txHash || solverResponse;
568
+ const hubTxHash = deliveryInfo?.hubTxHash || (intent as any)?.hubTxHash;
569
+ const dstTxHash = deliveryInfo?.dstTxHash;
570
+
571
+ return {
572
+ success: true,
573
+ txHash: String(spokeTxHash),
574
+ status: "success",
575
+ spokeTxHash: String(spokeTxHash),
576
+ hubTxHash: hubTxHash ? String(hubTxHash) : undefined,
577
+ intentHash: undefined,
578
+ operation: "supply",
579
+ chainId,
580
+ dstChainId: dstChainId || chainId,
581
+ token,
582
+ amount,
583
+ isCrossChain: crossChain,
584
+ message: crossChain
585
+ ? `Successfully supplied ${amount} ${token} on ${chainId}. Collateral available on ${dstChainId || chainId}.`
586
+ : `Successfully supplied ${amount} ${token} to money market on ${chainId}`,
587
+ warnings: warnings.length > 0 ? warnings : undefined,
588
+ };
589
+ } catch (error) {
590
+ const errorMessage = error instanceof Error ? error.message : "Unknown error during supply";
591
+ return {
592
+ success: false,
593
+ status: "failed",
594
+ operation: "supply",
595
+ chainId,
596
+ dstChainId: dstChainId || chainId,
597
+ token,
598
+ amount,
599
+ isCrossChain: crossChain,
600
+ message: `Money market supply failed: ${errorMessage}`,
601
+ };
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Withdraw tokens from the money market
607
+ *
608
+ * Supports cross-chain withdraw: withdraw collateral from chainId, receive tokens on dstChainId
609
+ */
610
+ async function handleWithdraw(
611
+ params: Static<typeof MoneyMarketWithdrawSchema>
612
+ ): Promise<MoneyMarketOperationResult> {
613
+ const {
614
+ walletId,
615
+ chainId,
616
+ token,
617
+ amount,
618
+ timeoutMs = 180000,
619
+ policyId,
620
+ withdrawType = 'default',
621
+ dstChainId,
622
+ recipient,
623
+ skipSimulation = false
624
+ } = params;
625
+
626
+ const crossChain = isCrossChainOperation(chainId, dstChainId);
627
+ const warnings: string[] = [];
628
+
629
+ try {
630
+ // Pre-operation checks
631
+ const { walletAddress, spokeProvider, tokenAddr } = await prepareMoneyMarketOperation(
632
+ walletId,
633
+ chainId,
634
+ token,
635
+ amount,
636
+ "withdraw",
637
+ policyId
638
+ );
639
+
640
+ // Parse amount with actual token decimals
641
+ const decimals = await getTokenDecimals(chainId, token);
642
+ const amountBigInt = parseTokenAmount(amount, decimals);
643
+
644
+ const sodaxClient = await getSodaxClient();
645
+
646
+ // Build withdraw parameters
647
+ const withdrawParams: any = {
648
+ action: 'withdraw',
649
+ token: tokenAddr,
650
+ amount: amountBigInt,
651
+
652
+ toAddress: recipient || walletAddress,
653
+ };
654
+
655
+ // Add cross-chain parameters if applicable
656
+ if (crossChain && dstChainId) {
657
+ withdrawParams.toChainId = toSodaxChainId(dstChainId);
658
+ warnings.push(`Cross-chain withdraw: withdrawing from ${chainId}, receiving tokens on ${dstChainId}`);
659
+ }
660
+
661
+ // Check and handle allowance (required for hub chain operations)
662
+ // Reference: sodax-frontend moneymarket-ops.ts
663
+ const isHubChain = chainId === 'sonic' || chainId === '146';
664
+ if (isHubChain) {
665
+ try {
666
+ const allowanceResult = await (sodaxClient as any).moneyMarket.isAllowanceValid(
667
+ { token: tokenAddr, amount: amountBigInt, action: 'withdraw' },
668
+ spokeProvider
669
+ );
670
+
671
+ if (allowanceResult.ok && !allowanceResult.value) {
672
+ console.log('[mm:withdraw] Approval needed, approving...');
673
+ const approveResult = await (sodaxClient as any).moneyMarket.approve(
674
+ { token: tokenAddr, amount: amountBigInt, action: 'withdraw' },
675
+ spokeProvider
676
+ );
677
+
678
+ if (!approveResult.ok) {
679
+ throw new Error(`Approval failed: ${serializeError(approveResult.error)}`);
680
+ }
681
+
682
+ // Wait for approval confirmation
683
+ const approvalTxHash = approveResult.value;
684
+ if (approvalTxHash && spokeProvider.walletProvider?.waitForTransactionReceipt) {
685
+ await spokeProvider.walletProvider.waitForTransactionReceipt(approvalTxHash);
686
+ console.log('[mm:withdraw] Approval confirmed');
687
+ }
688
+ }
689
+ } catch (allowanceError) {
690
+ console.warn('[mm:withdraw] Allowance check failed, proceeding anyway:', allowanceError);
691
+ }
692
+ }
693
+
694
+ // Execute withdraw
695
+ const withdrawResult = await (sodaxClient as any).moneyMarket.withdraw(
696
+ withdrawParams,
697
+ spokeProvider,
698
+ timeoutMs
699
+ );
700
+
701
+ // Handle Result type from SDK
702
+ if (withdrawResult.ok === false) {
703
+ throw new Error(`Withdraw failed: ${serializeError(withdrawResult.error)}`);
704
+ }
705
+
706
+ const value = withdrawResult.ok ? withdrawResult.value : withdrawResult;
707
+ const [solverResponse, intent, deliveryInfo] = Array.isArray(value) ? value : [value, undefined, undefined];
708
+
709
+ // Extract tx hashes from deliveryInfo (SDK returns 3-element tuple)
710
+ const spokeTxHash = deliveryInfo?.srcTxHash || (solverResponse as any)?.txHash || solverResponse;
711
+ const hubTxHash = deliveryInfo?.hubTxHash || (intent as any)?.hubTxHash;
712
+ const dstTxHash = deliveryInfo?.dstTxHash;
713
+
714
+ return {
715
+ success: true,
716
+ txHash: String(spokeTxHash),
717
+ status: "success",
718
+ spokeTxHash: String(spokeTxHash),
719
+ hubTxHash: hubTxHash ? String(hubTxHash) : undefined,
720
+ intentHash: undefined,
721
+ operation: "withdraw",
722
+ chainId,
723
+ dstChainId: dstChainId || chainId,
724
+ token,
725
+ amount,
726
+ isCrossChain: crossChain,
727
+ message: crossChain
728
+ ? `Successfully withdrew ${amount} ${token} from ${chainId} to ${dstChainId}`
729
+ : `Successfully withdrew ${amount} ${token} from money market on ${chainId}`,
730
+ warnings: warnings.length > 0 ? warnings : undefined,
731
+ };
732
+ } catch (error) {
733
+ const errorMessage = error instanceof Error ? error.message : "Unknown error during withdraw";
734
+ return {
735
+ success: false,
736
+ status: "failed",
737
+ operation: "withdraw",
738
+ chainId,
739
+ dstChainId: dstChainId || chainId,
740
+ token,
741
+ amount,
742
+ isCrossChain: crossChain,
743
+ message: `Money market withdraw failed: ${errorMessage}`,
744
+ };
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Borrow tokens from the money market
750
+ *
751
+ * KEY FEATURE: Can borrow to a DIFFERENT chain than where collateral is supplied!
752
+ * Example: Supply USDC on Ethereum (chainId), borrow USDT to Arbitrum (dstChainId)
753
+ *
754
+ * This is a powerful cross-chain DeFi primitive that allows:
755
+ * 1. Accessing liquidity without moving collateral
756
+ * 2. Arbitraging interest rates across chains
757
+ * 3. Efficient capital utilization across the entire SODAX network
758
+ */
759
+ async function handleBorrow(
760
+ params: Static<typeof MoneyMarketBorrowSchema>
761
+ ): Promise<MoneyMarketOperationResult> {
762
+ const {
763
+ walletId,
764
+ chainId,
765
+ token,
766
+ amount,
767
+ timeoutMs = 180000,
768
+ policyId,
769
+ interestRateMode = 2,
770
+ referralCode,
771
+ dstChainId, // KEY: This can be different from chainId for cross-chain borrow!
772
+ recipient,
773
+ skipSimulation = false
774
+ } = params;
775
+
776
+ const crossChain = isCrossChainOperation(chainId, dstChainId);
777
+ const warnings: string[] = [];
778
+
779
+ try {
780
+ // Pre-operation checks
781
+ const { walletAddress, spokeProvider, tokenAddr } = await prepareMoneyMarketOperation(
782
+ walletId,
783
+ chainId,
784
+ token,
785
+ amount,
786
+ "borrow",
787
+ policyId
788
+ );
789
+
790
+ // Parse amount with actual token decimals
791
+ const decimals = await getTokenDecimals(chainId, token);
792
+ const amountBigInt = parseTokenAmount(amount, decimals);
793
+
794
+ // Get user's positions to check health factor (best practice)
795
+ const sodaxClient = await getSodaxClient();
796
+
797
+ // For cross-chain borrow, resolve token on DESTINATION chain
798
+ // SDK expects: getMoneyMarketToken(toChainId, params.token)
799
+ // So params.token must be the destination chain's token address
800
+ let borrowTokenAddr = tokenAddr;
801
+ if (crossChain && dstChainId) {
802
+ borrowTokenAddr = await resolveToken(dstChainId, token);
803
+ console.log('[mm:borrow] Cross-chain: resolved token on destination chain', {
804
+ srcChain: chainId,
805
+ dstChain: dstChainId,
806
+ srcTokenAddr: tokenAddr,
807
+ dstTokenAddr: borrowTokenAddr,
808
+ });
809
+ }
810
+
811
+ // Build borrow parameters
812
+ const borrowParams: any = {
813
+ action: 'borrow',
814
+ token: borrowTokenAddr,
815
+ amount: amountBigInt,
816
+
817
+ toAddress: recipient || walletAddress,
818
+ };
819
+
820
+ // KEY CROSS-CHAIN FEATURE:
821
+ // If dstChainId is provided and different from chainId, the borrowed tokens
822
+ // will be delivered to dstChainId instead of chainId where the borrow is initiated
823
+ if (crossChain && dstChainId) {
824
+ borrowParams.toChainId = toSodaxChainId(dstChainId);
825
+ warnings.push(`Cross-chain borrow: Using collateral on ${chainId}, receiving borrowed tokens on ${dstChainId}`);
826
+ warnings.push(`Ensure you have sufficient collateral on ${chainId} to support this borrow`);
827
+ }
828
+
829
+ // Check and handle allowance (required for hub chain operations)
830
+ // Reference: sodax-frontend moneymarket-ops.ts
831
+ const isHubChain = chainId === 'sonic' || chainId === '146';
832
+ if (isHubChain) {
833
+ try {
834
+ const allowanceResult = await (sodaxClient as any).moneyMarket.isAllowanceValid(
835
+ { token: borrowTokenAddr, amount: amountBigInt, action: 'borrow' },
836
+ spokeProvider
837
+ );
838
+
839
+ if (allowanceResult.ok && !allowanceResult.value) {
840
+ console.log('[mm:borrow] Approval needed, approving...');
841
+ const approveResult = await (sodaxClient as any).moneyMarket.approve(
842
+ { token: borrowTokenAddr, amount: amountBigInt, action: 'borrow' },
843
+ spokeProvider
844
+ );
845
+
846
+ if (!approveResult.ok) {
847
+ throw new Error(`Approval failed: ${serializeError(approveResult.error)}`);
848
+ }
849
+
850
+ // Wait for approval confirmation
851
+ const approvalTxHash = approveResult.value;
852
+ if (approvalTxHash && spokeProvider.walletProvider?.waitForTransactionReceipt) {
853
+ await spokeProvider.walletProvider.waitForTransactionReceipt(approvalTxHash);
854
+ console.log('[mm:borrow] Approval confirmed');
855
+ }
856
+ }
857
+ } catch (allowanceError) {
858
+ console.warn('[mm:borrow] Allowance check failed, proceeding anyway:', allowanceError);
859
+ }
860
+ }
861
+
862
+ // Execute borrow
863
+ const borrowResult = await (sodaxClient as any).moneyMarket.borrow(
864
+ borrowParams,
865
+ spokeProvider,
866
+ timeoutMs
867
+ );
868
+
869
+ // Handle Result type from SDK
870
+ if (borrowResult.ok === false) {
871
+ throw new Error(`Borrow failed: ${serializeError(borrowResult.error)}`);
872
+ }
873
+
874
+ const value = borrowResult.ok ? borrowResult.value : borrowResult;
875
+ const [solverResponse, intent, deliveryInfo] = Array.isArray(value) ? value : [value, undefined, undefined];
876
+
877
+ // Extract tx hashes from deliveryInfo (SDK returns 3-element tuple)
878
+ const spokeTxHash = deliveryInfo?.srcTxHash || (solverResponse as any)?.txHash || solverResponse;
879
+ const hubTxHash = deliveryInfo?.hubTxHash || (intent as any)?.hubTxHash;
880
+ const dstTxHash = deliveryInfo?.dstTxHash;
881
+
882
+ return {
883
+ success: true,
884
+ txHash: String(spokeTxHash),
885
+ status: "success",
886
+ spokeTxHash: String(spokeTxHash),
887
+ hubTxHash: hubTxHash ? String(hubTxHash) : undefined,
888
+ intentHash: undefined,
889
+ operation: "borrow",
890
+ chainId,
891
+ dstChainId: dstChainId || chainId,
892
+ token,
893
+ amount,
894
+ isCrossChain: crossChain,
895
+ message: crossChain
896
+ ? `Successfully borrowed ${amount} ${token} on ${dstChainId} using collateral from ${chainId}. Interest rate mode: ${interestRateMode === 1 ? 'Stable' : 'Variable'}`
897
+ : `Successfully borrowed ${amount} ${token} from money market on ${chainId}. Interest rate mode: ${interestRateMode === 1 ? 'Stable' : 'Variable'}`,
898
+ warnings: warnings.length > 0 ? warnings : undefined,
899
+ };
900
+ } catch (error) {
901
+ const errorMessage = error instanceof Error ? error.message : "Unknown error during borrow";
902
+ return {
903
+ success: false,
904
+ status: "failed",
905
+ operation: "borrow",
906
+ chainId,
907
+ dstChainId: dstChainId || chainId,
908
+ token,
909
+ amount,
910
+ isCrossChain: crossChain,
911
+ message: `Money market borrow failed: ${errorMessage}`,
912
+ };
913
+ }
914
+ }
915
+
916
+ /**
917
+ * Repay borrowed tokens to the money market
918
+ *
919
+ * Supports cross-chain repay: repay debt using tokens from a different chain
920
+ */
921
+ async function handleRepay(
922
+ params: Static<typeof MoneyMarketRepaySchema>
923
+ ): Promise<MoneyMarketOperationResult> {
924
+ const {
925
+ walletId,
926
+ chainId,
927
+ token,
928
+ amount,
929
+ timeoutMs = 180000,
930
+ policyId,
931
+ interestRateMode = 2,
932
+ repayAll = false,
933
+ collateralChainId,
934
+ skipSimulation = false
935
+ } = params;
936
+
937
+ const crossChain = !!collateralChainId && collateralChainId !== chainId;
938
+ const warnings: string[] = [];
939
+
940
+ try {
941
+ // Pre-operation checks
942
+ const { walletAddress, spokeProvider, tokenAddr } = await prepareMoneyMarketOperation(
943
+ walletId,
944
+ chainId,
945
+ token,
946
+ amount,
947
+ "repay",
948
+ policyId
949
+ );
950
+
951
+ // Parse amount with actual token decimals (use -1 for max repay if repayAll is true)
952
+ const decimals = await getTokenDecimals(chainId, token);
953
+ const amountBigInt = repayAll ? BigInt(-1) : parseTokenAmount(amount, decimals);
954
+
955
+ // Check allowance for repay
956
+ const { approvalTxHash } = await ensureAllowance(
957
+ { token: tokenAddr, amount: amountBigInt === BigInt(-1) ? BigInt(0) : amountBigInt, action: 'repay' },
958
+ spokeProvider
959
+ );
960
+
961
+ if (approvalTxHash) {
962
+ warnings.push(`Approval transaction executed: ${approvalTxHash}`);
963
+ }
964
+
965
+ const sodaxClient = await getSodaxClient();
966
+
967
+ // Build repay parameters
968
+ const repayParams: any = {
969
+ action: 'repay',
970
+ token: tokenAddr,
971
+ amount: amountBigInt,
972
+
973
+ };
974
+
975
+ // Add cross-chain parameters if applicable
976
+ if (crossChain && collateralChainId) {
977
+ repayParams.toChainId = collateralChainId;
978
+ warnings.push(`Cross-chain repay: Repaying debt on ${collateralChainId} using tokens from ${chainId}`);
979
+ }
980
+
981
+ // Check and handle allowance (required for ALL repay operations)
982
+ // Reference: sodax-frontend moneymarket-ops.ts - repay ALWAYS checks allowance
983
+ try {
984
+ const allowanceResult = await (sodaxClient as any).moneyMarket.isAllowanceValid(
985
+ { token: tokenAddr, amount: amountBigInt, action: 'repay' },
986
+ spokeProvider
987
+ );
988
+
989
+ if (allowanceResult.ok && !allowanceResult.value) {
990
+ console.log('[mm:repay] Approval needed, approving...');
991
+ const approveResult = await (sodaxClient as any).moneyMarket.approve(
992
+ { token: tokenAddr, amount: amountBigInt, action: 'repay' },
993
+ spokeProvider
994
+ );
995
+
996
+ if (!approveResult.ok) {
997
+ throw new Error(`Approval failed: ${serializeError(approveResult.error)}`);
998
+ }
999
+
1000
+ // Wait for approval confirmation
1001
+ const approvalTxHash = approveResult.value;
1002
+ if (approvalTxHash && spokeProvider.walletProvider?.waitForTransactionReceipt) {
1003
+ await spokeProvider.walletProvider.waitForTransactionReceipt(approvalTxHash);
1004
+ console.log('[mm:repay] Approval confirmed');
1005
+ }
1006
+ }
1007
+ } catch (allowanceError) {
1008
+ console.warn('[mm:repay] Allowance check failed, proceeding anyway:', allowanceError);
1009
+ }
1010
+
1011
+ // Execute repay
1012
+ const repayResult = await (sodaxClient as any).moneyMarket.repay(
1013
+ repayParams,
1014
+ spokeProvider,
1015
+ timeoutMs
1016
+ );
1017
+
1018
+ // Handle Result type from SDK
1019
+ if (repayResult.ok === false) {
1020
+ throw new Error(`Repay failed: ${serializeError(repayResult.error)}`);
1021
+ }
1022
+
1023
+ const value = repayResult.ok ? repayResult.value : repayResult;
1024
+ const [solverResponse, intent, deliveryInfo] = Array.isArray(value) ? value : [value, undefined, undefined];
1025
+
1026
+ // Extract tx hashes from deliveryInfo (SDK returns 3-element tuple)
1027
+ const spokeTxHash = deliveryInfo?.srcTxHash || (solverResponse as any)?.txHash || solverResponse;
1028
+ const hubTxHash = deliveryInfo?.hubTxHash || (intent as any)?.hubTxHash;
1029
+ const dstTxHash = deliveryInfo?.dstTxHash;
1030
+
1031
+ return {
1032
+ success: true,
1033
+ txHash: String(spokeTxHash),
1034
+ status: "success",
1035
+ spokeTxHash: String(spokeTxHash),
1036
+ hubTxHash: hubTxHash ? String(hubTxHash) : undefined,
1037
+ intentHash: undefined,
1038
+ operation: "repay",
1039
+ chainId,
1040
+ token,
1041
+ amount: repayAll ? "max (full debt)" : amount,
1042
+ isCrossChain: crossChain,
1043
+ message: repayAll
1044
+ ? `Successfully repaid full debt for ${token} on ${chainId}`
1045
+ : `Successfully repaid ${amount} ${token} to money market on ${chainId}`,
1046
+ warnings: warnings.length > 0 ? warnings : undefined,
1047
+ };
1048
+ } catch (error) {
1049
+ const errorMessage = error instanceof Error ? error.message : "Unknown error during repay";
1050
+ return {
1051
+ success: false,
1052
+ status: "failed",
1053
+ operation: "repay",
1054
+ chainId,
1055
+ token,
1056
+ amount: repayAll ? "max (full debt)" : amount,
1057
+ isCrossChain: crossChain,
1058
+ message: `Money market repay failed: ${errorMessage}`,
1059
+ };
1060
+ }
1061
+ }
1062
+
1063
+ // ============================================================================
1064
+ // Intent Creation Handlers (Advanced)
1065
+ // ============================================================================
1066
+
1067
+ /**
1068
+ * Create a supply intent without executing (for custom flows)
1069
+ */
1070
+ async function handleCreateSupplyIntent(
1071
+ params: Static<typeof CreateSupplyIntentSchema>
1072
+ ): Promise<IntentResult> {
1073
+ const { walletId, chainId, token, amount, useAsCollateral = true, dstChainId, recipient, raw = true } = params;
1074
+
1075
+ try {
1076
+ const { walletAddress, spokeProvider, tokenAddr } = await prepareMoneyMarketOperation(
1077
+ walletId, chainId, token, amount, "supply"
1078
+ );
1079
+
1080
+ const decimals = await getTokenDecimals(chainId, token);
1081
+ const amountBigInt = parseTokenAmount(amount, decimals);
1082
+ const sodaxClient = await getSodaxClient();
1083
+
1084
+ const supplyParams: any = {
1085
+ action: 'supply',
1086
+ token: tokenAddr,
1087
+ amount: amountBigInt,
1088
+
1089
+ toAddress: recipient || walletAddress,
1090
+ };
1091
+
1092
+ if (dstChainId) {
1093
+ supplyParams.toChainId = toSodaxChainId(dstChainId);
1094
+ }
1095
+
1096
+ const intentData = await sodaxClient.moneyMarket.createSupplyIntent(
1097
+ supplyParams,
1098
+ spokeProvider,
1099
+ raw
1100
+ );
1101
+
1102
+ return {
1103
+ success: true,
1104
+ status: "pending",
1105
+ operation: "createSupplyIntent",
1106
+ chainId,
1107
+ dstChainId: dstChainId || chainId,
1108
+ token,
1109
+ amount,
1110
+ intentData,
1111
+ requiresSubmission: true,
1112
+ message: "Supply intent created. Submit this intent to execute the supply operation.",
1113
+ };
1114
+ } catch (error) {
1115
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1116
+ throw new Error(`Create supply intent failed: ${errorMessage}`);
1117
+ }
1118
+ }
1119
+
1120
+ /**
1121
+ * Create a borrow intent without executing (for custom flows)
1122
+ */
1123
+ async function handleCreateBorrowIntent(
1124
+ params: Static<typeof CreateBorrowIntentSchema>
1125
+ ): Promise<IntentResult> {
1126
+ const { walletId, chainId, token, amount, interestRateMode = 2, dstChainId, recipient, raw = true } = params;
1127
+
1128
+ try {
1129
+ const { walletAddress, spokeProvider, tokenAddr } = await prepareMoneyMarketOperation(
1130
+ walletId, chainId, token, amount, "borrow"
1131
+ );
1132
+
1133
+ const decimals = await getTokenDecimals(chainId, token);
1134
+ const amountBigInt = parseTokenAmount(amount, decimals);
1135
+ const sodaxClient = await getSodaxClient();
1136
+
1137
+ const borrowParams: any = {
1138
+ action: 'borrow',
1139
+ token: tokenAddr,
1140
+ amount: amountBigInt,
1141
+
1142
+ toAddress: recipient || walletAddress,
1143
+ };
1144
+
1145
+ if (dstChainId) {
1146
+ borrowParams.toChainId = toSodaxChainId(dstChainId);
1147
+ }
1148
+
1149
+ const intentData = await sodaxClient.moneyMarket.createBorrowIntent(
1150
+ borrowParams,
1151
+ spokeProvider,
1152
+ raw
1153
+ );
1154
+
1155
+ return {
1156
+ success: true,
1157
+ status: "pending",
1158
+ operation: "createBorrowIntent",
1159
+ chainId,
1160
+ dstChainId: dstChainId || chainId,
1161
+ token,
1162
+ amount,
1163
+ intentData,
1164
+ requiresSubmission: true,
1165
+ message: dstChainId && dstChainId !== chainId
1166
+ ? `Cross-chain borrow intent created. Collateral on ${chainId}, borrowed tokens to ${dstChainId}. Submit this intent to execute.`
1167
+ : "Borrow intent created. Submit this intent to execute the borrow operation.",
1168
+ };
1169
+ } catch (error) {
1170
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1171
+ throw new Error(`Create borrow intent failed: ${errorMessage}`);
1172
+ }
1173
+ }
1174
+
1175
+ // ============================================================================
1176
+ // Tool Registration
1177
+ // ============================================================================
1178
+
1179
+ /**
1180
+ * Registers all money market tools with the agent tools registry
1181
+ */
1182
+ export function registerMoneyMarketTools(agentTools: AgentTools): void {
1183
+ // Supply
1184
+ agentTools.register({
1185
+ name: "amped_mm_supply",
1186
+ summary: "Supply tokens as collateral to the SODAX money market. Supports same-chain and cross-chain supply (supply on chain A, collateral available on chain B).",
1187
+ schema: MoneyMarketSupplySchema,
1188
+ handler: handleSupply,
1189
+ });
1190
+
1191
+ // Withdraw
1192
+ agentTools.register({
1193
+ name: "amped_mm_withdraw",
1194
+ summary: "Withdraw supplied tokens from the SODAX money market. Supports cross-chain withdraw (withdraw from chain A, receive tokens on chain B).",
1195
+ schema: MoneyMarketWithdrawSchema,
1196
+ handler: handleWithdraw,
1197
+ });
1198
+
1199
+ // Borrow
1200
+ agentTools.register({
1201
+ name: "amped_mm_borrow",
1202
+ summary: "Borrow tokens from the SODAX money market. KEY FEATURE: Can borrow to a different chain than collateral! Example: Supply on Ethereum, borrow to Arbitrum using dstChainId parameter.",
1203
+ schema: MoneyMarketBorrowSchema,
1204
+ handler: handleBorrow,
1205
+ });
1206
+
1207
+ // Repay
1208
+ agentTools.register({
1209
+ name: "amped_mm_repay",
1210
+ summary: "Repay borrowed tokens to the SODAX money market. Use amount='-1' or repayAll=true to repay full debt. Supports cross-chain repay.",
1211
+ schema: MoneyMarketRepaySchema,
1212
+ handler: handleRepay,
1213
+ });
1214
+
1215
+ // Advanced: Create Intent variants for custom flows
1216
+ agentTools.register({
1217
+ name: "amped_mm_create_supply_intent",
1218
+ summary: "[Advanced] Create a supply intent without executing. Returns raw intent data for custom signing or multi-step flows.",
1219
+ schema: CreateSupplyIntentSchema,
1220
+ handler: handleCreateSupplyIntent,
1221
+ });
1222
+
1223
+ agentTools.register({
1224
+ name: "amped_mm_create_borrow_intent",
1225
+ summary: "[Advanced] Create a borrow intent without executing. Supports cross-chain borrow intents. Returns raw intent data for custom signing or multi-step flows.",
1226
+ schema: CreateBorrowIntentSchema,
1227
+ handler: handleCreateBorrowIntent,
1228
+ });
1229
+ }
1230
+
1231
+ // ============================================================================
1232
+ // Re-exports for testing and direct usage
1233
+ // ============================================================================
1234
+
1235
+ export {
1236
+ MoneyMarketBaseSchema,
1237
+ MoneyMarketSupplySchema,
1238
+ MoneyMarketWithdrawSchema,
1239
+ MoneyMarketBorrowSchema,
1240
+ MoneyMarketRepaySchema,
1241
+ CreateSupplyIntentSchema,
1242
+ CreateWithdrawIntentSchema,
1243
+ CreateBorrowIntentSchema,
1244
+ CreateRepayIntentSchema,
1245
+ handleSupply,
1246
+ handleWithdraw,
1247
+ handleBorrow,
1248
+ handleRepay,
1249
+ handleCreateSupplyIntent,
1250
+ handleCreateBorrowIntent,
1251
+ };
1252
+
1253
+ // Aliases for index.ts compatibility
1254
+ export {
1255
+ MoneyMarketSupplySchema as MmSupplySchema,
1256
+ MoneyMarketWithdrawSchema as MmWithdrawSchema,
1257
+ MoneyMarketBorrowSchema as MmBorrowSchema,
1258
+ MoneyMarketRepaySchema as MmRepaySchema,
1259
+ handleSupply as handleMmSupply,
1260
+ handleWithdraw as handleMmWithdraw,
1261
+ handleBorrow as handleMmBorrow,
1262
+ handleRepay as handleMmRepay,
1263
+ };
1264
+
1265
+ export type { MoneyMarketOperationResult, IntentResult };