@michaleffffff/mcp-trading-server 2.5.3 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,13 @@ Copy the example configuration:
62
62
  cp .env.example .env
63
63
  ```
64
64
 
65
+ ## Non-Auth Mode (No AccessToken)
66
+
67
+ This MCP server runs in **non-auth mode**:
68
+
69
+ - It does **not** call `myxClient.auth()` and does **not** manage AccessTokens.
70
+ - All tools are designed to work without AccessToken authentication.
71
+
65
72
  ## User Key Configuration (Safe Practices)
66
73
 
67
74
  - **Local dev**: Put `PRIVATE_KEY` in a local `.env` (ignored by `.gitignore`) and never commit it.
@@ -108,19 +115,43 @@ The server will run using `stdio` transport and can be connected by any MCP-comp
108
115
  2. **Configure env**: Copy `.env.example` to `.env` and fill in `PRIVATE_KEY`, `RPC_URL`, etc.
109
116
  3. **Start the server**: Run `npm run build` then `npm run start` (use `npm run dev` for development).
110
117
  4. **Configure your MCP client**: Set `command/args/env` in your MCP client (see Claude Desktop example below).
111
- 5. **Common flow**: Use `search_market` to get `poolId`, then `get_market_price` / `get_oracle_price` to view prices, and finally `execute_trade` to open a position (`direction: 0`=LONG, `1`=SHORT; `collateralAmount` is human‑readable).
118
+ 5. **Common flow**: Use `search_market` to get `poolId`, then optionally `get_market_price` / `get_oracle_price` to view prices, and finally use `open_position_simple` (recommended) to open a position.
112
119
 
113
120
  **Minimal open position example:**
114
121
 
115
122
  ```json
116
123
  {
117
- "poolId": "0x...",
124
+ "keyword": "BTC",
118
125
  "direction": 0,
119
126
  "collateralAmount": "100",
120
- "leverage": 5
127
+ "leverage": 5,
128
+ "orderType": "MARKET",
129
+ "autoDeposit": false
121
130
  }
122
131
  ```
123
132
 
133
+ **Dry run (no transaction) example:**
134
+
135
+ ```json
136
+ {
137
+ "keyword": "BTC",
138
+ "direction": 0,
139
+ "collateralAmount": "100",
140
+ "leverage": 5,
141
+ "orderType": "MARKET",
142
+ "autoDeposit": false,
143
+ "dryRun": true
144
+ }
145
+ ```
146
+
147
+ ## Units & Precision
148
+
149
+ - **Human by default**: For trading tools (e.g. `open_position_simple`), amounts like `"100"` mean **human-readable units**.
150
+ - **Raw units (explicit)**: Prefix with `raw:` to pass raw units exactly (e.g. `raw:100000000` for 100 USDC with 6 decimals).
151
+ - **Price**: `"price"` is human by default and will be converted to **30-decimal** format internally. Use `raw:` to pass a 30-decimal integer directly.
152
+ - **Slippage**: `slippagePct` uses 4-decimal raw units: `100` = `1%`, `50` = `0.5%`, `1000` = `10%`.
153
+ - **Note**: Some account-transfer tools (e.g. `account_deposit`, `account_withdraw`, `check_approval`) use **raw integer strings**.
154
+
124
155
  ---
125
156
 
126
157
  ## Secure Deployment Template (Systemd Example)
@@ -192,14 +223,15 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
192
223
 
193
224
  # Tools
194
225
 
195
- The server exposes 37 tools categorized for AI:
226
+ The server exposes 38 tools categorized for AI:
196
227
 
197
228
  ### Trading Operations
229
+ * **open_position_simple**: High-level open position helper (recommended). Computes price/size/tradingFee internally.
198
230
  * **execute_trade**: Execute a new trade or add to an existing position.
199
231
  * **close_position**: Close an open position.
200
232
  * **close_all_positions**: Emergency: close ALL open positions in a pool at once.
201
233
  * **cancel_order**: Cancel an open order by its order ID.
202
- * **cancel_all_orders**: Cancel all open orders at once (or a provided order ID list).
234
+ * **cancel_all_orders**: Cancel multiple open orders by order IDs (use `get_open_orders` first to get IDs).
203
235
  * **set_tp_sl**: Set take profit and stop loss prices for an open position.
204
236
  * **adjust_margin**: Adjust the margin (collateral) of an open position.
205
237
  * **get_user_trading_fee_rate**: Query current maker/taker fee rates by asset class and risk tier.
@@ -263,7 +295,7 @@ Example MCP client usage workflow:
263
295
  1. Connect to the MCP server.
264
296
  2. Read the `system://state` resource to confirm wallet connectivity.
265
297
  3. Call the `search_market` tool (e.g., `keyword: "ETH"`) to retrieve the `poolId`.
266
- 4. Call `execute_trade` passing the retrieved `poolId` to open a 10x long position.
298
+ 4. Call `open_position_simple` passing the retrieved `poolId` to open a 10x long position.
267
299
 
268
300
  Example integration scripts are located in:
269
301
  `examples/basicUsage.ts`
package/dist/server.js CHANGED
@@ -81,7 +81,7 @@ function zodSchemaToJsonSchema(zodSchema) {
81
81
  };
82
82
  }
83
83
  // ─── MCP Server ───
