@michaleffffff/mcp-trading-server 3.0.5 → 3.0.10

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.
@@ -2,6 +2,75 @@ import { pool, quote, base } from "@myx-trade/sdk";
2
2
  import { getChainId, resolveClient } from "../auth/resolveClient.js";
3
3
  import { extractErrorMessage } from "../utils/errorMessage.js";
4
4
  import { ensureUnits } from "../utils/units.js";
5
+ import { normalizeAddress } from "../utils/address.js";
6
+ import { Contract, parseUnits, MaxUint256 } from "ethers";
7
+ import { logger } from "../utils/logger.js";
8
+ const LP_DECIMALS = 18;
9
+ const POOL_MANAGER_BY_CHAIN = {
10
+ 421614: "0xf268D9FeD3Bd56fd9aBdb4FeEb993338613678A8",
11
+ };
12
+ const POOL_MANAGER_ABI = [
13
+ "function deployPool((bytes32 marketId,address baseToken))",
14
+ "function getMarketPool(bytes32 marketId,address asset) view returns ((bytes32 marketId,bytes32 poolId,address baseToken,address quoteToken,uint8 riskTier,uint8 state,address basePoolToken,address quotePoolToken,uint16 maxPriceDeviation,bool compoundEnabled,uint64 windowCapUsd,address poolVault,address tradingVault))",
15
+ ];
16
+ const LP_ROUTER_BY_CHAIN = {
17
+ 421614: {
18
+ router: "0x5E16C0699de2Ee5D69a620AB38AE2c81A29a58df",
19
+ basePool: "0xDb41D850e9b30fc0417F488D1aB64fdc1d0DdFdc",
20
+ quotePool: "0xF0784c1023C051Bf1D5b8470A896c536039B06B9",
21
+ },
22
+ 59141: {
23
+ router: "0xf1395643D77DD839E4b8d3Bb122fd0275Cf07631",
24
+ basePool: "0x31b093BBADbb2A3Bcd7E433E8Ed5bcdD76fFAFFe",
25
+ quotePool: "0xFF8A6797E2063A7a4cd1468eB7cC0db9798C753f",
26
+ },
27
+ 97: {
28
+ router: "0xe9F2E58562aD1D50AfB1eD92EAa6D367A6D0e552",
29
+ basePool: "0x39506f97D1c1A91EAfD59368697E20dC1fE5eEDB",
30
+ quotePool: "0x80DAa474e7e2C9c86708e434B80636b92fb4e9EE",
31
+ },
32
+ 56: {
33
+ router: "0x06d76c78B56D361de01A8903deA8aCEFFe6251d6",
34
+ basePool: "0x59E365A627C5A1CE459e8e3489C97Ab2BEd56FEa",
35
+ quotePool: "0xB1E6df749A602892FafB27bb39Fd4F044527121E",
36
+ },
37
+ };
38
+ const LP_ROUTER_BY_CHAIN_BETA = {
39
+ 421614: {
40
+ router: "0xfb790ECE13Cd9e296b4a06ABF38D10431360c236",
41
+ basePool: "0x1F767BEa83EDe5E0C18904f439C579f52c2c0F1b",
42
+ quotePool: "0x50ad7da312c58c6689bEF028954a366cb5b8ee13",
43
+ },
44
+ 59141: {
45
+ router: "0x0AE31989318565620c03d35889a0B2536b8fba5C",
46
+ basePool: "0xc9e3826c42183207B418116A16827C1605132c51",
47
+ quotePool: "0x9A9934D3b22103dE822d94A22A3177d728FE0e5a",
48
+ },
49
+ 97: {
50
+ router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
51
+ basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
52
+ quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
53
+ },
54
+ };
55
+ const PREVIEW_POOL_ABI = [
56
+ "function previewLpAmountOut(bytes32,uint256,uint256) view returns (uint256)",
57
+ "function previewQuoteAmountOut(bytes32,uint256,uint256) view returns (uint256)",
58
+ "function previewBaseAmountOut(bytes32,uint256,uint256) view returns (uint256)",
59
+ ];
60
+ const ROUTER_ABI = [
61
+ "function depositQuote((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))",
62
+ "function depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[])) payable",
63
+ "function withdrawQuote((bytes32,uint256,uint256,address))",
64
+ "function withdrawQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address)) payable",
65
+ "function depositBase((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))",
66
+ "function depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[])) payable",
67
+ "function withdrawBase((bytes32,uint256,uint256,address))",
68
+ "function withdrawBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address)) payable",
69
+ ];
70
+ const ERC20_ABI = [
71
+ "function allowance(address,address) view returns (uint256)",
72
+ "function approve(address,uint256) returns (bool)",
73
+ ];
5
74
  function isDivideByZeroError(message) {
6
75
  const lower = message.toLowerCase();
7
76
  return (lower.includes("divide_by_zero") ||
@@ -19,74 +88,378 @@ function toPositiveBigint(input) {
19
88
  return null;
20
89
  }
21
90
  }
91
+ function isAbiLengthMismatchError(message) {
92
+ const lower = message.toLowerCase();
93
+ return lower.includes("abi encoding params/values length mismatch");
94
+ }
95
+ function isSdkWaitNotFunctionError(message) {
96
+ return message.toLowerCase().includes("wait is not a function");
97
+ }
98
+ function readLastMockTxHash() {
99
+ const hash = String(globalThis?.__MCP_LAST_TX_HASH ?? "").trim();
100
+ if (!hash.startsWith("0x") || hash.length !== 66)
101
+ return null;
102
+ return hash;
103
+ }
104
+ function recoverSdkSubmittedTxHash(beforeHash, message, context) {
105
+ if (!isSdkWaitNotFunctionError(message))
106
+ return null;
107
+ const recovered = readLastMockTxHash();
108
+ if (!recovered || recovered === beforeHash)
109
+ return null;
110
+ logger.warn(`Recovered SDK-submitted tx hash for ${context.poolType} ${context.action} after wait() incompatibility: ${recovered}`);
111
+ return {
112
+ transactionHash: recovered,
113
+ fallback: {
114
+ mode: "sdk_submitted_txhash_recovery",
115
+ reason: "sdk_wait_not_function",
116
+ poolType: context.poolType,
117
+ action: context.action,
118
+ },
119
+ };
120
+ }
121
+ async function withMutedSdkAbiMismatchLogs(runner) {
122
+ const original = console.error;
123
+ console.error = (...args) => {
124
+ const first = args?.[0];
125
+ const firstText = typeof first === "string"
126
+ ? first
127
+ : first instanceof Error
128
+ ? first.message
129
+ : (first && typeof first === "object"
130
+ ? JSON.stringify(first)
131
+ : String(first ?? ""));
132
+ const lower = firstText.toLowerCase();
133
+ if (isAbiLengthMismatchError(firstText) || lower.includes("abiencodinglengthmismatcherror")) {
134
+ return;
135
+ }
136
+ original(...args);
137
+ };
138
+ try {
139
+ return await runner();
140
+ }
141
+ finally {
142
+ console.error = original;
143
+ }
144
+ }
145
+ function isBetaModeEnabled() {
146
+ return String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
147
+ }
148
+ function getPoolManagerAddress(chainId) {
149
+ const envAddress = String(process.env.POOL_MANAGER_ADDRESS ?? "").trim();
150
+ if (envAddress) {
151
+ return normalizeAddress(envAddress, "POOL_MANAGER_ADDRESS");
152
+ }
153
+ const mapped = POOL_MANAGER_BY_CHAIN[chainId];
154
+ if (!mapped) {
155
+ throw new Error(`Pool manager address is not configured for chainId=${chainId}. Set POOL_MANAGER_ADDRESS env var.`);
156
+ }
157
+ return mapped;
158
+ }
159
+ function getLpAddresses(chainId) {
160
+ const source = isBetaModeEnabled() ? LP_ROUTER_BY_CHAIN_BETA : LP_ROUTER_BY_CHAIN;
161
+ const matched = source[chainId];
162
+ if (!matched) {
163
+ throw new Error(`Liquidity router config not found for chainId=${chainId}.`);
164
+ }
165
+ return matched;
166
+ }
167
+ function normalizeSlippageRatio(slippage) {
168
+ if (!Number.isFinite(slippage) || slippage < 0)
169
+ return 0;
170
+ if (slippage > 1 && slippage <= 100)
171
+ return slippage / 100;
172
+ if (slippage > 1)
173
+ return 1;
174
+ return slippage;
175
+ }
176
+ function applyMinOutBySlippage(amountOut, slippage) {
177
+ if (amountOut <= 0n)
178
+ return 0n;
179
+ const ratio = normalizeSlippageRatio(slippage);
180
+ const scale = 1000000n;
181
+ const ratioScaled = BigInt(Math.round(ratio * 1_000_000));
182
+ const effective = ratioScaled >= scale ? 0n : (scale - ratioScaled);
183
+ return (amountOut * effective) / scale;
184
+ }
185
+ function shouldUseOraclePriceByState(state) {
186
+ const numeric = Number(state);
187
+ if (!Number.isFinite(numeric))
188
+ return true;
189
+ return numeric !== 0 && numeric !== 1;
190
+ }
191
+ async function getMarketDetailOrThrow(client, chainId, poolId) {
192
+ const raw = await client.markets.getMarketDetail({ chainId, poolId });
193
+ const detail = raw?.data ?? raw;
194
+ if (!detail || !detail.poolId) {
195
+ throw new Error(`Failed to fetch market detail for poolId=${poolId}.`);
196
+ }
197
+ return detail;
198
+ }
199
+ async function buildOraclePricePayload(client, chainId, poolId, fallbackOracleType) {
200
+ const oracle = await client.utils.getOraclePrice(poolId, chainId);
201
+ const vaa = String(oracle?.vaa ?? "").trim();
202
+ if (!vaa || !vaa.startsWith("0x")) {
203
+ throw new Error(`Oracle VAA unavailable for pool ${poolId}.`);
204
+ }
205
+ const publishTime = toPositiveBigint(oracle?.publishTime) ?? BigInt(Math.floor(Date.now() / 1000));
206
+ const oracleType = Number.isFinite(Number(oracle?.oracleType)) ? Number(oracle.oracleType) : fallbackOracleType;
207
+ const value = toPositiveBigint(oracle?.value) ?? 0n;
208
+ const referencePrice30 = BigInt(ensureUnits(String(oracle?.price ?? "0"), 30, "oracle price"));
209
+ return {
210
+ prices: [[poolId, oracleType, publishTime, vaa]],
211
+ value,
212
+ referencePrice30,
213
+ };
214
+ }
215
+ async function previewAmountOutForLiquidity(signer, chainId, poolId, poolType, action, amountIn, referencePrice30) {
216
+ try {
217
+ const addresses = getLpAddresses(chainId);
218
+ const poolAddress = poolType === "QUOTE" ? addresses.quotePool : addresses.basePool;
219
+ const previewContract = new Contract(poolAddress, PREVIEW_POOL_ABI, signer);
220
+ if (action === "deposit") {
221
+ const out = await previewContract.previewLpAmountOut(poolId, amountIn, referencePrice30);
222
+ return toPositiveBigint(out) ?? 0n;
223
+ }
224
+ if (poolType === "QUOTE") {
225
+ const out = await previewContract.previewQuoteAmountOut(poolId, amountIn, referencePrice30);
226
+ return toPositiveBigint(out) ?? 0n;
227
+ }
228
+ const out = await previewContract.previewBaseAmountOut(poolId, amountIn, referencePrice30);
229
+ return toPositiveBigint(out) ?? 0n;
230
+ }
231
+ catch (error) {
232
+ logger.warn("LP preview call failed; fallback to minAmountOut=0.", extractErrorMessage(error));
233
+ return 0n;
234
+ }
235
+ }
236
+ async function executeLiquidityTxViaRouter(params) {
237
+ const { chainId, poolId, poolType, action, amount, slippage } = params;
238
+ const { client, signer, address } = await resolveClient();
239
+ const marketDetail = await getMarketDetailOrThrow(client, chainId, poolId);
240
+ const addresses = getLpAddresses(chainId);
241
+ const decimals = action === "deposit"
242
+ ? Number(poolType === "QUOTE" ? marketDetail.quoteDecimals : marketDetail.baseDecimals)
243
+ : LP_DECIMALS;
244
+ if (!Number.isFinite(decimals) || decimals < 0) {
245
+ throw new Error(`Invalid decimals while preparing ${poolType} ${action} transaction.`);
246
+ }
247
+ const amountIn = parseUnits(String(amount), decimals);
248
+ if (amountIn <= 0n) {
249
+ throw new Error(`Liquidity ${poolType.toLowerCase()} ${action} amount must be > 0.`);
250
+ }
251
+ let approvalTxHash = null;
252
+ if (action === "deposit") {
253
+ const tokenAddress = normalizeAddress(poolType === "QUOTE" ? marketDetail.quoteToken : marketDetail.baseToken, poolType === "QUOTE" ? "quoteToken" : "baseToken");
254
+ const tokenContract = new Contract(tokenAddress, ERC20_ABI, signer);
255
+ const allowance = toPositiveBigint(await tokenContract.allowance(address, addresses.router)) ?? 0n;
256
+ if (allowance < amountIn) {
257
+ logger.info(`[LP fallback] allowance insufficient for ${poolType} deposit, approving router. required=${amountIn.toString()}, current=${allowance.toString()}`);
258
+ const approveTx = await tokenContract.approve(addresses.router, MaxUint256);
259
+ approvalTxHash = String(approveTx?.hash ?? "").trim() || null;
260
+ const approveReceipt = await approveTx?.wait?.();
261
+ if (approveReceipt && approveReceipt.status !== 1) {
262
+ throw new Error(`Router approval transaction reverted for ${poolType} deposit.`);
263
+ }
264
+ }
265
+ }
266
+ const needOraclePrice = shouldUseOraclePriceByState(marketDetail.state);
267
+ let oraclePayload = {
268
+ prices: [],
269
+ value: 0n,
270
+ referencePrice30: 0n,
271
+ };
272
+ if (needOraclePrice) {
273
+ oraclePayload = await buildOraclePricePayload(client, chainId, poolId, Number(marketDetail.oracleType ?? 1));
274
+ }
275
+ const amountOut = await previewAmountOutForLiquidity(signer, chainId, poolId, poolType, action, amountIn, oraclePayload.referencePrice30);
276
+ const minAmountOut = applyMinOutBySlippage(amountOut, slippage);
277
+ const routerContract = new Contract(addresses.router, ROUTER_ABI, signer);
278
+ const txOverrides = {};
279
+ if (oraclePayload.value > 0n) {
280
+ txOverrides.value = oraclePayload.value;
281
+ }
282
+ let tx;
283
+ if (poolType === "QUOTE" && action === "deposit") {
284
+ const paramsTuple = [
285
+ poolId,
286
+ amountIn,
287
+ minAmountOut,
288
+ address,
289
+ [],
290
+ ];
291
+ if (oraclePayload.prices.length > 0) {
292
+ tx = await routerContract["depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
293
+ }
294
+ else {
295
+ tx = await routerContract["depositQuote((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
296
+ }
297
+ }
298
+ else if (poolType === "QUOTE" && action === "withdraw") {
299
+ const paramsTuple = [poolId, amountIn, minAmountOut, address];
300
+ if (oraclePayload.prices.length > 0) {
301
+ tx = await routerContract["withdrawQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePayload.prices, paramsTuple, txOverrides);
302
+ }
303
+ else {
304
+ tx = await routerContract["withdrawQuote((bytes32,uint256,uint256,address))"](paramsTuple, txOverrides);
305
+ }
306
+ }
307
+ else if (poolType === "BASE" && action === "deposit") {
308
+ const paramsTuple = [
309
+ poolId,
310
+ amountIn,
311
+ minAmountOut,
312
+ address,
313
+ [],
314
+ ];
315
+ if (oraclePayload.prices.length > 0) {
316
+ tx = await routerContract["depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
317
+ }
318
+ else {
319
+ tx = await routerContract["depositBase((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
320
+ }
321
+ }
322
+ else {
323
+ const paramsTuple = [poolId, amountIn, minAmountOut, address];
324
+ if (oraclePayload.prices.length > 0) {
325
+ tx = await routerContract["withdrawBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePayload.prices, paramsTuple, txOverrides);
326
+ }
327
+ else {
328
+ tx = await routerContract["withdrawBase((bytes32,uint256,uint256,address))"](paramsTuple, txOverrides);
329
+ }
330
+ }
331
+ const txHash = String(tx?.hash ?? "").trim();
332
+ if (!txHash || !txHash.startsWith("0x")) {
333
+ throw new Error(`${poolType} ${action} fallback sent transaction but no hash was returned.`);
334
+ }
335
+ return {
336
+ transactionHash: txHash,
337
+ fallback: {
338
+ mode: "router_explicit_signature",
339
+ action,
340
+ poolType,
341
+ amountIn: amountIn.toString(),
342
+ minAmountOut: minAmountOut.toString(),
343
+ usedOraclePrice: oraclePayload.prices.length > 0,
344
+ oracleValue: oraclePayload.value.toString(),
345
+ approvalTxHash,
346
+ },
347
+ };
348
+ }
349
+ async function resolvePositiveMarketPrice30(client, poolId, chainId) {
350
+ if (!client)
351
+ return null;
352
+ try {
353
+ const oracle = await client.utils?.getOraclePrice?.(poolId, chainId);
354
+ const byValue = toPositiveBigint(oracle?.value);
355
+ const byPrice = toPositiveBigint(oracle?.price);
356
+ if (byValue)
357
+ return byValue;
358
+ if (byPrice)
359
+ return byPrice;
360
+ }
361
+ catch {
362
+ }
363
+ try {
364
+ const tickerRes = await client.markets?.getTickerList?.({ chainId, poolIds: [poolId] });
365
+ const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
366
+ if (row?.price) {
367
+ const tickerRaw = ensureUnits(row.price, 30, "ticker price");
368
+ const byTicker = toPositiveBigint(tickerRaw);
369
+ if (byTicker)
370
+ return byTicker;
371
+ }
372
+ }
373
+ catch {
374
+ }
375
+ return null;
376
+ }
22
377
  /**
23
378
  * 创建合约市场池子
24
379
  */
25
380
  export async function createPool(baseToken, marketId) {
26
- await resolveClient();
381
+ const { signer } = await resolveClient();
27
382
  const chainId = getChainId();
28
- return pool.createPool({ chainId, baseToken, marketId });
383
+ const normalizedBaseToken = normalizeAddress(baseToken, "baseToken");
384
+ const normalizedMarketId = String(marketId ?? "").trim();
385
+ try {
386
+ return await pool.createPool({ chainId, baseToken: normalizedBaseToken, marketId: normalizedMarketId });
387
+ }
388
+ catch (error) {
389
+ const message = extractErrorMessage(error).toLowerCase();
390
+ if (!message.includes("deploypool is not a function")) {
391
+ throw error;
392
+ }
393
+ logger.warn("SDK pool.createPool write path unavailable; falling back to direct PoolManager.deployPool.");
394
+ const poolManagerAddress = getPoolManagerAddress(chainId);
395
+ const contract = new Contract(poolManagerAddress, POOL_MANAGER_ABI, signer);
396
+ const tx = await contract.deployPool([normalizedMarketId, normalizedBaseToken]);
397
+ const txHash = String(tx?.hash ?? "").trim();
398
+ if (!txHash.startsWith("0x")) {
399
+ throw new Error("Direct deployPool fallback submitted transaction but no hash was returned.");
400
+ }
401
+ return {
402
+ transactionHash: txHash,
403
+ fallback: {
404
+ mode: "pool_manager_direct_deploy",
405
+ chainId,
406
+ poolManagerAddress,
407
+ },
408
+ };
409
+ }
410
+ }
411
+ export async function getMarketPoolByBaseToken(marketId, baseToken, chainIdOverride) {
412
+ const { signer } = await resolveClient();
413
+ const chainId = chainIdOverride ?? getChainId();
414
+ const normalizedBaseToken = normalizeAddress(baseToken, "baseToken");
415
+ const normalizedMarketId = String(marketId ?? "").trim();
416
+ const poolManagerAddress = getPoolManagerAddress(chainId);
417
+ const contract = new Contract(poolManagerAddress, POOL_MANAGER_ABI, signer);
418
+ const record = await contract.getMarketPool(normalizedMarketId, normalizedBaseToken);
419
+ return {
420
+ poolManagerAddress,
421
+ chainId,
422
+ marketId: String(record?.marketId ?? normalizedMarketId),
423
+ poolId: String(record?.poolId ?? ""),
424
+ baseToken: String(record?.baseToken ?? normalizedBaseToken),
425
+ quoteToken: String(record?.quoteToken ?? ""),
426
+ state: Number(record?.state ?? -1),
427
+ basePoolToken: String(record?.basePoolToken ?? ""),
428
+ quotePoolToken: String(record?.quotePoolToken ?? ""),
429
+ };
29
430
  }
30
431
  /**
31
432
  * 获取池子信息
32
433
  */
33
434
  export async function getPoolInfo(poolId, chainIdOverride, clientOverride) {
34
435
  const chainId = chainIdOverride ?? getChainId();
35
- let needOracleFallback = false;
436
+ const client = clientOverride ?? (await resolveClient()).client;
36
437
  try {
37
- const direct = await pool.getPoolInfo(chainId, poolId);
38
- if (direct)
39
- return direct;
40
- needOracleFallback = true;
438
+ const marketPrice30 = await resolvePositiveMarketPrice30(client, poolId, chainId);
439
+ if (marketPrice30 && marketPrice30 > 0n) {
440
+ const withPrice = await pool.getPoolInfo(chainId, poolId, marketPrice30);
441
+ if (withPrice)
442
+ return withPrice;
443
+ }
41
444
  }
42
445
  catch (error) {
43
446
  const message = extractErrorMessage(error);
44
447
  if (!isDivideByZeroError(message)) {
45
448
  throw new Error(`get_pool_info failed: ${message}`);
46
449
  }
47
- needOracleFallback = true;
48
450
  }
49
- if (!needOracleFallback)
50
- return undefined;
51
- const client = clientOverride ?? (await resolveClient()).client;
52
- if (!client?.utils?.getOraclePrice) {
53
- throw new Error("get_pool_info failed and oracle fallback is unavailable (client.utils.getOraclePrice missing).");
54
- }
55
- let oracleRaw = 0n;
56
451
  try {
57
- const oracle = await client.utils.getOraclePrice(poolId, chainId);
58
- const byValue = toPositiveBigint(oracle?.value);
59
- const byPrice = toPositiveBigint(oracle?.price);
60
- oracleRaw = byValue ?? byPrice ?? 0n;
452
+ const direct = await pool.getPoolInfo(chainId, poolId);
453
+ if (direct)
454
+ return direct;
455
+ throw new Error(`Pool info for ${poolId} returned undefined.`);
61
456
  }
62
457
  catch (error) {
63
- throw new Error(`get_pool_info fallback failed to fetch oracle price: ${extractErrorMessage(error)}`);
64
- }
65
- if (oracleRaw <= 0n) {
66
- try {
67
- const tickerRes = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
68
- const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
69
- if (row?.price) {
70
- const tickerRaw = ensureUnits(row.price, 30, "ticker price");
71
- const byTicker = toPositiveBigint(tickerRaw);
72
- oracleRaw = byTicker ?? 0n;
73
- }
74
- }
75
- catch {
76
- }
77
- }
78
- if (oracleRaw <= 0n) {
79
- throw new Error("get_pool_info fallback requires a positive oracle/ticker price, but both resolved to 0.");
80
- }
81
- try {
82
- const retried = await pool.getPoolInfo(chainId, poolId, oracleRaw);
83
- if (!retried) {
84
- throw new Error(`Pool info for ${poolId} returned undefined after oracle-price retry.`);
458
+ const message = extractErrorMessage(error);
459
+ if (isDivideByZeroError(message)) {
460
+ throw new Error("get_pool_info unavailable: pool reserves are currently empty or market price context is unresolved.");
85
461
  }
86
- return retried;
87
- }
88
- catch (error) {
89
- throw new Error(`get_pool_info failed after oracle-price retry: ${extractErrorMessage(error)}`);
462
+ throw new Error(`get_pool_info failed: ${message}`);
90
463
  }
91
464
  }
92
465
  /**
@@ -108,28 +481,80 @@ export async function getLiquidityInfo(client, poolId, marketPrice, chainIdOverr
108
481
  */
109
482
  export async function quoteDeposit(poolId, amount, slippage, chainIdOverride) {
110
483
  const chainId = chainIdOverride ?? getChainId();
111
- return quote.deposit({ chainId, poolId, amount, slippage });
484
+ const txHashBefore = readLastMockTxHash();
485
+ try {
486
+ return await withMutedSdkAbiMismatchLogs(() => quote.deposit({ chainId, poolId, amount, slippage }));
487
+ }
488
+ catch (error) {
489
+ const message = extractErrorMessage(error);
490
+ const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "QUOTE", action: "deposit" });
491
+ if (recovered)
492
+ return recovered;
493
+ if (!isAbiLengthMismatchError(message))
494
+ throw error;
495
+ logger.warn("quote.deposit hit SDK ABI mismatch; switching to explicit router path.");
496
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "deposit", amount, slippage });
497
+ }
112
498
  }
113
499
  /**
114
500
  * Quote 池 withdraw
115
501
  */
116
502
  export async function quoteWithdraw(poolId, amount, slippage, chainIdOverride) {
117
503
  const chainId = chainIdOverride ?? getChainId();
118
- return quote.withdraw({ chainId, poolId, amount, slippage });
504
+ const txHashBefore = readLastMockTxHash();
505
+ try {
506
+ return await withMutedSdkAbiMismatchLogs(() => quote.withdraw({ chainId, poolId, amount, slippage }));
507
+ }
508
+ catch (error) {
509
+ const message = extractErrorMessage(error);
510
+ const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "QUOTE", action: "withdraw" });
511
+ if (recovered)
512
+ return recovered;
513
+ if (!isAbiLengthMismatchError(message))
514
+ throw error;
515
+ logger.warn("quote.withdraw hit SDK ABI mismatch; switching to explicit router path.");
516
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "withdraw", amount, slippage });
517
+ }
119
518
  }
