@michaleffffff/mcp-trading-server 2.6.0 → 2.8.2
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/dist/prompts/index.js +1 -0
- package/dist/prompts/tradeAnalysisPrompt.js +11 -1
- package/dist/prompts/tradingGuide.js +43 -0
- package/dist/server.js +2 -2
- package/dist/services/marketService.js +60 -0
- package/dist/services/tradeService.js +26 -0
- package/dist/tools/accountInfo.js +12 -7
- package/dist/tools/executeTrade.js +18 -18
- package/dist/tools/openPositionSimple.js +16 -69
- package/dist/tools/setTpSl.js +21 -13
- package/dist/utils/errors.js +239 -0
- package/dist/utils/mappings.js +45 -0
- package/dist/utils/mutationResult.js +17 -1
- package/dist/utils/verification.js +25 -8
- package/package.json +1 -1
package/dist/prompts/index.js
CHANGED
|
@@ -20,7 +20,17 @@ export const tradeAnalysisPrompt = {
|
|
|
20
20
|
role: "user",
|
|
21
21
|
content: {
|
|
22
22
|
type: "text",
|
|
23
|
-
text: `
|
|
23
|
+
text: `Analyze this user's trading portfolio with professional rigor:
|
|
24
|
+
|
|
25
|
+
Positions: ${JSON.stringify(positions, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)}
|
|
26
|
+
|
|
27
|
+
Market Context: ${args?.marketContext || "None provided"}
|
|
28
|
+
|
|
29
|
+
## Analysis Requirements:
|
|
30
|
+
1. **Risk Level**: Calculate current margin health and distance to liquidation price.
|
|
31
|
+
2. **PnL Review**: Evaluate performance and identify if SL/TP are appropriately placed.
|
|
32
|
+
3. **Actionable Suggestions**: Suggest specific size adjustments or TP/SL updates based on context.
|
|
33
|
+
4. **Funding Outlook**: Brief comment on funding fee impacts if observable.`
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveClient } from "../auth/resolveClient.js";
|
|
2
|
+
export const tradingGuidePrompt = {
|
|
3
|
+
name: "trading_best_practices",
|
|
4
|
+
description: "Get the gold standard workflow and parameter advice for using this MCP trading server.",
|
|
5
|
+
arguments: [],
|
|
6
|
+
run: async () => {
|
|
7
|
+
const { address, chainId } = await resolveClient();
|
|
8
|
+
return {
|
|
9
|
+
messages: [
|
|
10
|
+
{
|
|
11
|
+
role: "assistant",
|
|
12
|
+
content: {
|
|
13
|
+
type: "text",
|
|
14
|
+
text: `
|
|
15
|
+
# MYX Trading MCP Best Practices (v2.8.0)
|
|
16
|
+
|
|
17
|
+
You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
|
|
18
|
+
|
|
19
|
+
## 1. The Standard Workflow
|
|
20
|
+
1. **Discovery**: Use \`search_market\` with a keyword (e.g., "BTC") to find the active \`poolId\`.
|
|
21
|
+
2. **Context**: Use \`get_market_price\` and \`get_account_info\` to check the current market state and your available margin.
|
|
22
|
+
3. **Execution**: Prefer \`open_position_simple\` for new trades. It handles unit conversions and pool resolution automatically.
|
|
23
|
+
4. **Validation**: Always check the \`verification.verified\` flag in the output. If \`false\`, read the \`cancelReason\` to explain the failure to the user.
|
|
24
|
+
|
|
25
|
+
## 2. Parameter Tips
|
|
26
|
+
- **Position IDs**: When opening a NEW position, \`positionId\` MUST be an empty string \`""\`.
|
|
27
|
+
- **Decimals**: Human-readable units (e.g., "0.1" BTC) are default for \`open_position_simple\`. SDK-native tools often require raw units; use the \`raw:\` prefix if you need forced precision.
|
|
28
|
+
- **Slippage**: Default is 100 (1%). For volatile meme tokens, consider 200-300 (2-3%).
|
|
29
|
+
- **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
|
|
30
|
+
|
|
31
|
+
## 3. Self-Healing
|
|
32
|
+
If a transaction reverts with a hex code, the server will attempt to decode it (e.g., "AccountInsufficientFreeAmount"). Inform the user specifically about what is missing rather than giving a generic error.
|
|
33
|
+
|
|
34
|
+
Current Session:
|
|
35
|
+
- Wallet: ${address}
|
|
36
|
+
- Chain ID: ${chainId}
|
|
37
|
+
`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
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.
|
|
84
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "2.8.2" }, { 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.
|
|
184
|
+
logger.info("🚀 MYX Trading MCP Server v2.8.2 running (stdio, pure on-chain, prod ready)");
|
|
185
185
|
}
|
|
186
186
|
main().catch((err) => {
|
|
187
187
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -79,3 +79,63 @@ export async function getPoolLevelConfig(client, poolId, chainIdOverride) {
|
|
|
79
79
|
const chainId = chainIdOverride ?? getChainId();
|
|
80
80
|
return client.markets.getPoolLevelConfig(poolId, chainId);
|
|
81
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,6 +60,32 @@ 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");
|
|
@@ -19,13 +19,18 @@ export const getAccountInfoTool = {
|
|
|
19
19
|
structuredData = {
|
|
20
20
|
...result,
|
|
21
21
|
data: {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
freeMargin: d[0],
|
|
23
|
+
walletBalance: d[1],
|
|
24
|
+
freeBaseAmount: d[2],
|
|
25
|
+
baseProfit: d[3],
|
|
26
|
+
quoteProfit: d[4],
|
|
27
|
+
reservedAmount: d[5],
|
|
28
|
+
releaseTime: d[6],
|
|
29
|
+
// Business Logic:
|
|
30
|
+
// tradeableMargin = freeMargin + walletBalance + (quoteProfit if releaseTime == 0)
|
|
31
|
+
tradeableMargin: (BigInt(d[0]) + BigInt(d[1]) + (BigInt(d[6]) === 0n ? BigInt(d[4]) : 0n)).toString(),
|
|
32
|
+
baseProfitStatus: "base token to be unlocked",
|
|
33
|
+
quoteProfitStatus: BigInt(d[6]) > 0n ? "quote token to be unlocked" : "quote token unlocked/available"
|
|
29
34
|
}
|
|
30
35
|
};
|
|
31
36
|
}
|
|
@@ -9,29 +9,29 @@ export const executeTradeTool = {
|
|
|
9
9
|
name: "execute_trade",
|
|
10
10
|
description: "Create an increase order using SDK-native parameters.",
|
|
11
11
|
schema: {
|
|
12
|
-
poolId: z.string().describe("Pool ID"),
|
|
12
|
+
poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via get_pool_list."),
|
|
13
13
|
positionId: z.string().refine((value) => POSITION_ID_RE.test(value), {
|
|
14
14
|
message: "positionId must be empty string for new position, or a bytes32 hex string.",
|
|
15
|
-
}).describe("Position ID: empty string for
|
|
16
|
-
orderType: z.coerce.number().int().min(0).max(3).describe("
|
|
17
|
-
triggerType: z.coerce.number().int().min(0).max(2).describe("
|
|
15
|
+
}).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones."),
|
|
16
|
+
orderType: z.coerce.number().int().min(0).max(3).describe("0=Market, 1=Limit, 2=Stop, 3=StopLimit"),
|
|
17
|
+
triggerType: z.coerce.number().int().min(0).max(2).describe("0=None (Market), 1=Price, 2=Trailing"),
|
|
18
18
|
direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
|
|
19
|
-
collateralAmount: z.coerce.string().describe("Collateral
|
|
20
|
-
size: z.coerce.string().describe("
|
|
21
|
-
price: z.coerce.string().describe("
|
|
22
|
-
timeInForce: z.coerce.number().int().describe("
|
|
23
|
-
postOnly: z.coerce.boolean().describe("
|
|
19
|
+
collateralAmount: z.coerce.string().describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
|
|
20
|
+
size: z.coerce.string().describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
|
|
21
|
+
price: z.coerce.string().describe("Execution or Limit price. e.g. '65000' or 'raw:65000000000000000000000000000000000' (30 decimals)."),
|
|
22
|
+
timeInForce: z.coerce.number().int().describe("0=GTC, 1=IOC, 2=FOK"),
|
|
23
|
+
postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
|
|
24
24
|
slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
|
|
25
25
|
message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
|
|
26
|
-
}).describe(SLIPPAGE_PCT_4DP_DESC),
|
|
27
|
-
executionFeeToken: z.string().describe("
|
|
28
|
-
leverage: z.coerce.number().describe("Leverage"),
|
|
29
|
-
tpSize: z.coerce.string().optional().describe("
|
|
30
|
-
tpPrice: z.coerce.string().optional().describe("
|
|
31
|
-
slSize: z.coerce.string().optional().describe("
|
|
32
|
-
slPrice: z.coerce.string().optional().describe("
|
|
33
|
-
tradingFee: z.coerce.string().describe("
|
|
34
|
-
marketId: z.string().describe("Market
|
|
26
|
+
}).describe(`${SLIPPAGE_PCT_4DP_DESC}. Standard is 100 (1%).`),
|
|
27
|
+
executionFeeToken: z.string().describe("Address of token to pay gas/execution fees (typically USDC)."),
|
|
28
|
+
leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10 for 10x."),
|
|
29
|
+
tpSize: z.coerce.string().optional().describe("Take Profit size. Use '0' to disable."),
|
|
30
|
+
tpPrice: z.coerce.string().optional().describe("Take Profit trigger price."),
|
|
31
|
+
slSize: z.coerce.string().optional().describe("Stop Loss size. Use '0' to disable."),
|
|
32
|
+
slPrice: z.coerce.string().optional().describe("Stop Loss trigger price."),
|
|
33
|
+
tradingFee: z.coerce.string().describe("Estimated fee in raw units. Fetch via get_user_trading_fee_rate."),
|
|
34
|
+
marketId: z.string().describe("Specific Market Config Hash. Fetch via get_market_list."),
|
|
35
35
|
},
|
|
36
36
|
handler: async (args) => {
|
|
37
37
|
try {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { OrderType } from "@myx-trade/sdk";
|
|
3
3
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolvePool } from "../services/marketService.js";
|
|
5
5
|
import { openPosition } from "../services/tradeService.js";
|
|
6
6
|
import { normalizeAddress } from "../utils/address.js";
|
|
7
7
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
8
|
+
import { mapDirection, mapOrderType } from "../utils/mappings.js";
|
|
8
9
|
import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
9
10
|
import { parseUserPrice30, parseUserUnits } from "../utils/units.js";
|
|
10
11
|
import { verifyTradeOutcome } from "../utils/verification.js";
|
|
@@ -32,51 +33,26 @@ function pickMarketDetail(res) {
|
|
|
32
33
|
return res;
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
|
-
function collectPoolRows(input) {
|
|
36
|
-
if (Array.isArray(input))
|
|
37
|
-
return input.flatMap(collectPoolRows);
|
|
38
|
-
if (!input || typeof input !== "object")
|
|
39
|
-
return [];
|
|
40
|
-
if (input.poolId || input.pool_id)
|
|
41
|
-
return [input];
|
|
42
|
-
return Object.values(input).flatMap(collectPoolRows);
|
|
43
|
-
}
|
|
44
|
-
function resolveOrderType(orderType) {
|
|
45
|
-
const raw = String(orderType ?? "MARKET").trim().toUpperCase();
|
|
46
|
-
if (raw === "MARKET")
|
|
47
|
-
return OrderType.MARKET;
|
|
48
|
-
if (raw === "LIMIT")
|
|
49
|
-
return OrderType.LIMIT;
|
|
50
|
-
if (raw === "STOP")
|
|
51
|
-
return OrderType.STOP;
|
|
52
|
-
throw new Error(`orderType must be one of: MARKET, LIMIT, STOP.`);
|
|
53
|
-
}
|
|
54
36
|
export const openPositionSimpleTool = {
|
|
55
37
|
name: "open_position_simple",
|
|
56
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.",
|
|
57
39
|
schema: {
|
|
58
|
-
poolId: z.string().optional().describe("Pool ID. Provide either poolId or keyword."),
|
|
59
|
-
keyword: z.string().optional().describe('Market keyword, e.g. "BTC"
|
|
60
|
-
direction: z.
|
|
61
|
-
.number()
|
|
62
|
-
.pipe(z.union([z.literal(0), z.literal(1)]))
|
|
63
|
-
.describe("0 = LONG, 1 = SHORT"),
|
|
40
|
+
poolId: z.string().optional().describe("Hex Pool ID. Provide either poolId or keyword."),
|
|
41
|
+
keyword: z.string().optional().describe('Recommended: Market keyword, e.g. "BTC", "ETH", "XRP".'),
|
|
42
|
+
direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
|
|
64
43
|
collateralAmount: z.coerce
|
|
65
44
|
.string()
|
|
66
|
-
.describe("Collateral
|
|
45
|
+
.describe("Collateral. e.g. '100' (quoted in USDC) or 'raw:100000000'."),
|
|
67
46
|
leverage: z.coerce.number().int().positive().describe("Leverage (integer, e.g. 5, 10)."),
|
|
68
|
-
orderType: z
|
|
69
|
-
.enum(["MARKET", "LIMIT", "STOP"])
|
|
70
|
-
.optional()
|
|
71
|
-
.describe("Order type (default MARKET)."),
|
|
47
|
+
orderType: z.union([z.string(), z.number()]).optional().describe("MARKET, LIMIT, STOP (default MARKET). Strings allowed."),
|
|
72
48
|
price: z.coerce
|
|
73
49
|
.string()
|
|
74
50
|
.optional()
|
|
75
|
-
.describe("Price
|
|
51
|
+
.describe("Price. e.g. '62000' or 'raw:...' (30 dec). Required for LIMIT/STOP."),
|
|
76
52
|
size: z.coerce
|
|
77
53
|
.string()
|
|
78
54
|
.optional()
|
|
79
|
-
.describe("Position size
|
|
55
|
+
.describe("Position size. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
|
|
80
56
|
slippagePct: z.coerce
|
|
81
57
|
.string()
|
|
82
58
|
.optional()
|
|
@@ -88,13 +64,13 @@ export const openPositionSimpleTool = {
|
|
|
88
64
|
tradingFee: z.coerce
|
|
89
65
|
.string()
|
|
90
66
|
.optional()
|
|
91
|
-
.describe("Trading fee
|
|
92
|
-
autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve
|
|
93
|
-
approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false
|
|
67
|
+
.describe("Trading fee. e.g. '0.2' USDC or 'raw:...'. Default: computed via getUserTradingFeeRate."),
|
|
68
|
+
autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token spend (default false)."),
|
|
69
|
+
approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false)."),
|
|
94
70
|
autoDeposit: z.coerce
|
|
95
71
|
.boolean()
|
|
96
72
|
.optional()
|
|
97
|
-
.describe("If true, auto-deposit to margin account
|
|
73
|
+
.describe("If true, auto-deposit to margin account if marginBalance < needed (default false)."),
|
|
98
74
|
dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
|
|
99
75
|
},
|
|
100
76
|
handler: async (args) => {
|
|
@@ -102,34 +78,7 @@ export const openPositionSimpleTool = {
|
|
|
102
78
|
const { client, address, signer } = await resolveClient();
|
|
103
79
|
const chainId = getChainId();
|
|
104
80
|
// 1) Resolve poolId (poolId or keyword)
|
|
105
|
-
|
|
106
|
-
if (!poolId) {
|
|
107
|
-
const keyword = String(args.keyword ?? "").trim();
|
|
108
|
-
if (!keyword)
|
|
109
|
-
throw new Error("Either poolId or keyword is required.");
|
|
110
|
-
const markets = await searchMarket(client, keyword, 20);
|
|
111
|
-
if (markets.length) {
|
|
112
|
-
poolId = String(markets[0].poolId);
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
// Fallback: some environments return null for markets.searchMarket; use api.getPoolList instead.
|
|
116
|
-
const poolListRes = await client.api.getPoolList().catch(() => null);
|
|
117
|
-
const rows = collectPoolRows(poolListRes?.data ?? poolListRes);
|
|
118
|
-
const kw = keyword.toUpperCase();
|
|
119
|
-
const match = rows.find((row) => {
|
|
120
|
-
if (Number(row?.state) !== 2)
|
|
121
|
-
return false;
|
|
122
|
-
const base = String(row?.baseSymbol ?? "").toUpperCase();
|
|
123
|
-
const fallbackPair = row?.baseSymbol && row?.quoteSymbol ? `${row.baseSymbol}/${row.quoteSymbol}` : "";
|
|
124
|
-
const pair = String(row?.baseQuoteSymbol ?? fallbackPair).toUpperCase();
|
|
125
|
-
return base === kw || pair.includes(kw);
|
|
126
|
-
});
|
|
127
|
-
if (!match?.poolId && !match?.pool_id) {
|
|
128
|
-
throw new Error(`No active market found for keyword: ${keyword}`);
|
|
129
|
-
}
|
|
130
|
-
poolId = String(match.poolId ?? match.pool_id);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
81
|
+
const poolId = await resolvePool(client, args.poolId, args.keyword);
|
|
133
82
|
// 2) Fetch market detail for decimals, quote token, marketId
|
|
134
83
|
const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
|
|
135
84
|
const detail = pickMarketDetail(detailRes);
|
|
@@ -154,9 +103,7 @@ export const openPositionSimpleTool = {
|
|
|
154
103
|
}
|
|
155
104
|
const defaultAssetClass = Number(poolLevelConfig?.levelConfig?.assetClass ?? 0);
|
|
156
105
|
// 3) Parse & validate primary inputs
|
|
157
|
-
const dir =
|
|
158
|
-
if (dir !== 0 && dir !== 1)
|
|
159
|
-
throw new Error("direction must be 0 (LONG) or 1 (SHORT).");
|
|
106
|
+
const dir = mapDirection(args.direction);
|
|
160
107
|
const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
161
108
|
const collateralRawBig = asBigint(collateralRaw, "collateralAmount");
|
|
162
109
|
if (collateralRawBig <= 0n)
|
|
@@ -169,7 +116,7 @@ export const openPositionSimpleTool = {
|
|
|
169
116
|
throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
|
|
170
117
|
}
|
|
171
118
|
}
|
|
172
|
-
const orderType =
|
|
119
|
+
const orderType = mapOrderType(args.orderType ?? 0);
|
|
173
120
|
const postOnly = Boolean(args.postOnly ?? false);
|
|
174
121
|
const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "100");
|
|
175
122
|
const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
|
package/dist/tools/setTpSl.js
CHANGED
|
@@ -2,30 +2,38 @@ 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",
|
|
8
9
|
description: "Create TP/SL order using SDK-native parameters.",
|
|
9
10
|
schema: {
|
|
10
|
-
poolId: z.string().describe("Pool ID"),
|
|
11
|
-
positionId: z.string().describe("Position ID"),
|
|
12
|
-
direction: z.
|
|
13
|
-
leverage: z.coerce.number().describe("Leverage"),
|
|
14
|
-
executionFeeToken: z.string().describe("
|
|
15
|
-
tpTriggerType: z.
|
|
16
|
-
slTriggerType: z.
|
|
11
|
+
poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via get_pool_list."),
|
|
12
|
+
positionId: z.string().describe("Active Position ID. Get via get_positions."),
|
|
13
|
+
direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
|
|
14
|
+
leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10."),
|
|
15
|
+
executionFeeToken: z.string().describe("Address of token to pay gas/execution fees."),
|
|
16
|
+
tpTriggerType: z.union([z.number(), z.string()]).optional().describe("0=None, 1=Price (Market), 2=Trailing."),
|
|
17
|
+
slTriggerType: z.union([z.number(), z.string()]).optional().describe("0=None, 1=Price (Market), 2=Trailing."),
|
|
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
|
-
}).describe(SLIPPAGE_PCT_4DP_DESC),
|
|
20
|
-
tpPrice: z.coerce.string().optional().describe("
|
|
21
|
-
tpSize: z.coerce.string().optional().describe("TP size
|
|
22
|
-
slPrice: z.coerce.string().optional().describe("
|
|
23
|
-
slSize: z.coerce.string().optional().describe("SL size
|
|
20
|
+
}).describe(`${SLIPPAGE_PCT_4DP_DESC}. Standard is 100 (1%).`),
|
|
21
|
+
tpPrice: z.coerce.string().optional().describe("Take Profit trigger price. e.g. '2.5' or 'raw:...' (30 decimals)."),
|
|
22
|
+
tpSize: z.coerce.string().optional().describe("TP size in base tokens. Use '0' to disable."),
|
|
23
|
+
slPrice: z.coerce.string().optional().describe("Stop Loss trigger price. e.g. '2.1' or 'raw:...' (30 decimals)."),
|
|
24
|
+
slSize: z.coerce.string().optional().describe("SL size in base tokens. Use '0' to disable."),
|
|
24
25
|
},
|
|
25
26
|
handler: async (args) => {
|
|
26
27
|
try {
|
|
27
28
|
const { client, address, signer } = await resolveClient();
|
|
28
|
-
|
|
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
|
}
|
package/dist/utils/errors.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/utils/mappings.js
CHANGED
|
@@ -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
|
-
|
|
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,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 =
|
|
9
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
}
|