84
- const server = new Server({ name: "myx-mcp-trading-server", version: "2.5.3" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
84
+ const server = new Server({ name: "myx-mcp-trading-server", version: "2.6.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
85
85
  // List tools
86
86
  server.setRequestHandler(ListToolsRequestSchema, async () => {
87
87
  return {
@@ -181,7 +181,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
181
181
  async function main() {
182
182
  const transport = new StdioServerTransport();
183
183
  await server.connect(transport);
184
- logger.info("🚀 MYX Trading MCP Server v2.5.3 running (stdio, pure on-chain, prod ready)");
184
+ logger.info("🚀 MYX Trading MCP Server v2.6.0 running (stdio, pure on-chain, prod ready)");
185
185
  }
186
186
  main().catch((err) => {
187
187
  logger.error("Fatal Server Startup Error", err);
@@ -2,11 +2,12 @@ import { getChainId } from "../auth/resolveClient.js";
2
2
  import { getMarketStateDesc } from "../utils/mappings.js";
3
3
  export async function getMarketPrice(client, poolId, chainIdOverride) {
4
4
  const chainId = chainIdOverride ?? getChainId();
5
- const ticker = await client.markets.getTickerList({
5
+ const tickerRes = await client.markets.getTickerList({
6
6
  chainId,
7
7
  poolIds: [poolId],
8
8
  });
9
- return ticker?.data?.[0] ?? null;
9
+ const rows = Array.isArray(tickerRes) ? tickerRes : (tickerRes?.data ?? []);
10
+ return rows?.[0] ?? null;
10
11
  }
11
12
  export async function getOraclePrice(client, poolId, chainIdOverride) {
12
13
  const chainId = chainIdOverride ?? getChainId();
@@ -78,3 +79,63 @@ export async function getPoolLevelConfig(client, poolId, chainIdOverride) {
78
79
  const chainId = chainIdOverride ?? getChainId();
79
80
  return client.markets.getPoolLevelConfig(poolId, chainId);
80
81
  }
82
+ /**
83
+ * 智能解析 Pool ID (支持 ID 校验与关键词回退)
84
+ */
85
+ export async function resolvePool(client, poolId, keyword) {
86
+ const chainId = getChainId();
87
+ let pid = poolId?.trim();
88
+ // 1. 如果提供了 poolId,先尝试验证其是否存在
89
+ if (pid) {
90
+ try {
91
+ const detail = await client.markets.getMarketDetail({ chainId, poolId: pid });
92
+ const marketId = detail?.marketId || detail?.data?.marketId;
93
+ if (marketId)
94
+ return pid;
95
+ }
96
+ catch {
97
+ // 验证失败,记录并尝试通过 keyword 寻址
98
+ console.warn(`[resolvePool] PoolId ${pid} not found, trying keyword fallback...`);
99
+ }
100
+ }
101
+ // 2. 如果提供了 keyword,执行搜索
102
+ const kw = keyword?.trim();
103
+ if (kw) {
104
+ const markets = await searchMarket(client, kw, 10);
105
+ if (markets.length > 0) {
106
+ return markets[0].poolId;
107
+ }
108
+ }
109
+ // 3. 最后手段:遍历全量活跃池列表
110
+ if (kw || pid) {
111
+ const poolListRes = await client.api.getPoolList().catch(() => null);
112
+ if (poolListRes) {
113
+ // 这里的逻辑参考 openPositionSimple 中的 collectPoolRows
114
+ const collect = (input) => {
115
+ if (Array.isArray(input))
116
+ return input.flatMap(collect);
117
+ if (!input || typeof input !== "object")
118
+ return [];
119
+ if (input.poolId || input.pool_id)
120
+ return [input];
121
+ return Object.values(input).flatMap(collect);
122
+ };
123
+ const rows = collect(poolListRes.data ?? poolListRes);
124
+ const searchKey = (kw || pid || "").toUpperCase();
125
+ const match = rows.find((row) => {
126
+ if (Number(row?.state) !== 2)
127
+ return false;
128
+ const base = String(row?.baseSymbol ?? "").toUpperCase();
129
+ const pair = String(row?.baseQuoteSymbol ?? "").toUpperCase();
130
+ const id = String(row?.poolId ?? row?.pool_id ?? "").toUpperCase();
131
+ return base === searchKey || pair.includes(searchKey) || id === searchKey;
132
+ });
133
+ if (match) {
134
+ return String(match.poolId ?? match.pool_id);
135
+ }
136
+ }
137
+ }
138
+ if (pid)
139
+ return pid; // 如果没有更好的选择,返回原 ID
140
+ throw new Error(`Could not resolve pool for keyword: ${kw} / poolId: ${pid}`);
141
+ }
@@ -60,10 +60,37 @@ export async function openPosition(client, address, args) {
60
60
  const baseDecimals = poolData.baseDecimals || 18;
61
61
  const quoteDecimals = poolData.quoteDecimals || 6;
62
62
  const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
63
+ // --- Pre-flight Check: minOrderSizeInUsd ---
64
+ try {
65
+ const levelRes = await client.markets.getPoolLevelConfig(args.poolId, chainId);
66
+ // SDK might return { levelConfig: ... } or { data: { levelConfig: ... } }
67
+ const levelConfig = levelRes?.levelConfig || levelRes?.data?.levelConfig;
68
+ const minOrderSizeInUsdRaw = levelConfig?.minOrderSizeInUsd;
69
+ if (minOrderSizeInUsdRaw) {
70
+ // If the value is very large (e.g. 100,000,000), it might be scaled by 1e6.
71
+ // But based on observation, it's often already human-friendly (e.g. 100).
72
+ let minOrderSizeInUsd = Number(minOrderSizeInUsdRaw);
73
+ if (minOrderSizeInUsd > 1000000)
74
+ minOrderSizeInUsd /= 1000000;
75
+ const leverage = Number(args.leverage || 1);
76
+ const collateralHuman = Number(collateralRaw) / (10 ** quoteDecimals);
77
+ const notionalUsd = collateralHuman * leverage;
78
+ if (notionalUsd > 0 && notionalUsd < minOrderSizeInUsd) {
79
+ throw new Error(`Order size out of range: Calculated notional ${notionalUsd.toFixed(2)} USD is less than the minimum required ${minOrderSizeInUsd} USD for this pool. ` +
80
+ `Please increase your collateral or leverage.`);
81
+ }
82
+ }
83
+ }
84
+ catch (e) {
85
+ if (e.message.includes("Order size out of range"))
86
+ throw e;
87
+ console.warn(`[tradeService] Limit check skipped: ${e.message}`);
88
+ }
63
89
  const sizeRaw = ensureUnits(args.size, baseDecimals, "size");
64
90
  const priceRaw = ensureUnits(args.price, 30, "price");
65
91
  const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee");
66
92
  // --- Auto-Deposit Logic (Strict) ---
93
+ const allowAutoDeposit = args.autoDeposit !== false;
67
94
  console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
68
95
  const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
69
96
  let marginBalanceRaw = BigInt(0);
@@ -80,6 +107,10 @@ export async function openPosition(client, address, args) {
80
107
  }
81
108
  const requiredRaw = BigInt(collateralRaw);
82
109
  if (marginBalanceRaw < requiredRaw) {
110
+ if (!allowAutoDeposit) {
111
+ throw new Error(`Insufficient marginBalance (${marginBalanceRaw.toString()}) for required collateral (${requiredRaw.toString()}). ` +
112
+ `Deposit to trading account first (account_deposit) or retry with autoDeposit=true.`);
113
+ }
83
114
  const neededRaw = requiredRaw - marginBalanceRaw;
84
115
  console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
85
116
  if (walletBalanceRaw < neededRaw) {
@@ -1,4 +1,5 @@
1
1
  // Tools — 交易
2
+ export { openPositionSimpleTool } from "./openPositionSimple.js";
2
3
  export { executeTradeTool } from "./executeTrade.js";
3
4
  export { cancelOrderTool } from "./cancelOrder.js";
4
5
  export { cancelAllOrdersTool } from "./cancelAllOrders.js";
@@ -0,0 +1,292 @@
1
+ import { z } from "zod";
2
+ import { OrderType } from "@myx-trade/sdk";
3
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
+ import { resolvePool } from "../services/marketService.js";
5
+ import { openPosition } from "../services/tradeService.js";
6
+ import { normalizeAddress } from "../utils/address.js";
7
+ import { finalizeMutationResult } from "../utils/mutationResult.js";
8
+ import { mapDirection, mapOrderType } from "../utils/mappings.js";
9
+ import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
10
+ import { parseUserPrice30, parseUserUnits } from "../utils/units.js";
11
+ import { verifyTradeOutcome } from "../utils/verification.js";
12
+ const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
13
+ function pow10(decimals) {
14
+ if (!Number.isInteger(decimals) || decimals < 0) {
15
+ throw new Error(`Invalid decimals: ${decimals}`);
16
+ }
17
+ return 10n ** BigInt(decimals);
18
+ }
19
+ function asBigint(raw, label) {
20
+ try {
21
+ return BigInt(raw);
22
+ }
23
+ catch {
24
+ throw new Error(`${label} must be an integer string.`);
25
+ }
26
+ }
27
+ function pickMarketDetail(res) {
28
+ if (!res)
29
+ return null;
30
+ if (res.data && typeof res.data === "object")
31
+ return res.data;
32
+ if (res.marketId)
33
+ return res;
34
+ return null;
35
+ }
36
+ export const openPositionSimpleTool = {
37
+ name: "open_position_simple",
38
+ description: "High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
39
+ schema: {
40
+ poolId: z.string().optional().describe("Pool ID. Provide either poolId or keyword."),
41
+ keyword: z.string().optional().describe('Market keyword, e.g. "BTC". Provide either keyword or poolId.'),
42
+ direction: z.any().describe("LONG (0), SHORT (1) or string BUY/SELL/LONG/SHORT"),
43
+ collateralAmount: z.coerce
44
+ .string()
45
+ .describe("Collateral amount in quote token units (human by default; 'raw:' prefix for raw)."),
46
+ leverage: z.coerce.number().int().positive().describe("Leverage (integer, e.g. 5, 10)."),
47
+ orderType: z.union([z.string(), z.number()]).optional().describe("MARKET, LIMIT, STOP (default MARKET)."),
48
+ price: z.coerce
49
+ .string()
50
+ .optional()
51
+ .describe("Price (human by default; 30-dec raw with 'raw:' prefix). Required for LIMIT/STOP."),
52
+ size: z.coerce
53
+ .string()
54
+ .optional()
55
+ .describe("Position size in base token units (human by default; 'raw:' prefix for raw). If omitted, computed from collateral*leverage/price."),
56
+ slippagePct: z.coerce
57
+ .string()
58
+ .optional()
59
+ .describe(`${SLIPPAGE_PCT_4DP_DESC} Default 100 (=1%).`),
60
+ postOnly: z.coerce.boolean().optional().describe("Post-only (default false)."),
61
+ executionFeeToken: z.string().optional().describe("Execution fee token address (default quoteToken)."),
62
+ assetClass: z.coerce.number().int().nonnegative().optional().describe("Fee query assetClass (default 1)."),
63
+ riskTier: z.coerce.number().int().nonnegative().optional().describe("Fee query riskTier (default 1)."),
64
+ tradingFee: z.coerce
65
+ .string()
66
+ .optional()
67
+ .describe("Trading fee in quote token units (human by default; 'raw:' prefix for raw). If omitted, computed via getUserTradingFeeRate."),
68
+ autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve quote token spend when needed (default false)."),
69
+ approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false approves exact amount)."),
70
+ autoDeposit: z.coerce
71
+ .boolean()
72
+ .optional()
73
+ .describe("If true, auto-deposit to margin account when marginBalance is insufficient (default false)."),
74
+ dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
75
+ },
76
+ handler: async (args) => {
77
+ try {
78
+ const { client, address, signer } = await resolveClient();
79
+ const chainId = getChainId();
80
+ // 1) Resolve poolId (poolId or keyword)
81
+ const poolId = await resolvePool(client, args.poolId, args.keyword);
82
+ // 2) Fetch market detail for decimals, quote token, marketId
83
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
84
+ const detail = pickMarketDetail(detailRes);
85
+ if (!detail) {
86
+ throw new Error(`Could not resolve market detail for poolId=${poolId}`);
87
+ }
88
+ const marketId = String(detail.marketId ?? "").trim();
89
+ if (!marketId)
90
+ throw new Error(`marketId missing from market detail for poolId=${poolId}`);
91
+ const baseDecimals = Number(detail.baseDecimals ?? 18);
92
+ const quoteDecimals = Number(detail.quoteDecimals ?? 6);
93
+ const quoteToken = String(detail.quoteToken ?? "").trim();
94
+ if (!quoteToken)
95
+ throw new Error(`quoteToken missing from market detail for poolId=${poolId}`);
96
+ // Optional: read pool level config (best-effort) for defaults like assetClass
97
+ let poolLevelConfig = null;
98
+ try {
99
+ poolLevelConfig = await client.markets.getPoolLevelConfig(poolId, chainId);
100
+ }
101
+ catch {
102
+ poolLevelConfig = null;
103
+ }
104
+ const defaultAssetClass = Number(poolLevelConfig?.levelConfig?.assetClass ?? 0);
105
+ // 3) Parse & validate primary inputs
106
+ const dir = mapDirection(args.direction);
107
+ const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
108
+ const collateralRawBig = asBigint(collateralRaw, "collateralAmount");
109
+ if (collateralRawBig <= 0n)
110
+ throw new Error("collateralAmount must be > 0.");
111
+ const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
112
+ if (maxTradeAmountHuman) {
113
+ const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
114
+ const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
115
+ if (collateralRawBig > maxTradeRawBig) {
116
+ throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
117
+ }
118
+ }
119
+ const orderType = mapOrderType(args.orderType ?? 0);
120
+ const postOnly = Boolean(args.postOnly ?? false);
121
+ const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "100");
122
+ const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
123
+ // 4) Determine reference price (30 decimals)
124
+ let price30;
125
+ let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
126
+ if (orderType === OrderType.MARKET) {
127
+ try {
128
+ const oracle = await client.utils.getOraclePrice(poolId, chainId);
129
+ price30 = parseUserPrice30(oracle.price, "oraclePrice");
130
+ priceMeta = { source: "oracle", publishTime: oracle.publishTime, oracleType: oracle.oracleType, human: oracle.price };
131
+ }
132
+ catch (e) {
133
+ const tickers = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
134
+ const row = Array.isArray(tickers) ? tickers[0] : tickers?.data?.[0];
135
+ if (!row?.price)
136
+ throw new Error(`Failed to fetch oracle and ticker price for poolId=${poolId}: ${e?.message || e}`);
137
+ price30 = parseUserPrice30(row.price, "marketPrice");
138
+ priceMeta = { source: "ticker", publishTime: null, oracleType: null, human: row.price };
139
+ }
140
+ }
141
+ else {
142
+ const userPrice = String(args.price ?? "").trim();
143
+ if (!userPrice)
144
+ throw new Error("price is required for LIMIT/STOP.");
145
+ price30 = parseUserPrice30(userPrice, "price");
146
+ priceMeta = { source: "user", publishTime: null, oracleType: null, human: userPrice };
147
+ }
148
+ const price30Big = asBigint(price30, "price");
149
+ if (price30Big <= 0n)
150
+ throw new Error("price must be > 0.");
151
+ // 5) Compute or parse size (base raw units)
152
+ let sizeRaw = "";
153
+ let sizeMeta = { source: "computed" };
154
+ const userSize = String(args.size ?? "").trim();
155
+ if (userSize) {
156
+ sizeRaw = parseUserUnits(userSize, baseDecimals, "size");
157
+ sizeMeta = { source: "user" };
158
+ }
159
+ else {
160
+ const notionalQuoteRaw = collateralRawBig * BigInt(args.leverage);
161
+ const numerator = notionalQuoteRaw * pow10(30 + baseDecimals);
162
+ const denominator = price30Big * pow10(quoteDecimals);
163
+ const computed = numerator / denominator;
164
+ if (computed <= 0n) {
165
+ throw new Error("Computed size is 0. Increase collateralAmount/leverage or check price.");
166
+ }
167
+ sizeRaw = computed.toString();
168
+ sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
169
+ }
170
+ // 6) Compute tradingFee (quote raw units)
171
+ let tradingFeeRaw = null;
172
+ let tradingFeeMeta = { source: "computed" };
173
+ const userTradingFee = String(args.tradingFee ?? "").trim();
174
+ if (userTradingFee) {
175
+ tradingFeeRaw = parseUserUnits(userTradingFee, quoteDecimals, "tradingFee");
176
+ tradingFeeMeta = { source: "user" };
177
+ }
178
+ else {
179
+ const assetClass = Number(args.assetClass ?? defaultAssetClass);
180
+ const riskTier = Number(args.riskTier ?? 0);
181
+ tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: null, error: null };
182
+ try {
183
+ const feeRes = await client.utils.getUserTradingFeeRate(assetClass, riskTier, chainId);
184
+ if (feeRes && Number(feeRes.code) === 0 && feeRes.data) {
185
+ const rateRaw = postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
186
+ tradingFeeMeta.feeRate = rateRaw;
187
+ const rate = asBigint(String(rateRaw), "feeRate");
188
+ const fee = (collateralRawBig * rate) / 1000000n;
189
+ tradingFeeRaw = fee.toString();
190
+ }
191
+ else {
192
+ tradingFeeMeta.error = feeRes?.message ?? "fee_rate_unavailable";
193
+ }
194
+ }
195
+ catch (e) {
196
+ tradingFeeMeta.error = e?.message || String(e);
197
+ }
198
+ if (tradingFeeRaw === null && !args.dryRun) {
199
+ throw new Error(`Failed to fetch user trading fee rate (assetClass=${assetClass}, riskTier=${riskTier}). Provide tradingFee manually if needed.`);
200
+ }
201
+ }
202
+ const prep = {
203
+ chainId,
204
+ poolId,
205
+ marketId,
206
+ baseDecimals,
207
+ quoteDecimals,
208
+ quoteToken,
209
+ direction: dir,
210
+ collateralRaw,
211
+ sizeRaw,
212
+ price30,
213
+ leverage: Number(args.leverage),
214
+ orderType,
215
+ triggerType: 0,
216
+ timeInForce: 0,
217
+ postOnly,
218
+ slippagePct,
219
+ executionFeeToken,
220
+ tradingFeeRaw,
221
+ tradingFeeMeta,
222
+ autoDeposit: Boolean(args.autoDeposit ?? false),
223
+ priceMeta,
224
+ sizeMeta,
225
+ };
226
+ if (args.dryRun) {
227
+ return {
228
+ content: [
229
+ {
230
+ type: "text",
231
+ text: JSON.stringify({ status: "success", data: { dryRun: true, prepared: prep } }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2),
232
+ },
233
+ ],
234
+ };
235
+ }
236
+ // 7) Optional approval
237
+ let approval = null;
238
+ if (args.autoApprove) {
239
+ const requiredApprovalRaw = tradingFeeRaw && /^\d+$/.test(String(tradingFeeRaw))
240
+ ? (collateralRawBig + asBigint(String(tradingFeeRaw), "tradingFee")).toString()
241
+ : collateralRaw;
242
+ const needApproval = await client.utils.needsApproval(address, chainId, quoteToken, requiredApprovalRaw);
243
+ if (needApproval) {
244
+ const approveAmount = args.approveMax ? MAX_UINT256 : requiredApprovalRaw;
245
+ const rawApprove = await client.utils.approveAuthorization({
246
+ chainId,
247
+ quoteAddress: quoteToken,
248
+ amount: approveAmount,
249
+ });
250
+ approval = await finalizeMutationResult(rawApprove, signer, "approve_authorization");
251
+ }
252
+ else {
253
+ approval = { needApproval: false };
254
+ }
255
+ }
256
+ // 8) Submit increase order using existing trade service
257
+ const raw = await openPosition(client, address, {
258
+ poolId,
259
+ positionId: "",
260
+ orderType,
261
+ triggerType: 0,
262
+ direction: dir,
263
+ collateralAmount: `raw:${collateralRaw}`,
264
+ size: `raw:${sizeRaw}`,
265
+ price: `raw:${price30}`,
266
+ timeInForce: 0,
267
+ postOnly,
268
+ slippagePct,
269
+ executionFeeToken,
270
+ leverage: Number(args.leverage),
271
+ tradingFee: `raw:${String(tradingFeeRaw)}`,
272
+ marketId,
273
+ autoDeposit: Boolean(args.autoDeposit ?? false),
274
+ });
275
+ const data = await finalizeMutationResult(raw, signer, "open_position_simple");
276
+ const txHash = data.confirmation?.txHash;
277
+ const verification = txHash ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
278
+ const payload = { prepared: prep, approval, ...data, verification };
279
+ return {
280
+ content: [
281
+ {
282
+ type: "text",
283
+ text: JSON.stringify({ status: "success", data: payload }, (_, v) => (typeof v === "bigint" ? v.toString() : v), 2),
284
+ },
285
+ ],
286
+ };
287
+ }
288
+ catch (error) {
289
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
290
+ }
291
+ },
292
+ };
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { setPositionTpSl } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
+ import { mapDirection, mapTriggerType } from "../utils/mappings.js";
5
6
  import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
7
  export const setTpSlTool = {
7
8
  name: "set_tp_sl",
@@ -9,23 +10,30 @@ export const setTpSlTool = {
9
10
  schema: {
10
11
  poolId: z.string().describe("Pool ID"),
11
12
  positionId: z.string().describe("Position ID"),
12
- direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
13
+ direction: z.any().describe("LONG (0), SHORT (1) or string BUY/SELL/LONG/SHORT"),
13
14
  leverage: z.coerce.number().describe("Leverage"),
14
15
  executionFeeToken: z.string().describe("Execution fee token address"),
15
- tpTriggerType: z.coerce.number().int().min(0).max(2).describe("TP trigger type"),
16
- slTriggerType: z.coerce.number().int().min(0).max(2).describe("SL trigger type"),
16
+ tpTriggerType: z.union([z.number(), z.string()]).optional().describe("TP trigger type (0=NONE, 1=GTE, 2=LTE)"),
17
+ slTriggerType: z.union([z.number(), z.string()]).optional().describe("SL trigger type (0=NONE, 1=GTE, 2=LTE)"),
17
18
  slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
18
19
  message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
19
20
  }).describe(SLIPPAGE_PCT_4DP_DESC),
20
- tpPrice: z.coerce.string().regex(/^\d+$/).optional().describe("TP price raw units (30 decimals)"),
21
- tpSize: z.coerce.string().regex(/^\d+$/).optional().describe("TP size raw units"),
22
- slPrice: z.coerce.string().regex(/^\d+$/).optional().describe("SL price raw units (30 decimals)"),
23
- slSize: z.coerce.string().regex(/^\d+$/).optional().describe("SL size raw units"),
21
+ tpPrice: z.coerce.string().optional().describe("TP price (raw or human-readable, 30 decimals)"),
22
+ tpSize: z.coerce.string().optional().describe("TP size (raw or human-readable)"),
23
+ slPrice: z.coerce.string().optional().describe("SL price (raw or human-readable, 30 decimals)"),
24
+ slSize: z.coerce.string().optional().describe("SL size (raw or human-readable)"),
24
25
  },
25
26
  handler: async (args) => {
26
27
  try {
27
28
  const { client, address, signer } = await resolveClient();
28
- const raw = await setPositionTpSl(client, address, args);
29
+ // Map inputs
30
+ const mappedArgs = {
31
+ ...args,
32
+ direction: mapDirection(args.direction),
33
+ tpTriggerType: args.tpTriggerType !== undefined ? mapTriggerType(args.tpTriggerType) : undefined,
34
+ slTriggerType: args.slTriggerType !== undefined ? mapTriggerType(args.slTriggerType) : undefined,
35
+ };
36
+ const raw = await setPositionTpSl(client, address, mappedArgs);
29
37
  const data = await finalizeMutationResult(raw, signer, "set_tp_sl");
30
38
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
31
39
  }
@@ -8,12 +8,12 @@ export const updateOrderTpSlTool = {
8
8
  schema: {
9
9
  orderId: z.string().describe("The ID of the order to update"),
10
10
  marketId: z.string().describe("The market ID (config hash) for the order"),
11
- size: z.string().regex(/^\d+$/).describe("Order size raw units"),
12
- price: z.string().regex(/^\d+$/).describe("Order price raw units (30 decimals)"),
13
- tpPrice: z.string().regex(/^\d+$/).describe("TP price raw units (30 decimals)"),
14
- tpSize: z.string().regex(/^\d+$/).describe("TP size raw units"),
15
- slPrice: z.string().regex(/^\d+$/).describe("SL price raw units (30 decimals)"),
16
- slSize: z.string().regex(/^\d+$/).describe("SL size raw units"),
11
+ size: z.string().describe("Order size (raw or human-readable)"),
12
+ price: z.string().describe("Order price (raw or human-readable, 30 decimals)"),
13
+ tpPrice: z.string().describe("TP price (raw or human-readable, 30 decimals)"),
14
+ tpSize: z.string().describe("TP size (raw or human-readable)"),
15
+ slPrice: z.string().describe("SL price (raw or human-readable, 30 decimals)"),
16
+ slSize: z.string().describe("SL size (raw or human-readable)"),
17
17
  useOrderCollateral: z.boolean().describe("Whether to use order collateral"),
18
18
  isTpSlOrder: z.boolean().optional().describe("Whether this is a TP/SL order"),
19
19
  quoteToken: z.string().describe("Quote token address"),
@@ -1,3 +1,6 @@
1
+ /**
2
+ * MCP Standard Error Codes
3
+ */
1
4
  export var ErrorCode;
2
5
  (function (ErrorCode) {
3
6
  ErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
@@ -7,6 +10,9 @@ export var ErrorCode;
7
10
  ErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
8
11
  ErrorCode["BLOCKCHAIN_ERROR"] = "BLOCKCHAIN_ERROR";
9
12
  })(ErrorCode || (ErrorCode = {}));
13
+ /**
14
+ * MCP Standard Error Class
15
+ */
10
16
  export class MCPError extends Error {
11
17
  code;
12
18
  constructor(code, message) {
@@ -15,3 +21,236 @@ export class MCPError extends Error {
15
21
  this.name = "MCPError";
16
22
  }
17
23
  }
24
+ /**
25
+ * Common MYX Contract Error Selectors
26
+ */
27
+ export const CONTRACT_ERRORS = {
28
+ "fa52dfc0": "AccountInsufficientFreeAmount()",
29
+ "e2a1a260": "AccountInsufficientReservedAmount()",
30
+ "ffd10028": "AccountInsufficientTradableAmount(uint256,uint256)",
31
+ "9996b315": "AddressEmptyCode(address)",
32
+ "4c9c8ce3": "ERC1967InvalidImplementation(address)",
33
+ "b398979f": "ERC1967NonPayable()",
34
+ "d6bda275": "FailedCall()",
35
+ "f92ee8a9": "InvalidInitialization()",
36
+ "44d3438f": "NotAddressManager()",
37
+ "3fc81f20": "NotDependencyManager()",
38
+ "d7e6bcf8": "NotInitializing()",
39
+ "507f487a": "NotProxyAdmin()",
40
+ "e03f6024": "PermissionDenied(address,address)",
41
+ "5274afe7": "SafeERC20FailedOperation(address)",
42
+ "e07c8dba": "UUPSUnauthorizedCallContext()",
43
+ "aa1d49a4": "UUPSUnsupportedProxiableUUID(bytes32)",
44
+ "24775e06": "SafeCastOverflowedUintToInt(uint256)",
45
+ "ba767932": "ConvertAmountMismatch(uint256,uint256)",
46
+ "d93c0665": "EnforcedPause()",
47
+ "8dfc202b": "ExpectedPause()",
48
+ "059e2f49": "InRewindMode()",
49
+ "db42144d": "InsufficientBalance(address,uint256,uint256)",
50
+ "42301c23": "InsufficientOutputAmount()",
51
+ "caa99aac": "MismatchExecuteFee(uint256,uint256,uint256)",
52
+ "8637dfc0": "NotInRewindMode()",
53
+ "4578ddb8": "OnlyRelayer()",
54
+ "1e4fbdf7": "OwnableInvalidOwner(address)",
55
+ "118cdaa7": "OwnableUnauthorizedAccount(address)",
56
+ "3ee5aeb5": "ReentrancyGuardReentrantCall()",
57
+ "90b8ec18": "TransferFailed()",
58
+ "f4d678b8": "InsufficientBalance()",
59
+ "6aee3c1a": "InsufficientRiskReserves()",
60
+ "2c5211c6": "InvalidAmount()",
61
+ "82cb17ef": "InvalidSplitConfig()",
62
+ "f645eedf": "ECDSAInvalidSignature()",
63
+ "fce698f7": "ECDSAInvalidSignatureLength(uint256)",
64
+ "d78bce0c": "ECDSAInvalidSignatureS(bytes32)",
65
+ "48834bee": "ExpiredFeeData()",
66
+ "56d69198": "InvalidFeeRate()",
67
+ "613970e0": "InvalidParameter()",
68
+ "80577032": "NoRebateToClaim()",
69
+ "27d08510": "NotActiveBroker(address)",
70
+ "6e6b79b0": "NotBrokerSigner(address)",
71
+ "f6412b5a": "NotOrderOwner()",
72
+ "70d645e3": "NotPositionOwner()",
73
+ "ff70343d": "UnsupportedAssetClass(AssetClass)",
74
+ "185676be": "UnsupportedFeeTier(uint8)",
75
+ "60b25fe4": "BrokerAlreadyExists()",
76
+ "7eb4a674": "BrokerNotFound()",
77
+ "8c3b5bf0": "NotBrokerAdmin()",
78
+ "3733548a": "InvalidFeeTier()",
79
+ "192105d7": "InitializationFunctionReverted(address,bytes)",
80
+ "0dc149f0": "AlreadyInitialized()",
81
+ "664431a8": "NotAllowedTarget(address)",
82
+ "03357c6c": "ExceedsMaximumRelayFee()",
83
+ "0d10f63b": "InconsistentParamsLength()",
84
+ "38802743": "InsufficientFeeAllowance(address,uint256,uint256)",
85
+ "d95b4ad5": "InsufficientFeeBalance(address,uint256,uint256)",
86
+ "a3972305": "MismatchedSender(address)",
87
+ "c3b80e86": "RelayerRegistered(address)",
88
+ "ee0844a3": "RemoveRelayerFailed()",
89
+ "c583a8da": "IncorrectFee(uint256)",
90
+ "00bfc921": "InvalidPrice()",
91
+ "d96ce906": "PriceIdMismatch()",
92
+ "148cd0dd": "VerifyPriceFailed()",
93
+ "b12d13eb": "ETHTransferFailed()",
94
+ "a83325d4": "PoolOracleFeeCharged()",
95
+ "6b75f90d": "PoolOracleFeeNotCharged()",
96
+ "42a0e2a7": "PoolOracleFeeNotExisted()",
97
+ "8ee01e1c": "PoolOracleFeeNotSoldOut()",
98
+ "5e0a829b": "ETHTransferFailed(address,uint256)",
99
+ "4ba6536f": "GasLimitExceeded(address,uint256,uint256)",
100
+ "ca1aae4b": "GasLimitNotSet(address)",
101
+ "3728b83d": "InvalidAmount(uint256)",
102
+ "3484727e": "BaseFeeNotSoldOut()",
103
+ "0251bde4": "LPNotFullyMinted()",
104
+ "7decb035": "PoolDebtNotCleared()",
105
+ "1acb203e": "PositionNotEmpty()",
106
+ "2be7b24b": "UnexpectedPoolState()",
107
+ "7bd42a2e": "NotEmptyAddress()",
108
+ "6697b232": "AccessControlBadConfirmation()",
109
+ "e2517d3f": "AccessControlUnauthorizedAccount(address,bytes32)",
110
+ "7c9a1cf9": "AlreadyVoted()",
111
+ "796ea3a6": "BondNotReleased()",
112
+ "6511c20d": "BondZeroAmount()",
113
+ "f38e5973": "CaseAppealNotFinished()",
114
+ "1eaa4a59": "CaseDeadlineNotReached()",
115
+ "e6c67e3a": "CaseDeadlineReached()",
116
+ "0fc957b1": "CaseNotAccepted()",
117
+ "3ddb819d": "CaseNotExist(CaseId)",
118
+ "79eab18d": "CaseRespondentAppealed(CaseId,address)",
119
+ "a179f8c9": "ChainIdMismatch()",
120
+ "311c16d3": "DisputeNotAllowed()",
121
+ "752d88c0": "InvalidAccountNonce(address,uint256)",
122
+ "a710429d": "InvalidContractAddress()",
123
+ "8076dd8a": "InvalidFunctionSignature()",
124
+ "c37906a0": "InvalidPayloadLength(uint256,uint256)",
125
+ "dcdedda9": "InvalidPoolToken()",
126
+ "3471a3c2": "InvalidProfitAmount()",
127
+ "1d9617a0": "InvalidResponseVersion()",
128
+ "9284b197": "InvalidSourceChain()",
129
+ "b9021668": "NoChainResponse()",
130
+ "c546bca4": "NotCaseRespondent(CaseId,address)",
131
+ "84ae4a30": "NumberOfResponsesMismatch()",
132
+ "02164961": "RequestTypeMismatch()",
133
+ "4cf72652": "RiskCloseNotCompleted()",
134
+ "0819bdcd": "SignatureExpired()",
135
+ "c00ca938": "UnexpectedCaseState()",
136
+ "7935e939": "UnexpectedCaseType()",
137
+ "5e7bd6ec": "UnexpectedNumberOfResults()",
138
+ "51ee5853": "UnsupportedQueryType(uint8)",
139
+ "29ca666b": "UntrustfulVoting()",
140
+ "439cc0cd": "VerificationFailed()",
141
+ "714f5513": "VersionMismatch()",
142
+ "96b8e05b": "WrongQueryType(uint8,uint8)",
143
+ "bb6b170d": "ZeroQueries()",
144
+ "a9214540": "AlreadyClaimed(CaseId,address)",
145
+ "d4ac59c1": "InvalidAmount(CaseId,address)",
146
+ "7a6f5328": "MerkleTreeVerificationFailed(CaseId,address)",
147
+ "094a5cfe": "ReimbursementValidity(CaseId)",
148
+ "8b922563": "TreeAlreadySet()",
149
+ "7b27120a": "InDisputeMode()",
150
+ "5646203f": "InsufficientCollateral(PositionId,uint256)",
151
+ "b04111ef": "InsufficientFreeCollateral(PositionId,uint256)",
152
+ "12f1b11a": "InsufficientLockedCollateral(PositionId,uint256)",
153
+ "a8ce4432": "SafeCastOverflowedIntToUint(int256)",
154
+ "6dfcc650": "SafeCastOverflowedUintDowncast(uint8,uint256)",
155
+ "d24b47fb": "UserProfitFrozen()",
156
+ "1c151780": "ExceedMinOutput(uint256,uint256)",
157
+ "e1f0493d": "NotAllowedCaller(address)",
158
+ "fb8f41b2": "ERC20InsufficientAllowance(address,uint256,uint256)",
159
+ "e450d38c": "ERC20InsufficientBalance(address,uint256,uint256)",
160
+ "e602df05": "ERC20InvalidApprover(address)",
161
+ "ec442f05": "ERC20InvalidReceiver(address)",
162
+ "96c6fd1e": "ERC20InvalidSender(address)",
163
+ "94280d62": "ERC20InvalidSpender(address)",
164
+ "62791302": "ERC2612ExpiredSignature(uint256)",
165
+ "4b800e46": "ERC2612InvalidSigner(address,address)",
166
+ "b3512b0c": "InvalidShortString()",
167
+ "305a27a9": "StringTooLong(string)",
168
+ "fd0f789d": "ExceedMaxPriceDeviation()",
169
+ "407b87e5": "ExchangeRateAlreadyApplied()",
170
+ "5c6c5686": "ExchangeRateAlreadyDisabled()",
171
+ "1ae17fcd": "InvalidDeviationRatio()",
172
+ "37bc9350": "InvalidPriceTimestamp()",
173
+ "7a5c919f": "InvalidRewindPrice()",
174
+ "18b88897": "InvalidUpdateFee()",
175
+ "f76740e3": "PriceDeviationBelowMinimum()",
176
+ "49386283": "PriceDeviationThresholdReached()",
177
+ "7f84faec": "PublishTimeMismatch()",
178
+ "19abf40e": "StalePrice()",
179
+ "e351cd13": "ExceedMaxExchangeableAmount()",
180
+ "14be833f": "InsufficientReturnAmount(uint256,uint256)",
181
+ "15912a6f": "NotSupportVersion()",
182
+ "f1364a74": "ArrayEmpty()",
183
+ "15ed381d": "ExceedMaxProfit()",
184
+ "dc82bd68": "ExceedMinOutputAmount()",
185
+ "ba01b06f": "PoolNotActive(PoolId)",
186
+ "ba8f5df5": "PoolNotCompoundable(PoolId)",
187
+ "51aeee6c": "PoolNotExist(PoolId)",
188
+ "70f6c197": "InvalidQuoteTokenAddress()",
189
+ "0b8457f4": "InvalidRatioParams()",
190
+ "29dae146": "MarketAlreadyExisted()",
191
+ "f040b67a": "MarketNotExisted()",
192
+ "0e442a4a": "InvalidBaseToken()",
193
+ "24e219c7": "MarketNotExist(MarketId)",
194
+ "cc36f935": "PoolExists(PoolId)",
195
+ "e84c308d": "ExceedBaseReserved(uint256,uint256)",
196
+ "3e241751": "ExceedQuoteReserved(uint256,uint256)",
197
+ "de656889": "ExceedReservable(uint256,uint256,uint256)",
198
+ "d54d0fc4": "InsufficientLiquidity(uint256,uint256,uint256)",
199
+ "7e562a65": "InvalidDistributionAmount()",
200
+ "83c7580d": "ReservableNotEnough(uint256,uint256)",
201
+ "94eef58a": "ERC2771ForwarderExpiredRequest(uint48)",
202
+ "c845a056": "ERC2771ForwarderInvalidSigner(address,address)",
203
+ "70647f79": "ERC2771ForwarderMismatchedValue(uint256,uint256)",
204
+ "d2650cd1": "ERC2771UntrustfulTarget(address,address)",
205
+ "cf479181": "InsufficientBalance(uint256,uint256)",
206
+ "4c150d8f": "DifferentMarket(PoolId,PoolId)",
207
+ "aa98b06a": "InsufficientQuoteIn(uint256,uint256,uint256)",
208
+ "3e589bee": "InvalidLiquidityAmount()",
209
+ "aebd3617": "InvalidTpsl(uint256)",
210
+ "e079169e": "NotReachedPrice(OrderId,uint256,uint256,TriggerType)",
211
+ "7fe81129": "SamePoolMigration(PoolId)",
212
+ "71c4efed": "SlippageExceeded(uint256,uint256)",
213
+ "62b9bc7b": "DesignatedTokenMismatch(address,address)",
214
+ "49465eb0": "NotForwardAllowedTarget(address)",
215
+ "e921c36b": "AlreadyMigrated(PositionId,PositionId)",
216
+ "ddefae28": "AlreadyMinted()",
217
+ "b4762117": "ExceedMaxLeverage(PositionId)",
218
+ "97c7f537": "ExcessiveSlippage()",
219
+ "301b6707": "ExecutionFeeNotCollected()",
220
+ "1b5305a8": "InsufficientRedeemable()",
221
+ "c6e8248a": "InsufficientSize()",
222
+ "700deaad": "InvalidADLPosition(OrderId,PositionId)",
223
+ "f64fa6a8": "InvalidOrder(OrderId)",
224
+ "1dab59cf": "InvalidOrderPair(OrderId,OrderId)",
225
+ "8ea9158f": "InvalidPosition(PositionId)",
226
+ "d15b4fe2": "InvalidQuoteToken()",
227
+ "d8daec7c": "MarketNotInitialized()",
228
+ "419ecd12": "MatchNotSupported()",
229
+ "d4944235": "NoADLNeeded(OrderId)",
230
+ "cd4891b6": "NotInDisputeMode()",
231
+ "17229ec4": "NotMeetEarlyCloseCriteria(PositionId)",
232
+ "1ad308dc": "OrderExpired(OrderId)",
233
+ "e75316c6": "OrderNotExist(OrderId)",
234
+ "230e8e43": "PoolNotInPreBenchState(PoolId)",
235
+ "486aa307": "PoolNotInitialized()",
236
+ "a5afd143": "PositionNotHealthy(PositionId,uint256)",
237
+ "ba0d3752": "PositionNotInitialized(PositionId)",
238
+ "c53f84e7": "PositionRemainsHealthy(PositionId)",
239
+ "107dec14": "RiskCloseNotAllowed()",
240
+ "759b3876": "UnhealthyAfterRiskTierApplied(PositionId)",
241
+ };
242
+ /**
243
+ * Tries to decode an error data string into a human-readable name.
244
+ * @param errorData Hex string of the error data (e.g. "0xfa52dfc0...")
245
+ */
246
+ export function decodeErrorSelector(errorData) {
247
+ if (!errorData || typeof errorData !== "string")
248
+ return null;
249
+ // Clean prefix
250
+ let hex = errorData.toLowerCase();
251
+ if (hex.startsWith("0x"))
252
+ hex = hex.slice(2);
253
+ // Selector is first 4 bytes (8 hex chars)
254
+ const selector = hex.slice(0, 8);
255
+ return CONTRACT_ERRORS[selector] || null;
256
+ }
@@ -117,3 +117,48 @@ export const getCloseTypeDesc = (type) => {
117
117
  };
118
118
  return types[type] || `Unknown(${type})`;
119
119
  };
120
+ /**
121
+ * 映射输入方向为数值
122
+ */
123
+ export const mapDirection = (input) => {
124
+ if (input === 0 || input === 1)
125
+ return input;
126
+ const s = String(input ?? "").trim().toUpperCase();
127
+ if (s === "0" || s === "LONG" || s === "BUY")
128
+ return 0;
129
+ if (s === "1" || s === "SHORT" || s === "SELL")
130
+ return 1;
131
+ throw new Error(`Invalid direction: ${input}. Use 0/LONG or 1/SHORT.`);
132
+ };
133
+ /**
134
+ * 映射输入订单类型为数值
135
+ */
136
+ export const mapOrderType = (input) => {
137
+ if (input === 0 || input === 1 || input === 2 || input === 3)
138
+ return input;
139
+ const s = String(input ?? "").trim().toUpperCase();
140
+ if (s === "0" || s === "MARKET")
141
+ return 0;
142
+ if (s === "1" || s === "LIMIT")
143
+ return 1;
144
+ if (s === "2" || s === "STOP")
145
+ return 2;
146
+ if (s === "3" || s === "CONDITIONAL")
147
+ return 3;
148
+ throw new Error(`Invalid orderType: ${input}. Use MARKET, LIMIT, STOP or CONDITIONAL.`);
149
+ };
150
+ /**
151
+ * 映射输入触发业务类型为数值
152
+ */
153
+ export const mapTriggerType = (input) => {
154
+ if (input === 0 || input === 1 || input === 2)
155
+ return input;
156
+ const s = String(input ?? "").trim().toUpperCase();
157
+ if (s === "0" || s === "NONE")
158
+ return 0;
159
+ if (s === "1" || s === "GTE" || s === ">=")
160
+ return 1;
161
+ if (s === "2" || s === "LTE" || s === "<=")
162
+ return 2;
163
+ throw new Error(`Invalid triggerType: ${input}. Use 0/NONE, 1/GTE or 2/LTE.`);
164
+ };
@@ -1,3 +1,4 @@
1
+ import { decodeErrorSelector } from "./errors.js";
1
2
  const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
2
3
  const TX_HASH_KEYS = new Set(["hash", "txHash", "transactionHash"]);
3
4
  function isObject(value) {
@@ -36,7 +37,22 @@ function assertSdkCode(result, actionName) {
36
37
  throw new Error(`${actionName} failed: invalid SDK code.`);
37
38
  }
38
39
  if (code !== 0) {
39
- const msg = result.msg ?? result.message ?? "unknown error";
40
+ let msg = result.msg ?? result.message ?? "unknown error";
41
+ // 尝试解码可能存在的自定义错误 (通常在 data 或 msg 中)
42
+ const data = result.data;
43
+ if (typeof data === "string" && data.startsWith("0x")) {
44
+ const decoded = decodeErrorSelector(data);
45
+ if (decoded)
46
+ msg = `${msg} (Contract Error: ${decoded})`;
47
+ }
48
+ else if (typeof msg === "string" && msg.includes("0x")) {
49
+ const match = msg.match(/0x[0-9a-f]{8}/i);
50
+ if (match) {
51
+ const decoded = decodeErrorSelector(match[0]);
52
+ if (decoded)
53
+ msg = `${msg} (Decoded: ${decoded})`;
54
+ }
55
+ }
40
56
  throw new Error(`${actionName} failed: code=${code}, msg=${String(msg)}`);
41
57
  }
42
58
  }
@@ -1,5 +1,7 @@
1
1
  import { parseUnits } from "ethers";
2
2
  const DECIMAL_RE = /^-?\d+(\.\d+)?$/;
3
+ const RAW_PREFIX_RE = /^raw:/i;
4
+ const INTEGER_RE = /^-?\d+$/;
3
5
  function normalizeDecimal(input) {
4
6
  let value = input.trim();
5
7
  let sign = "";
@@ -21,6 +23,12 @@ export function ensureUnits(value, decimals, label = "value") {
21
23
  const str = String(value).trim();
22
24
  if (!str)
23
25
  throw new Error(`${label} is required.`);
26
+ if (RAW_PREFIX_RE.test(str)) {
27
+ const raw = str.replace(RAW_PREFIX_RE, "").trim();
28
+ if (!INTEGER_RE.test(raw))
29
+ throw new Error(`${label} must be an integer raw units string.`);
30
+ return raw;
31
+ }
24
32
  if (!DECIMAL_RE.test(str))
25
33
  throw new Error(`${label} must be a numeric string.`);
26
34
  // If it's already a very large integer (e.g. > 12 digits or > decimals digits),
@@ -36,6 +44,23 @@ export function ensureUnits(value, decimals, label = "value") {
36
44
  return str;
37
45
  }
38
46
  }
47
+ export function parseUserUnits(value, decimals, label = "value") {
48
+ const str = String(value).trim();
49
+ if (!str)
50
+ throw new Error(`${label} is required.`);
51
+ if (RAW_PREFIX_RE.test(str)) {
52
+ const raw = str.replace(RAW_PREFIX_RE, "").trim();
53
+ if (!INTEGER_RE.test(raw))
54
+ throw new Error(`${label} must be an integer raw units string.`);
55
+ return raw;
56
+ }
57
+ if (!DECIMAL_RE.test(str))
58
+ throw new Error(`${label} must be a numeric string.`);
59
+ return parseUnits(str, decimals).toString();
60
+ }
61
+ export function parseUserPrice30(value, label = "price") {
62
+ return parseUserUnits(value, 30, label);
63
+ }
39
64
  export function parseHumanUnits(value, decimals, label = "value") {
40
65
  const str = String(value).trim();
41
66
  if (!str)
@@ -1,12 +1,13 @@
1
1
  import { getChainId } from "../auth/resolveClient.js";
2
+ import { decodeErrorSelector } from "./errors.js";
2
3
  /**
3
- * 等待后端索引并验证交易结果
4
+ * 等待后端索引并验证交易结果 (增强版)
4
5
  */
5
6
  export async function verifyTradeOutcome(client, address, poolId, txHash) {
6
7
  const chainId = getChainId();
7
- // 给后端索引一定的缓冲时间
8
- const maxAttempts = 5;
9
- const intervalMs = 1000;
8
+ // 给后端索引一定的缓冲时间,采用指数退避
9
+ const maxAttempts = 6;
10
+ let currentDelay = 1000;
10
11
  let matchedOrder = null;
11
12
  for (let i = 0; i < maxAttempts; i++) {
12
13
  try {
@@ -19,13 +20,22 @@ export async function verifyTradeOutcome(client, address, poolId, txHash) {
19
20
  const history = historyRes?.data || historyRes?.data?.data || [];
20
21
  matchedOrder = history.find((o) => String(o.orderHash).toLowerCase() === txHash.toLowerCase() ||
21
22
  String(o.txHash).toLowerCase() === txHash.toLowerCase());
22
- if (matchedOrder)
23
- break;
23
+ // 如果找到了订单且已经有最终状态,则退出轮询
24
+ if (matchedOrder) {
25
+ const status = Number(matchedOrder.status);
26
+ if (status === 1 || status === 9 || status === 2) {
27
+ // 1: Cancelled, 9: Successful, 2: Expired
28
+ break;
29
+ }
30
+ }
24
31
  }
25
32
  catch (e) {
26
33
  console.warn(`[verifyTradeOutcome] Attempt ${i + 1} failed:`, e);
27
34
  }
28
- await new Promise(resolve => setTimeout(resolve, intervalMs));
35
+ if (i < maxAttempts - 1) {
36
+ await new Promise(resolve => setTimeout(resolve, currentDelay));
37
+ currentDelay *= 2; // 指数退避 (1s, 2s, 4s...)
38
+ }
29
39
  }
30
40
  // 查询当前持仓
31
41
  let positions = [];
@@ -36,9 +46,16 @@ export async function verifyTradeOutcome(client, address, poolId, txHash) {
36
46
  catch (e) {
37
47
  console.warn(`[verifyTradeOutcome] Failed to fetch positions:`, e);
38
48
  }
49
+ let cancelReason = matchedOrder?.cancelReason || (matchedOrder?.status === 1 ? "Unknown cancellation" : null);
50
+ if (cancelReason && cancelReason.startsWith("0x")) {
51
+ const decoded = decodeErrorSelector(cancelReason);
52
+ if (decoded)
53
+ cancelReason = `${cancelReason} (${decoded})`;
54
+ }
39
55
  return {
40
56
  order: matchedOrder,
41
57
  positions: positions,
42
- verified: !!matchedOrder
58
+ verified: !!matchedOrder,
59
+ cancelReason
43
60
  };
44
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "2.5.3",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.27.1",
20
- "@myx-trade/sdk": "^0.1.269",
20
+ "@myx-trade/sdk": "^0.1.270",
21
21
  "dotenv": "^17.3.1",
22
22
  "ethers": "^6.13.1",
23
23
  "zod": "^4.3.6"