@michaleffffff/mcp-trading-server 3.0.7 β†’ 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 CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.10 - 2026-03-18
4
+
5
+ ### Fixed
6
+ - Hardened `manage_tp_sl` delete behavior (`tpPrice=0` + `slPrice=0`):
7
+ - Added a unified cancellation path helper to always map delete intent to `cancelAllOrders` (by `orderId` or by `positionId`).
8
+ - Added fallback recovery for SDK/contract `InvalidParameter` reverts (including selector `0x613970e0`) so zero-price delete intent will still downgrade to explicit cancellation instead of failing.
9
+
10
+ ## 3.0.9 - 2026-03-18
11
+
12
+ ### Fixed
13
+ - Fixed `create_perp_market` observability and compatibility:
14
+ - Added strict `marketId` (66-char hash) and `baseToken` address validation.
15
+ - Added structured error payloads with decoded contract selector hints (e.g. `PoolExists(PoolId)`).
16
+ - Added fallback for SDK v1.0.2 write-path incompatibility (`deployPool is not a function`) by directly calling `PoolManager.deployPool`.
17
+ - Fixed `manage_tp_sl` delete semantics:
18
+ - `tpPrice=0` + `slPrice=0` now maps to explicit TP/SL order cancellation (by `orderId` or by `positionId`) instead of sending invalid on-chain TP/SL orders.
19
+ - Fixed `get_base_detail` null-read behavior:
20
+ - `success + null` is now returned as a structured error (`NOT_FOUND`) with chain/base context.
21
+ - Fixed `get_orders` OPEN status mapping:
22
+ - Removed `Unknown(undefined)` and normalized missing OPEN status to `Open`.
23
+ - Fixed LP SDK wait incompatibility:
24
+ - Added submitted tx-hash recovery path when SDK returns `...wait is not a function` after broadcast (e.g., BASE withdraw).
25
+ - Hardened direct LP router fallback:
26
+ - Added token allowance auto-approval before fallback deposit.
27
+
28
+ ### Verified (Real Funds, Arbitrum Sepolia 421614)
29
+ - Deployed and minted new base token to active wallet:
30
+ - token: `0xDae49922Ff1699CA2A6cc4eE835B2c5a9f3Fe870`
31
+ - deploy tx: `0x99e69d66cac1b3a033281bae45dd421bba37794e1a80c1a12eddded99c48acce`
32
+ - Created new perp pool for the token (marketId: `0x2a3fee38e8beba148141bea5cab0bcbbb0cf24fd5509117346991cc438cb2fe6`):
33
+ - create tx: `0x97266886c673cee13531837bcf9a0524034bd85a018036f3557c4d126fef3771`
34
+ - derived poolId: `0x6c1a8af5123a0cf636293aff3fce2ea6addd4bce172c39c6467d4dc95ac3f83e`
35
+ - BASE LP add/remove validated on the new pool:
36
+ - add approval tx: `0xa2739346558f43f474953c7c93be05f0c66f2659f80d8b07804823f03926bdb0`
37
+ - add tx: `0x6a10d7dc5d2794643b10bc2b53080bfa8df1c99eedf35cd48a46edaa6d97d832`
38
+ - remove tx: `0x4148585452d259f6a9928404c7b8d324013e7ee13859392302ab78aa34fab2bc`
39
+
40
+ ## 3.0.8 - 2026-03-18
41
+
42
+ ### Fixed
43
+ - Fixed `manage_liquidity` QUOTE pool ABI mismatch (`Expected length params=1, values=2`) by adding a safe fallback path:
44
+ - Keep SDK LP call as primary path.
45
+ - On SDK ABI-overload mismatch, switch to explicit-signature router transaction path (`depositQuote`/`withdrawQuote`) to avoid overloaded function ambiguity.
46
+ - Added the same overload-mismatch fallback for BASE pool LP operations (`depositBase`/`withdrawBase`) to prevent the same class of failure.
47
+ - Added SDK ABI-mismatch log suppression for LP calls to avoid noisy stack traces when fallback is activated.
48
+
49
+ ### Verified
50
+ - Executed a real QUOTE remove liquidity transaction via MCP after the fix:
51
+ - txHash: `0x69e089b805cccd3b14d0c511309a0ce2aecf988344ec23ae27df929ad99af390`
52
+ - status: success (confirmed on-chain)
53
+
3
54
  ## 3.0.7 - 2026-03-18
4
55
 
5
56
  ### Changed
package/README.md CHANGED
@@ -6,7 +6,7 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
6
6
 
7
7
  # Release Notes
8
8
 