120
519
  /**
121
520
  * Base 池 deposit
122
521
  */
123
522
  export async function baseDeposit(poolId, amount, slippage, chainIdOverride) {
124
523
  const chainId = chainIdOverride ?? getChainId();
125
- return base.deposit({ chainId, poolId, amount, slippage });
524
+ const txHashBefore = readLastMockTxHash();
525
+ try {
526
+ return await withMutedSdkAbiMismatchLogs(() => base.deposit({ chainId, poolId, amount, slippage }));
527
+ }
528
+ catch (error) {
529
+ const message = extractErrorMessage(error);
530
+ const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "BASE", action: "deposit" });
531
+ if (recovered)
532
+ return recovered;
533
+ if (!isAbiLengthMismatchError(message))
534
+ throw error;
535
+ logger.warn("base.deposit hit SDK ABI mismatch; switching to explicit router path.");
536
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "deposit", amount, slippage });
537
+ }
126
538
  }
127
539
  /**
128
540
  * Base 池 withdraw
129
541
  */
130
542
  export async function baseWithdraw(poolId, amount, slippage, chainIdOverride) {
131
543
  const chainId = chainIdOverride ?? getChainId();
132
- return base.withdraw({ chainId, poolId, amount, slippage });
544
+ const txHashBefore = readLastMockTxHash();
545
+ try {
546
+ return await withMutedSdkAbiMismatchLogs(() => base.withdraw({ chainId, poolId, amount, slippage }));
547
+ }
548
+ catch (error) {
549
+ const message = extractErrorMessage(error);
550
+ const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "BASE", action: "withdraw" });
551
+ if (recovered)
552
+ return recovered;
553
+ if (!isAbiLengthMismatchError(message))
554
+ throw error;
555
+ logger.warn("base.withdraw hit SDK ABI mismatch; switching to explicit router path.");
556
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "withdraw", amount, slippage });
557
+ }
133
558
  }
