@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.
- package/CHANGELOG.md +75 -0
- package/README.md +3 -2
- package/TOOL_EXAMPLES.md +13 -0
- package/dist/auth/resolveClient.js +19 -2
- package/dist/server.js +2 -2
- package/dist/services/poolService.js +474 -49
- package/dist/services/tradeService.js +18 -13
- package/dist/tools/accountInfo.js +4 -52
- package/dist/tools/accountTransfer.js +5 -10
- package/dist/tools/checkAccountReady.js +16 -10
- package/dist/tools/closePosition.js +17 -10
- package/dist/tools/createPerpMarket.js +132 -5
- package/dist/tools/getAccountSnapshot.js +10 -4
- package/dist/tools/getBaseDetail.js +43 -3
- package/dist/tools/getOrders.js +30 -2
- package/dist/tools/getPoolMetadata.js +16 -4
- package/dist/tools/getUserTradingFeeRate.js +65 -2
- package/dist/tools/manageTpSl.js +239 -31
- package/dist/tools/openPositionSimple.js +5 -3
- package/dist/utils/injectProvider.js +1 -0
- package/dist/utils/mappings.js +12 -7
- package/package.json +4 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
436
|
+
const client = clientOverride ?? (await resolveClient()).client;
|
|
36
437
|
try {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
197
|
-
if (marginInfo?.code === 0) {
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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()}) +
|
|
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
|
|
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,
|