9
- - **Current release: 3.0.7**
9
+ - **Current release: 3.0.10**
10
10
  - **SDK baseline**: `@myx-trade/sdk@^1.0.2` compatibility completed.
11
11
  - **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
12
12
  - **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
@@ -54,7 +54,7 @@ QUOTE_TOKEN_DECIMALS=18
54
54
  * **`close_position`**: Strategy-based closing of specific positions.
55
55
  * **`close_all_positions`**: Emergency exit for all positions in a specific pool.
56
56
  * **`cancel_orders`**: Unified cancellation (Single ID, Pool-wide, or Account-wide).
57
- * **`manage_tp_sl`**: Adjust protection orders for active positions or pending orders.
57
+ * **`manage_tp_sl`**: Adjust protection orders for active positions or pending orders. Deletion is supported via `tpPrice=0` + `slPrice=0`.
58
58
  * **`adjust_margin`**: Add or remove collateral to manage liquidation risk.
59
59
 
60
60
  ### πŸ“ Account & Portfolio
package/TOOL_EXAMPLES.md CHANGED
@@ -60,6 +60,19 @@ Update existing protection orders or set new ones for a position.
60
60
  }
61
61
  ```
62
62
 
63
+ Delete both TP/SL for a position:
64
+ ```json
65
+ {
66
+ "name": "manage_tp_sl",
67
+ "arguments": {
68
+ "poolId": "BTC",
69
+ "positionId": "0xABC...",
70
+ "tpPrice": "0",
71
+ "slPrice": "0"
72
+ }
73
+ }
74
+ ```
75
+
63
76
  ---
64
77
 
65
78
  ## πŸ”΅ Market Data
package/dist/server.js CHANGED
@@ -370,7 +370,7 @@ function zodSchemaToJsonSchema(zodSchema) {
370
370
  };
371
371
  }
372
372
  // ─── MCP Server ───
373
- const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.7" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
373
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.10" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
374
374
  // List tools
375
375
  server.setRequestHandler(ListToolsRequestSchema, async () => {
376
376
  return {
@@ -491,7 +491,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
491
491
  async function main() {
492
492
  const transport = new StdioServerTransport();
493
493
  await server.connect(transport);
494
- logger.info("πŸš€ MYX Trading MCP Server v3.0.7 running (stdio, pure on-chain, prod ready)");
494
+ logger.info("πŸš€ MYX Trading MCP Server v3.0.10 running (stdio, pure on-chain, prod ready)");
495
495
  }
496
496
  main().catch((err) => {
497
497
  logger.error("Fatal Server Startup Error", err);
@@ -3,6 +3,74 @@ import { getChainId, resolveClient } from "../auth/resolveClient.js";
3
3
  import { extractErrorMessage } from "../utils/errorMessage.js";
4
4
  import { ensureUnits } from "../utils/units.js";
5
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
+ ];
6
74
  function isDivideByZeroError(message) {
7
75
  const lower = message.toLowerCase();
8
76
  return (lower.includes("divide_by_zero") ||
@@ -20,6 +88,264 @@ function toPositiveBigint(input) {
20
88
  return null;
21
89
  }
22
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
+ }
23
349
  async function resolvePositiveMarketPrice30(client, poolId, chainId) {
24
350
  if (!client)
25
351
  return null;
@@ -52,9 +378,55 @@ async function resolvePositiveMarketPrice30(client, poolId, chainId) {
52
378
  * εˆ›ε»ΊεˆηΊ¦εΈ‚εœΊζ± ε­
53
379
  */
54
380
  export async function createPool(baseToken, marketId) {
55
- await resolveClient();
381
+ const { signer } = await resolveClient();
56
382
  const chainId = getChainId();
57
- return pool.createPool({ chainId, baseToken: normalizeAddress(baseToken, "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
+ };
58
430
  }
59
431
  /**
60
432
  * θŽ·ε–ζ± ε­δΏ‘ζ―
@@ -109,28 +481,80 @@ export async function getLiquidityInfo(client, poolId, marketPrice, chainIdOverr
109
481
  */
110
482
  export async function quoteDeposit(poolId, amount, slippage, chainIdOverride) {
111
483
  const chainId = chainIdOverride ?? getChainId();
112
- 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
+ }
113
498
  }
114
499
  /**
115
500
  * Quote ζ±  withdraw
116
501
  */
117
502
  export async function quoteWithdraw(poolId, amount, slippage, chainIdOverride) {
118
503
  const chainId = chainIdOverride ?? getChainId();
119
- 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
+ }
120
518
  }
121
519
  /**
122
520
  * Base ζ±  deposit
123
521
  */
124
522
  export async function baseDeposit(poolId, amount, slippage, chainIdOverride) {
125
523
  const chainId = chainIdOverride ?? getChainId();
126
- 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
+ }
127
538
  }
128
539
  /**
129
540
  * Base ζ±  withdraw
130
541
  */
131
542
  export async function baseWithdraw(poolId, amount, slippage, chainIdOverride) {
132
543
  const chainId = chainIdOverride ?? getChainId();
133
- 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
+ }
134
558
  }
135
559
  /**
136
560
  * θŽ·ε– LP δ»·ζ Ό
@@ -1,7 +1,115 @@
1
1
  import { z } from "zod";
2
- import { createPool } from "../services/poolService.js";
3
- import { resolveClient } from "../auth/resolveClient.js";
2
+ import { createPool, getMarketPoolByBaseToken } from "../services/poolService.js";
3
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
+ import { normalizeAddress } from "../utils/address.js";
6
+ import { extractErrorMessage } from "../utils/errorMessage.js";
7
+ import { decodeErrorSelector } from "../utils/errors.js";
8
+ const MARKET_ID_RE = /^0x[0-9a-fA-F]{64}$/;
9
+ function compactMessage(message) {
10
+ const flat = String(message ?? "").replace(/\s+/g, " ").trim();
11
+ if (!flat)
12
+ return "Unknown create_perp_market error.";
13
+ if (flat.length <= 420)
14
+ return flat;
15
+ return `${flat.slice(0, 420)}...`;
16
+ }
17
+ function extractSelectorCandidate(input) {
18
+ const queue = [input];
19
+ const visited = new Set();
20
+ const LONG_HEX_RE = /0x[0-9a-fA-F]{8,}/g;
21
+ const EXACT_SELECTOR_RE = /0x[0-9a-fA-F]{8}\b/g;
22
+ const KEYED_SELECTOR_RE = /\b(?:data|selector|error|revert)\s*[:=]\s*["']?(0x[0-9a-fA-F]{8,})/gi;
23
+ let fallback = null;
24
+ while (queue.length > 0) {
25
+ const current = queue.shift();
26
+ if (current === null || current === undefined)
27
+ continue;
28
+ if (visited.has(current))
29
+ continue;
30
+ visited.add(current);
31
+ if (typeof current === "string") {
32
+ const exactMatches = current.match(EXACT_SELECTOR_RE) ?? [];
33
+ for (const candidate of exactMatches) {
34
+ const normalized = candidate.toLowerCase();
35
+ if (decodeErrorSelector(normalized))
36
+ return normalized;
37
+ if (!fallback)
38
+ fallback = normalized;
39
+ }
40
+ for (const match of current.matchAll(KEYED_SELECTOR_RE)) {
41
+ const raw = String(match[1] ?? "").toLowerCase();
42
+ if (raw.length < 10)
43
+ continue;
44
+ const selector = `0x${raw.slice(2, 10)}`;
45
+ if (decodeErrorSelector(selector))
46
+ return selector;
47
+ if (!fallback)
48
+ fallback = selector;
49
+ }
50
+ const matches = current.match(LONG_HEX_RE) ?? [];
51
+ for (const hex of matches) {
52
+ const normalized = hex.toLowerCase();
53
+ if (normalized.length === 42)
54
+ continue; // likely plain address
55
+ if (normalized.length < 10)
56
+ continue;
57
+ const selector = `0x${normalized.slice(2, 10)}`;
58
+ if (decodeErrorSelector(selector)) {
59
+ return selector;
60
+ }
61
+ if (!fallback)
62
+ fallback = selector;
63
+ }
64
+ continue;
65
+ }
66
+ if (typeof current !== "object")
67
+ continue;
68
+ const record = current;
69
+ for (const value of Object.values(record)) {
70
+ queue.push(value);
71
+ }
72
+ }
73
+ return fallback;
74
+ }
75
+ function buildCreateMarketErrorPayload(args, messageLike) {
76
+ const message = compactMessage(extractErrorMessage(messageLike, "create_perp_market failed."));
77
+ const selectorCandidate = extractSelectorCandidate(messageLike);
78
+ const decodedSelector = selectorCandidate ? decodeErrorSelector(selectorCandidate) : null;
79
+ const lower = message.toLowerCase();
80
+ const code = lower.includes("marketid") && lower.includes("66")
81
+ ? "INVALID_PARAM"
82
+ : lower.includes("not a valid evm address")
83
+ ? "INVALID_PARAM"
84
+ : lower.includes("abi") && lower.includes("size")
85
+ ? "INVALID_PARAM"
86
+ : lower.includes("reverted")
87
+ ? "CONTRACT_REVERT"
88
+ : "TOOL_EXECUTION_ERROR";
89
+ const decoratedMessage = decodedSelector
90
+ ? `${message} (Decoded Contract Error: ${decodedSelector})`
91
+ : message;
92
+ const hint = code === "INVALID_PARAM"
93
+ ? "Use a valid baseToken address and a 66-char marketId config hash (0x + 64 hex)."
94
+ : "Check market allocation / permissions / duplicate pool status, then retry.";
95
+ return {
96
+ status: "error",
97
+ error: {
98
+ tool: "create_perp_market",
99
+ code,
100
+ message: decoratedMessage,
101
+ hint,
102
+ action: "Adjust parameters or prerequisites and retry.",
103
+ details: {
104
+ chainId: getChainId(),
105
+ baseToken: args?.baseToken ?? null,
106
+ marketId: args?.marketId ?? null,
107
+ selector: selectorCandidate,
108
+ decodedSelector,
109
+ },
110
+ },
111
+ };
112
+ }
5
113
  export const createPerpMarketTool = {
6
114
  name: "create_perp_market",
7
115
  description: "[LIQUIDITY] Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use get_pool_metadata (after find_pool/list_pools) to fetch an existing marketId if you don't have a specific newly allocated one.",
@@ -13,13 +121,32 @@ export const createPerpMarketTool = {
13
121
  try {
14
122
  if (!args.baseToken || !args.marketId)
15
123
  throw new Error("baseToken and marketId are required.");
124
+ const baseToken = normalizeAddress(args.baseToken, "baseToken");
125
+ const marketId = String(args.marketId).trim();
126
+ if (!MARKET_ID_RE.test(marketId)) {
127
+ throw new Error(`marketId must be a 66-character config hash (0x + 64 hex). Received: ${marketId}`);
128
+ }
16
129
  const { signer } = await resolveClient();
17
- const raw = await createPool(args.baseToken, args.marketId);
130
+ const raw = await createPool(baseToken, marketId);
18
131
  const data = await finalizeMutationResult(raw, signer, "create_perp_market");
19
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
132
+ let onChainPool = null;
133
+ try {
134
+ const resolved = await getMarketPoolByBaseToken(marketId, baseToken, getChainId());
135
+ if (resolved?.poolId && !/^0x0{64}$/i.test(String(resolved.poolId))) {
136
+ onChainPool = resolved;
137
+ }
138
+ }
139
+ catch {
140
+ onChainPool = null;
141
+ }
142
+ const payload = onChainPool
143
+ ? { ...data, onChainPool }
144
+ : data;
145
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
20
146
  }
21
147
  catch (error) {
22
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
148
+ const payload = buildCreateMarketErrorPayload(args, error);
149
+ return { content: [{ type: "text", text: JSON.stringify(payload, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
23
150
  }
24
151
  },
25
152
  };
@@ -1,5 +1,28 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { normalizeAddress } from "../utils/address.js";
4
+ import { extractErrorMessage } from "../utils/errorMessage.js";
5
+ function isNonEmptyObject(value) {
6
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
7
+ }
8
+ function buildReadErrorPayload(args, messageLike, code = "SDK_READ_ERROR") {
9
+ const chainId = args.chainId ?? getChainId();
10
+ const message = extractErrorMessage(messageLike, "Failed to read base token detail.");
11
+ return {
12
+ status: "error",
13
+ error: {
14
+ tool: "get_base_detail",
15
+ code,
16
+ message,
17
+ hint: "Check baseAddress validity and market availability on current chain.",
18
+ action: "Use list_pools/find_pool/get_pool_metadata to confirm base token, then retry.",
19
+ details: {
20
+ chainId,
21
+ baseAddress: args.baseAddress ?? null,
22
+ },
23
+ },
24
+ };
25
+ }
3
26
  export const getBaseDetailTool = {
4
27
  name: "get_base_detail",
5
28
  description: "[MARKET] Get base token details.",
@@ -11,11 +34,28 @@ export const getBaseDetailTool = {
11
34
  try {
12
35
  const { client } = await resolveClient();
13
36
  const chainId = args.chainId ?? getChainId();
14
- const result = await client.markets.getBaseDetail({ chainId, baseAddress: args.baseAddress });
15
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
37
+ const baseAddress = normalizeAddress(args.baseAddress, "baseAddress");
38
+ const result = await client.markets.getBaseDetail({ chainId, baseAddress });
39
+ const hasCode = !!result && typeof result === "object" && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, "code");
40
+ const code = hasCode ? Number(result.code) : 0;
41
+ const payload = hasCode ? result.data : result;
42
+ if (hasCode && Number.isFinite(code) && code !== 0) {
43
+ const body = buildReadErrorPayload({ ...args, baseAddress }, result.msg ?? result.message ?? result, "SDK_READ_ERROR");
44
+ return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
45
+ }
46
+ if (payload === null || payload === undefined) {
47
+ const body = buildReadErrorPayload({ ...args, baseAddress }, "get_base_detail returned empty data.", "NOT_FOUND");
48
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
49
+ }
50
+ if (typeof payload === "object" && !Array.isArray(payload) && !isNonEmptyObject(payload)) {
51
+ const body = buildReadErrorPayload({ ...args, baseAddress }, "get_base_detail returned an empty object.", "NOT_FOUND");
52
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
53
+ }
54
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
16
55
  }
17
56
  catch (error) {
18
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
57
+ const body = buildReadErrorPayload(args, error, "TOOL_EXECUTION_ERROR");
58
+ return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
19
59
  }
20
60
  },
21
61
  };
@@ -1,6 +1,34 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getOrderTypeDesc, getOrderStatusDesc, getDirectionDesc, getHistoryOrderStatusDesc, getExecTypeDesc } from "../utils/mappings.js";
4
+ function pickFirstDefined(...values) {
5
+ for (const value of values) {
6
+ if (value !== undefined && value !== null && String(value).trim() !== "") {
7
+ return value;
8
+ }
9
+ }
10
+ return undefined;
11
+ }
12
+ function toMaybeNumber(value) {
13
+ if (value === undefined || value === null || value === "")
14
+ return undefined;
15
+ const numeric = Number(value);
16
+ return Number.isFinite(numeric) ? numeric : undefined;
17
+ }
18
+ function resolveOpenOrderStatus(order) {
19
+ const statusRaw = toMaybeNumber(pickFirstDefined(order?.status, order?.orderStatus, order?.order_status));
20
+ if (statusRaw === undefined) {
21
+ return { statusRaw: undefined, statusDesc: "Open" };
22
+ }
23
+ return { statusRaw, statusDesc: getOrderStatusDesc(statusRaw) };
24
+ }
25
+ function resolveHistoryOrderStatus(order) {
26
+ const orderStatusRaw = toMaybeNumber(pickFirstDefined(order?.orderStatus, order?.order_status, order?.status));
27
+ return {
28
+ orderStatusRaw,
29
+ orderStatusDesc: getHistoryOrderStatusDesc(orderStatusRaw),
30
+ };
31
+ }
4
32
  export const getOrdersTool = {
5
33
  name: "get_orders",
6
34
  description: "[ACCOUNT] Get orders (open or history) with optional filters.",
@@ -19,7 +47,7 @@ export const getOrdersTool = {
19
47
  results.open = (openRes?.data || []).map((order) => ({
20
48
  ...order,
21
49
  orderTypeDesc: getOrderTypeDesc(order.orderType),
22
- statusDesc: getOrderStatusDesc(order.status),
50
+ ...resolveOpenOrderStatus(order),
23
51
  directionDesc: getDirectionDesc(order.direction)
24
52
  }));
25
53
  if (args.poolId) {
@@ -32,7 +60,7 @@ export const getOrdersTool = {
32
60
  results.history = (historyRes?.data || []).map((order) => ({
33
61
  ...order,
34
62
  orderTypeDesc: getOrderTypeDesc(order.orderType),
35
- orderStatusDesc: getHistoryOrderStatusDesc(order.orderStatus),
63
+ ...resolveHistoryOrderStatus(order),
36
64
  directionDesc: getDirectionDesc(order.direction),
37
65
  execTypeDesc: getExecTypeDesc(order.execType)
38
66
  }));
@@ -39,6 +39,66 @@ function normalizeUnitsInput(text) {
39
39
  return `raw:${raw}`;
40
40
  return raw;
41
41
  }
42
+ function isExplicitZeroValue(value) {
43
+ if (value === undefined || value === null)
44
+ return false;
45
+ if (typeof value === "number") {
46
+ return Number.isFinite(value) && value === 0;
47
+ }
48
+ const text = String(value).trim();
49
+ if (!text)
50
+ return false;
51
+ const payload = /^(raw|human):/i.test(text) ? text.replace(/^(raw|human):/i, "").trim() : text;
52
+ if (!payload)
53
+ return false;
54
+ if (!/^[-+]?\d+(\.\d+)?$/.test(payload))
55
+ return false;
56
+ return Number(payload) === 0;
57
+ }
58
+ function normalizeId(value) {
59
+ return String(value ?? "").trim().toLowerCase();
60
+ }
61
+ function hasOwnKey(input, key) {
62
+ return !!input && typeof input === "object" && Object.prototype.hasOwnProperty.call(input, key);
63
+ }
64
+ function isDeleteTpSlIntent(args) {
65
+ if (!hasOwnKey(args, "tpPrice") || !hasOwnKey(args, "slPrice"))
66
+ return false;
67
+ return isExplicitZeroValue(args.tpPrice) && isExplicitZeroValue(args.slPrice);
68
+ }
69
+ function isInvalidParameterRevert(error) {
70
+ const message = String(error?.message ?? error ?? "").toLowerCase();
71
+ return (message.includes("0x613970e0") ||
72
+ message.includes("invalidparameter") ||
73
+ message.includes("invalid parameter"));
74
+ }
75
+ function readOrderId(order) {
76
+ const id = String(order?.orderId ?? order?.id ?? "").trim();
77
+ return id;
78
+ }
79
+ function isTpSlOrder(order) {
80
+ const orderType = Number(order?.orderType ?? order?.type);
81
+ const operation = Number(order?.operation ?? order?.op);
82
+ if (Number.isFinite(orderType) && Number.isFinite(operation)) {
83
+ return orderType === 2 && operation === 1;
84
+ }
85
+ // Fallback: when operation/orderType are missing, try TP/SL related shape.
86
+ return isNonEmpty(order?.triggerType) && isNonEmpty(order?.positionId);
87
+ }
88
+ async function findOpenTpSlOrderIdsForPosition(client, address, poolId, positionId) {
89
+ const targetPool = normalizeId(poolId);
90
+ const targetPosition = normalizeId(positionId);
91
+ if (!targetPool || !targetPosition)
92
+ return [];
93
+ const openRes = await client.order.getOrders(address);
94
+ const openOrders = Array.isArray(openRes?.data) ? openRes.data : [];
95
+ return openOrders
96
+ .filter((order) => normalizeId(order?.poolId ?? order?.pool_id) === targetPool)
97
+ .filter((order) => normalizeId(order?.positionId ?? order?.position_id) === targetPosition)
98
+ .filter((order) => isTpSlOrder(order))
99
+ .map((order) => readOrderId(order))
100
+ .filter((id) => id.length > 0);
101
+ }
42
102
  async function findOrderSnapshot(client, address, chainId, orderId, poolId) {
43
103
  const target = String(orderId).toLowerCase();
44
104
  try {
@@ -100,6 +160,63 @@ function resolvePositionSizeRaw(positionSnapshot, baseDecimals) {
100
160
  }
101
161
  return "";
102
162
  }
163
+ async function cancelTpSlByIntent(client, address, signer, chainId, args) {
164
+ if (args.orderId) {
165
+ const raw = await client.order.cancelAllOrders([String(args.orderId)], chainId);
166
+ const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
167
+ return {
168
+ content: [{
169
+ type: "text",
170
+ text: JSON.stringify({
171
+ status: "success",
172
+ data: {
173
+ mode: "delete_tpsl_by_order",
174
+ cancelledCount: 1,
175
+ orderIds: [String(args.orderId)],
176
+ result: data
177
+ }
178
+ }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2)
179
+ }]
180
+ };
181
+ }
182
+ if (!args.positionId) {
183
+ throw new Error("positionId (or orderId) is required when deleting TP/SL with tpPrice=0 and slPrice=0.");
184
+ }
185
+ const orderIds = await findOpenTpSlOrderIdsForPosition(client, address, args.poolId, args.positionId);
186
+ if (orderIds.length === 0) {
187
+ return {
188
+ content: [{
189
+ type: "text",
190
+ text: JSON.stringify({
191
+ status: "success",
192
+ data: {
193
+ mode: "delete_tpsl_by_position",
194
+ cancelledCount: 0,
195
+ positionId: args.positionId,
196
+ message: "No open TP/SL orders found for this position."
197
+ }
198
+ }, null, 2)
199
+ }]
200
+ };
201
+ }
202
+ const raw = await client.order.cancelAllOrders(orderIds, chainId);
203
+ const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
204
+ return {
205
+ content: [{
206
+ type: "text",
207
+ text: JSON.stringify({
208
+ status: "success",
209
+ data: {
210
+ mode: "delete_tpsl_by_position",
211
+ cancelledCount: orderIds.length,
212
+ positionId: args.positionId,
213
+ orderIds,
214
+ result: data
215
+ }
216
+ }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2)
217
+ }]
218
+ };
219
+ }
103
220
  export const manageTpSlTool = {
104
221
  name: "manage_tp_sl",
105
222
  description: "[TRADE] Set or update Take Profit (TP) and Stop Loss (SL) for a position.",
@@ -126,6 +243,10 @@ export const manageTpSlTool = {
126
243
  const chainId = args.chainId ?? getChainId();
127
244
  const direction = normalizeDirectionInput(args.direction);
128
245
  const slippagePct = args.slippagePct ?? "100";
246
+ const wantsDeleteTpSl = isDeleteTpSlIntent(args);
247
+ if (wantsDeleteTpSl) {
248
+ return await cancelTpSlByIntent(client, address, signer, chainId, args);
249
+ }
129
250
  const { setPositionTpSl, updateOrderTpSl } = await import("../services/tradeService.js");
130
251
  const { getMarketDetail } = await import("../services/marketService.js");
131
252
  const marketRes = await getMarketDetail(client, args.poolId, chainId);
@@ -162,19 +283,27 @@ export const manageTpSlTool = {
162
283
  if ((slPrice && !slSize) || (!slPrice && slSize)) {
163
284
  throw new Error("SL update requires both slPrice and slSize (or resolvable existing SL fields).");
164
285
  }
165
- raw = await updateOrderTpSl(client, address, {
166
- orderId: args.orderId,
167
- marketId: market.marketId,
168
- poolId: args.poolId,
169
- size: normalizeUnitsInput(size),
170
- price: normalizeUnitsInput(price),
171
- tpPrice: tpPrice ? normalizeUnitsInput(tpPrice) : "0",
172
- tpSize: tpSize ? normalizeUnitsInput(tpSize) : "0",
173
- slPrice: slPrice ? normalizeUnitsInput(slPrice) : "0",
174
- slSize: slSize ? normalizeUnitsInput(slSize) : "0",
175
- quoteToken: market.quoteToken,
176
- useOrderCollateral: args.useOrderCollateral ?? true
177
- }, chainId);
286
+ try {
287
+ raw = await updateOrderTpSl(client, address, {
288
+ orderId: args.orderId,
289
+ marketId: market.marketId,
290
+ poolId: args.poolId,
291
+ size: normalizeUnitsInput(size),
292
+ price: normalizeUnitsInput(price),
293
+ tpPrice: tpPrice ? normalizeUnitsInput(tpPrice) : "0",
294
+ tpSize: tpSize ? normalizeUnitsInput(tpSize) : "0",
295
+ slPrice: slPrice ? normalizeUnitsInput(slPrice) : "0",
296
+ slSize: slSize ? normalizeUnitsInput(slSize) : "0",
297
+ quoteToken: market.quoteToken,
298
+ useOrderCollateral: args.useOrderCollateral ?? true
299
+ }, chainId);
300
+ }
301
+ catch (updateError) {
302
+ if (isDeleteTpSlIntent(args) || (isInvalidParameterRevert(updateError) && isExplicitZeroValue(args.tpPrice) && isExplicitZeroValue(args.slPrice))) {
303
+ return await cancelTpSlByIntent(client, address, signer, chainId, args);
304
+ }
305
+ throw updateError;
306
+ }
178
307
  }
179
308
  else {
180
309
  // Create new
@@ -211,18 +340,26 @@ export const manageTpSlTool = {
211
340
  if (needsSlSize)
212
341
  slSizeInput = `raw:${positionSizeRaw}`;
213
342
  }
214
- raw = await setPositionTpSl(client, address, {
215
- poolId: args.poolId,
216
- positionId: args.positionId,
217
- direction: resolvedDirection,
218
- leverage: Number(resolvedLeverage),
219
- executionFeeToken: args.executionFeeToken || market.quoteToken,
220
- slippagePct,
221
- tpPrice: args.tpPrice,
222
- tpSize: tpSizeInput || undefined,
223
- slPrice: args.slPrice,
224
- slSize: slSizeInput || undefined
225
- }, chainId);
343
+ try {
344
+ raw = await setPositionTpSl(client, address, {
345
+ poolId: args.poolId,
346
+ positionId: args.positionId,
347
+ direction: resolvedDirection,
348
+ leverage: Number(resolvedLeverage),
349
+ executionFeeToken: args.executionFeeToken || market.quoteToken,
350
+ slippagePct,
351
+ tpPrice: args.tpPrice,
352
+ tpSize: tpSizeInput || undefined,
353
+ slPrice: args.slPrice,
354
+ slSize: slSizeInput || undefined
355
+ }, chainId);
356
+ }
357
+ catch (setError) {
358
+ if (isDeleteTpSlIntent(args) || (isInvalidParameterRevert(setError) && isExplicitZeroValue(args.tpPrice) && isExplicitZeroValue(args.slPrice))) {
359
+ return await cancelTpSlByIntent(client, address, signer, chainId, args);
360
+ }
361
+ throw setError;
362
+ }
226
363
  }
227
364
  const data = await finalizeMutationResult(raw, signer, "manage_tp_sl");
228
365
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
@@ -28,6 +28,7 @@ export function injectBrowserProviderMock(chainId, provider, signer) {
28
28
  tx.from = signer.address;
29
29
  const txRes = await signer.sendTransaction(tx);
30
30
  console.error(`[MCP Router] δΊ€ζ˜“ε·²ζδΊ€οΌŒε“ˆεΈŒ: ${txRes.hash}`);
31
+ g.__MCP_LAST_TX_HASH = txRes.hash;
31
32
  return txRes.hash;
32
33
  }
33
34
  if (method === "eth_estimateGas" || method === "eth_gasPrice" || method === "eth_getBalance" || method === "eth_call") {
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Direction: LONG=0, SHORT=1
3
3
  */
4
+ function formatUnknown(value) {
5
+ if (value === undefined || value === null || value === "")
6
+ return "Unknown";
7
+ return `Unknown(${String(value)})`;
8
+ }
4
9
  export const getDirectionDesc = (direction) => {
5
10
  switch (direction) {
6
11
  case 0: return "Long";
7
12
  case 1: return "Short";
8
- default: return `Unknown(${direction})`;
13
+ default: return formatUnknown(direction);
9
14
  }
10
15
  };
11
16
  /**
@@ -17,7 +22,7 @@ export const getOrderTypeDesc = (type) => {
17
22
  case 1: return "Limit";
18
23
  case 2: return "Stop";
19
24
  case 3: return "Conditional";
20
- default: return `Unknown(${type})`;
25
+ default: return formatUnknown(type);
21
26
  }
22
27
  };
23
28
  /**
@@ -31,7 +36,7 @@ export const getOrderStatusDesc = (status) => {
31
36
  case 3: return "Cancelled";
32
37
  case 4: return "Rejected";
33
38
  case 5: return "Expired";
34
- default: return `Unknown(${status})`;
39
+ default: return formatUnknown(status);
35
40
  }
36
41
  };
37
42
  /**
@@ -43,7 +48,7 @@ export const getHistoryOrderStatusDesc = (status) => {
43
48
  case 2: return "Expired";
44
49
  case 9: return "Successful";
45
50
  case 8: return "PartialFilled";
46
- default: return `Unknown(${status})`;
51
+ default: return formatUnknown(status);
47
52
  }
48
53
  };
49
54
  /**
@@ -83,7 +88,7 @@ export const getTradeFlowTypeDesc = (type) => {
83
88
  13: "ReferralReward",
84
89
  14: "ReferralRewardClaim"
85
90
  };
86
- return types[type] || `Unknown(${type})`;
91
+ return types[type] || formatUnknown(type);
87
92
  };
88
93
  /**
89
94
  * ExecTypeEnum
@@ -100,7 +105,7 @@ export const getExecTypeDesc = (type) => {
100
105
  8: "EarlyClose",
101
106
  9: "MarketClose"
102
107
  };
103
- return types[type] || `Unknown(${type})`;
108
+ return types[type] || formatUnknown(type);
104
109
  };
105
110
  /**
106
111
  * CloseTypeEnum
@@ -118,7 +123,7 @@ export const getCloseTypeDesc = (type) => {
118
123
  8: "SL",
119
124
  9: "Increase"
120
125
  };
121
- return types[type] || `Unknown(${type})`;
126
+ return types[type] || formatUnknown(type);
122
127
  };
123
128
  /**
124
129
  * ζ˜ ε°„θΎ“ε…₯方向为数值
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.0.7",
3
+ "version": "3.0.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -16,7 +16,8 @@
16
16
  "start": "node dist/server.js",
17
17
  "dev": "tsx src/server.ts",
18
18
  "test": "tsx tests/test_sepolia.ts",
19
- "test:new-tools": "tsx tests/test_new_tools_p0_p1.ts"
19
+ "test:new-tools": "tsx tests/test_new_tools_p0_p1.ts",
20
+ "test:manage-tpsl-delete": "tsx tests/test_manage_tpsl_delete_live.ts"
20
21
  },
21
22
  "dependencies": {
22
23
  "@modelcontextprotocol/sdk": "^1.27.1",