134
559
  /**
135
560
  * 获取 LP 价格
@@ -93,6 +93,17 @@ function parseDecimals(value, fallback) {
93
93
  function normalizeIdentifier(value) {
94
94
  return String(value ?? "").trim().toLowerCase();
95
95
  }
96
+ function toBigIntOrZero(value) {
97
+ try {
98
+ const text = String(value ?? "").trim();
99
+ if (!text)
100
+ return 0n;
101
+ return BigInt(text);
102
+ }
103
+ catch {
104
+ return 0n;
105
+ }
106
+ }
96
107
  async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
97
108
  let baseDecimals = 18;
98
109
  let quoteDecimals = getQuoteDecimals();
@@ -193,16 +204,10 @@ export async function openPosition(client, address, args) {
193
204
  console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
194
205
  const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
195
206
  let marginBalanceRaw = BigInt(0);
196
- let walletBalanceRaw = BigInt(0); // Using availableMargin as wallet balance per user
197
- if (marginInfo?.code === 0) {
198
- if (Array.isArray(marginInfo.data)) {
199
- marginBalanceRaw = BigInt(marginInfo.data[0] || "0");
200
- walletBalanceRaw = BigInt(marginInfo.data[1] || "0");
201
- }
202
- else {
203
- marginBalanceRaw = BigInt(marginInfo.data?.marginBalance || "0");
204
- walletBalanceRaw = BigInt(marginInfo.data?.availableMargin || "0");
205
- }
207
+ let walletBalanceRaw = BigInt(0);
208
+ if (marginInfo?.code === 0 && marginInfo?.data) {
209
+ marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
210
+ walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
206
211
  }
207
212
  const requiredRaw = BigInt(collateralRaw);
208
213
  if (marginBalanceRaw < requiredRaw) {
@@ -213,15 +218,15 @@ export async function openPosition(client, address, args) {
213
218
  const neededRaw = requiredRaw - marginBalanceRaw;
214
219
  console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
215
220
  if (walletBalanceRaw < neededRaw) {
216
- // Also check real wallet balance just in case user's availableMargin is truly empty
221
+ // Also check real wallet balance just in case account info wallet field is stale.
217
222
  const realWalletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
218
223
  const realWalletRaw = BigInt(realWalletRes?.data || "0");
219
224
  if (realWalletRaw < neededRaw) {
220
- throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + availableMargin (as wallet: ${walletBalanceRaw.toString()}) + real wallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
225
+ throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + walletBalance (${walletBalanceRaw.toString()}) + realWallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
221
226
  }
222
227
  walletBalanceRaw = realWalletRaw;
223
228
  }
224
- console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet (availableMargin)...`);
229
+ console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet...`);
225
230
  const depositRaw = await client.account.deposit({
226
231
  amount: neededRaw.toString(),
227
232
  tokenAddress: poolData.quoteToken,