@michaleffffff/mcp-trading-server 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +8 -5
- package/TOOL_EXAMPLES.md +58 -1
- package/dist/prompts/tradingGuide.js +3 -0
- package/dist/server.js +6 -15
- package/dist/services/poolService.js +76 -2
- package/dist/services/tradeService.js +116 -11
- package/dist/tools/adjustMargin.js +1 -1
- package/dist/tools/cancelAllOrders.js +35 -3
- package/dist/tools/manageLiquidity.js +6 -2
- package/dist/tools/marketInfo.js +5 -4
- package/dist/tools/updateOrderTpSl.js +11 -9
- package/dist/utils/errorMessage.js +69 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.1 - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `extractErrorMessage` utility for cleaner error reporting across all tools.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- `manage_liquidity` now performs strict SDK code validation before finalizing transactions.
|
|
10
|
+
- `cancel_all_orders` improved to handle comma-separated and JSON-array strings for `orderIds`.
|
|
11
|
+
- Integrated meaningful error extraction into `marketInfo`, `manageLiquidity`, and `updateOrderTpSl`.
|
|
12
|
+
|
|
3
13
|
## 3.0.0 - 2026-03-17
|
|
4
14
|
|
|
5
15
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -161,6 +161,7 @@ The server will run using `stdio` transport and can be connected by any MCP-comp
|
|
|
161
161
|
- **Price**: `"price"` is human by default and will be converted to **30-decimal** format internally. Use `raw:` to pass a 30-decimal integer directly.
|
|
162
162
|
- **Slippage**: `slippagePct` uses 4-decimal raw units: `100` = `1%`, `50` = `0.5%`, `1000` = `10%`.
|
|
163
163
|
- **Note**: Some account-transfer tools (e.g. `account_deposit`, `account_withdraw`, `check_approval`) use **raw integer strings**.
|
|
164
|
+
- **`adjust_margin` units**: human amount is supported (e.g. `"1"` means 1 quote token). For exact atomic amount, use `raw:` (e.g. `"raw:1"`).
|
|
164
165
|
|
|
165
166
|
## P0 Reliability Contract (AI-friendly)
|
|
166
167
|
|
|
@@ -294,8 +295,9 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
|
|
|
294
295
|
* **close_position**: Close an open position.
|
|
295
296
|
* **close_all_positions**: Emergency: close ALL open positions in a pool at once.
|
|
296
297
|
* **cancel_order**: Cancel an open order by its order ID.
|
|
297
|
-
* **cancel_all_orders**: Cancel
|
|
298
|
+
* **cancel_all_orders**: Cancel open orders by IDs (supports array, JSON-array string, comma string, or single ID; use `get_open_orders` first).
|
|
298
299
|
* **set_tp_sl**: Set take profit and stop loss prices for an open position.
|
|
300
|
+
* **update_order_tp_sl**: Update TP/SL fields for an existing order. Accepts boolean-like values for `useOrderCollateral` / `isTpSlOrder`.
|
|
299
301
|
* **adjust_margin**: Adjust the margin (collateral) of an open position.
|
|
300
302
|
* **get_user_trading_fee_rate**: Query current maker/taker fee rates by asset class and risk tier.
|
|
301
303
|
* **get_network_fee**: Query estimated network fee requirements for a market.
|
|
@@ -306,7 +308,7 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
|
|
|
306
308
|
* **get_oracle_price**: Get the current EVM oracle price for a specific pool.
|
|
307
309
|
* **get_kline_latest_bar**: Get only the latest bar for an interval.
|
|
308
310
|
* **get_all_tickers**: Get all ticker snapshots in one request.
|
|
309
|
-
* **get_market_list**, **get_market_list_raw**, **get_kline**, **get_pool_info**...
|
|
311
|
+
* **get_market_list**, **get_market_list_raw**, **get_kline**, **get_pool_info**... (`get_pool_info` auto-retries with oracle/ticker price on divide-by-zero reads)
|
|
310
312
|
* **get_pool_symbol_all**, **get_base_detail**, **get_pool_level_config**...
|
|
311
313
|
|
|
312
314
|
### Account & Portfolio
|
|
@@ -321,7 +323,7 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
|
|
|
321
323
|
- If one section fails (wallet or trading-account), the tool may return a **partial** snapshot with `meta.partial=true` and section-level `error` details.
|
|
322
324
|
|
|
323
325
|
### Liquidity Provision (LP)
|
|
324
|
-
* **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool.
|
|
326
|
+
* **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool (aliases: `add/remove/increase/decrease` are supported).
|
|
325
327
|
* **create_perp_market**: Create a new perpetual trading pair.
|
|
326
328
|
|
|
327
329
|
---
|
|
@@ -371,8 +373,9 @@ Use this query/mutation loop for limit-order management:
|
|
|
371
373
|
|
|
372
374
|
1. Place order with `execute_trade` (`orderType=LIMIT`).
|
|
373
375
|
2. Query active orders with `get_open_orders`.
|
|
374
|
-
3.
|
|
375
|
-
4.
|
|
376
|
+
3. If still pending, you can cancel/update order-level fields; if filled into a position, prefer `set_tp_sl` for position TP/SL.
|
|
377
|
+
4. Check historical fills/cancellations with `get_order_history`.
|
|
378
|
+
5. Cancel one order via `cancel_order` or batch via `cancel_all_orders`.
|
|
376
379
|
|
|
377
380
|
Minimal query examples:
|
|
378
381
|
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -140,6 +140,15 @@ It includes:
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
```
|
|
143
|
+
Single-ID string is also accepted:
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"name": "cancel_all_orders",
|
|
147
|
+
"arguments": {
|
|
148
|
+
"orderIds": "0xORDER_ID_1"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
143
152
|
|
|
144
153
|
### `set_tp_sl`
|
|
145
154
|
```json
|
|
@@ -180,6 +189,28 @@ It includes:
|
|
|
180
189
|
}
|
|
181
190
|
}
|
|
182
191
|
```
|
|
192
|
+
String booleans are also accepted:
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"name": "update_order_tp_sl",
|
|
196
|
+
"arguments": {
|
|
197
|
+
"orderId": "0xORDER_ID",
|
|
198
|
+
"marketId": "0xMARKET_ID",
|
|
199
|
+
"size": 0.01,
|
|
200
|
+
"price": 65000,
|
|
201
|
+
"tpPrice": 70000,
|
|
202
|
+
"tpSize": 0.005,
|
|
203
|
+
"slPrice": 60000,
|
|
204
|
+
"slSize": 0.005,
|
|
205
|
+
"useOrderCollateral": "true",
|
|
206
|
+
"isTpSlOrder": "true",
|
|
207
|
+
"quoteToken": "0xQUOTE_TOKEN"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
Note:
|
|
212
|
+
- This tool is most reliable for existing TP/SL orders or open positions.
|
|
213
|
+
- If an unfilled LIMIT/STOP order returns `Failed to update order`, wait for fill and use `set_tp_sl`.
|
|
183
214
|
|
|
184
215
|
### `adjust_margin`
|
|
185
216
|
```json
|
|
@@ -188,7 +219,18 @@ It includes:
|
|
|
188
219
|
"arguments": {
|
|
189
220
|
"poolId": "0xPOOL_ID",
|
|
190
221
|
"positionId": "0xPOSITION_ID",
|
|
191
|
-
"adjustAmount": "
|
|
222
|
+
"adjustAmount": "1"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
Raw precision mode:
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"name": "adjust_margin",
|
|
230
|
+
"arguments": {
|
|
231
|
+
"poolId": "0xPOOL_ID",
|
|
232
|
+
"positionId": "0xPOSITION_ID",
|
|
233
|
+
"adjustAmount": "raw:1000000"
|
|
192
234
|
}
|
|
193
235
|
}
|
|
194
236
|
```
|
|
@@ -295,6 +337,7 @@ Note: this tool may internally fallback to SDK `api.getMarketList()` / `api.getP
|
|
|
295
337
|
}
|
|
296
338
|
}
|
|
297
339
|
```
|
|
340
|
+
Note: the server auto-retries with oracle/ticker market price when direct on-chain read hits divide-by-zero.
|
|
298
341
|
|
|
299
342
|
### `get_liquidity_info`
|
|
300
343
|
```json
|
|
@@ -502,6 +545,20 @@ If one section fails, check `data.meta.partial` and section-level `error` fields
|
|
|
502
545
|
}
|
|
503
546
|
}
|
|
504
547
|
```
|
|
548
|
+
Alias action example (`remove` == `withdraw`):
|
|
549
|
+
```json
|
|
550
|
+
{
|
|
551
|
+
"name": "manage_liquidity",
|
|
552
|
+
"arguments": {
|
|
553
|
+
"action": "remove",
|
|
554
|
+
"poolType": "BASE",
|
|
555
|
+
"poolId": "0xPOOL_ID",
|
|
556
|
+
"amount": 1.5,
|
|
557
|
+
"slippage": 0.01
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
If operation fails, check `error.message` for concrete reasons (for example `Insufficient Balance`) instead of generic `undefined`.
|
|
505
562
|
|
|
506
563
|
### `get_lp_price`
|
|
507
564
|
```json
|
|
@@ -29,6 +29,9 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
29
29
|
- **Slippage**: Default is 100 (1%). For volatile meme tokens, consider 200-300 (2-3%).
|
|
30
30
|
- **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
|
|
31
31
|
- **Balances**: Use \`get_account\` to clearly separate wallet balance vs trading-account balance (pass \`poolId\` for trading-account metrics).
|
|
32
|
+
- **Adjust Margin**: \`adjust_margin.adjustAmount\` supports human amount (e.g. "1"), and also supports exact raw amount via \`raw:\` prefix.
|
|
33
|
+
- **TP/SL Updates**: \`update_order_tp_sl\` accepts boolean-like values for \`useOrderCollateral\` and \`isTpSlOrder\`, but pending LIMIT/STOP orders can still be rejected by protocol rules; after fill, prefer \`set_tp_sl\`.
|
|
34
|
+
- **Pool Reads**: \`get_pool_info\` auto-retries with oracle/ticker price when direct on-chain read returns divide-by-zero.
|
|
32
35
|
- **Examples**: Follow \`TOOL_EXAMPLES.md\` payload patterns when building tool arguments.
|
|
33
36
|
|
|
34
37
|
## 3. Self-Healing
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as baseResources from "./resources/index.js";
|
|
|
11
11
|
import * as basePrompts from "./prompts/index.js";
|
|
12
12
|
import { logger } from "./utils/logger.js";
|
|
13
13
|
import { MCPError, ErrorCode } from "./utils/errors.js";
|
|
14
|
+
import { extractErrorMessage, isMeaningfulErrorMessage } from "./utils/errorMessage.js";
|
|
14
15
|
// --- Process Logic Protection ---
|
|
15
16
|
// Catch unhandled promise rejections
|
|
16
17
|
process.on('unhandledRejection', (reason, promise) => {
|
|
@@ -36,16 +37,6 @@ const allPrompts = Object.values(basePrompts);
|
|
|
36
37
|
function safeJsonStringify(value) {
|
|
37
38
|
return JSON.stringify(value, (_, item) => (typeof item === "bigint" ? item.toString() : item), 2);
|
|
38
39
|
}
|
|
39
|
-
function extractErrorMessage(raw) {
|
|
40
|
-
if (raw instanceof Error)
|
|
41
|
-
return raw.message;
|
|
42
|
-
if (typeof raw === "string")
|
|
43
|
-
return raw;
|
|
44
|
-
if (raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string") {
|
|
45
|
-
return String(raw.message);
|
|
46
|
-
}
|
|
47
|
-
return String(raw ?? "Unknown error");
|
|
48
|
-
}
|
|
49
40
|
function inferToolErrorCode(message) {
|
|
50
41
|
const lower = message.toLowerCase();
|
|
51
42
|
if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized")) {
|
|
@@ -134,14 +125,14 @@ function normalizeToolErrorResult(toolName, result) {
|
|
|
134
125
|
parsed.status === "error" &&
|
|
135
126
|
parsed.error &&
|
|
136
127
|
typeof parsed.error.code === "string" &&
|
|
137
|
-
|
|
128
|
+
isMeaningfulErrorMessage(parsed.error.message)) {
|
|
138
129
|
return result;
|
|
139
130
|
}
|
|
140
|
-
const plainMessage = typeof parsed?.error?.message === "string"
|
|
131
|
+
const plainMessage = extractErrorMessage(typeof parsed?.error?.message === "string"
|
|
141
132
|
? parsed.error.message
|
|
142
133
|
: typeof parsed?.message === "string"
|
|
143
134
|
? parsed.message
|
|
144
|
-
: rawText.replace(/^Error:\s*/i, "").trim();
|
|
135
|
+
: rawText.replace(/^Error:\s*/i, "").trim(), `Tool "${toolName}" failed.`);
|
|
145
136
|
const code = typeof parsed?.error?.code === "string"
|
|
146
137
|
? parsed.error.code
|
|
147
138
|
: inferToolErrorCode(plainMessage);
|
|
@@ -261,7 +252,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
261
252
|
};
|
|
262
253
|
}
|
|
263
254
|
// ─── MCP Server ───
|
|
264
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.
|
|
255
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
265
256
|
// List tools
|
|
266
257
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
267
258
|
return {
|
|
@@ -381,7 +372,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
381
372
|
async function main() {
|
|
382
373
|
const transport = new StdioServerTransport();
|
|
383
374
|
await server.connect(transport);
|
|
384
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
375
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.1 running (stdio, pure on-chain, prod ready)");
|
|
385
376
|
}
|
|
386
377
|
main().catch((err) => {
|
|
387
378
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { pool, quote, base } from "@myx-trade/sdk";
|
|
2
2
|
import { getChainId, resolveClient } from "../auth/resolveClient.js";
|
|
3
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
4
|
+
import { ensureUnits } from "../utils/units.js";
|
|
5
|
+
function isDivideByZeroError(message) {
|
|
6
|
+
const lower = message.toLowerCase();
|
|
7
|
+
return (lower.includes("divide_by_zero") ||
|
|
8
|
+
lower.includes("division by zero") ||
|
|
9
|
+
lower.includes("panic code 0x12") ||
|
|
10
|
+
lower.includes("panic code: 0x12") ||
|
|
11
|
+
lower.includes("0x4e487b71"));
|
|
12
|
+
}
|
|
13
|
+
function toPositiveBigint(input) {
|
|
14
|
+
try {
|
|
15
|
+
const value = BigInt(String(input ?? "").trim());
|
|
16
|
+
return value > 0n ? value : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
3
22
|
/**
|
|
4
23
|
* 创建合约市场池子
|
|
5
24
|
*/
|
|
@@ -11,9 +30,64 @@ export async function createPool(baseToken, marketId) {
|
|
|
11
30
|
/**
|
|
12
31
|
* 获取池子信息
|
|
13
32
|
*/
|
|
14
|
-
export async function getPoolInfo(poolId, chainIdOverride) {
|
|
33
|
+
export async function getPoolInfo(poolId, chainIdOverride, clientOverride) {
|
|
15
34
|
const chainId = chainIdOverride ?? getChainId();
|
|
16
|
-
|
|
35
|
+
let needOracleFallback = false;
|
|
36
|
+
try {
|
|
37
|
+
const direct = await pool.getPoolInfo(chainId, poolId);
|
|
38
|
+
if (direct)
|
|
39
|
+
return direct;
|
|
40
|
+
needOracleFallback = true;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = extractErrorMessage(error);
|
|
44
|
+
if (!isDivideByZeroError(message)) {
|
|
45
|
+
throw new Error(`get_pool_info failed: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
needOracleFallback = true;
|
|
48
|
+
}
|
|
49
|
+
if (!needOracleFallback)
|
|
50
|
+
return undefined;
|
|
51
|
+
const client = clientOverride ?? (await resolveClient()).client;
|
|
52
|
+
if (!client?.utils?.getOraclePrice) {
|
|
53
|
+
throw new Error("get_pool_info failed and oracle fallback is unavailable (client.utils.getOraclePrice missing).");
|
|
54
|
+
}
|
|
55
|
+
let oracleRaw = 0n;
|
|
56
|
+
try {
|
|
57
|
+
const oracle = await client.utils.getOraclePrice(poolId, chainId);
|
|
58
|
+
const byValue = toPositiveBigint(oracle?.value);
|
|
59
|
+
const byPrice = toPositiveBigint(oracle?.price);
|
|
60
|
+
oracleRaw = byValue ?? byPrice ?? 0n;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`get_pool_info fallback failed to fetch oracle price: ${extractErrorMessage(error)}`);
|
|
64
|
+
}
|
|
65
|
+
if (oracleRaw <= 0n) {
|
|
66
|
+
try {
|
|
67
|
+
const tickerRes = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
|
|
68
|
+
const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
|
|
69
|
+
if (row?.price) {
|
|
70
|
+
const tickerRaw = ensureUnits(row.price, 30, "ticker price");
|
|
71
|
+
const byTicker = toPositiveBigint(tickerRaw);
|
|
72
|
+
oracleRaw = byTicker ?? 0n;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (oracleRaw <= 0n) {
|
|
79
|
+
throw new Error("get_pool_info fallback requires a positive oracle/ticker price, but both resolved to 0.");
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const retried = await pool.getPoolInfo(chainId, poolId, oracleRaw);
|
|
83
|
+
if (!retried) {
|
|
84
|
+
throw new Error(`Pool info for ${poolId} returned undefined after oracle-price retry.`);
|
|
85
|
+
}
|
|
86
|
+
return retried;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new Error(`get_pool_info failed after oracle-price retry: ${extractErrorMessage(error)}`);
|
|
90
|
+
}
|
|
17
91
|
}
|
|
18
92
|
/**
|
|
19
93
|
* 获取池子详情
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Direction, OrderType, TriggerType } from "@myx-trade/sdk";
|
|
2
|
-
import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
2
|
+
import { getChainId, getQuoteToken, getQuoteDecimals } from "../auth/resolveClient.js";
|
|
3
3
|
import { ensureUnits } from "../utils/units.js";
|
|
4
4
|
import { normalizeAddress } from "../utils/address.js";
|
|
5
5
|
import { normalizeSlippagePct4dp } from "../utils/slippage.js";
|
|
6
6
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
7
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
7
8
|
function resolveDirection(direction) {
|
|
8
9
|
if (direction !== 0 && direction !== 1) {
|
|
9
10
|
throw new Error("direction must be 0 (LONG) or 1 (SHORT).");
|
|
@@ -51,6 +52,74 @@ function resolveTpSlTriggerType(isTp, direction, triggerType) {
|
|
|
51
52
|
return direction === 0 ? 2 : 1;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
function collectRows(input) {
|
|
56
|
+
if (Array.isArray(input))
|
|
57
|
+
return input.flatMap(collectRows);
|
|
58
|
+
if (!input || typeof input !== "object")
|
|
59
|
+
return [];
|
|
60
|
+
if (input.poolId || input.pool_id || input.marketId || input.market_id)
|
|
61
|
+
return [input];
|
|
62
|
+
return Object.values(input).flatMap(collectRows);
|
|
63
|
+
}
|
|
64
|
+
function parseDecimals(value, fallback) {
|
|
65
|
+
const parsed = Number(value);
|
|
66
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
67
|
+
return Math.floor(parsed);
|
|
68
|
+
}
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
function normalizeIdentifier(value) {
|
|
72
|
+
return String(value ?? "").trim().toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
|
|
75
|
+
let baseDecimals = 18;
|
|
76
|
+
let quoteDecimals = getQuoteDecimals();
|
|
77
|
+
let resolvedPoolId = String(poolIdHint ?? "").trim();
|
|
78
|
+
const hydrateFromPoolDetail = async (poolId) => {
|
|
79
|
+
if (!poolId)
|
|
80
|
+
return;
|
|
81
|
+
const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
|
|
82
|
+
const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
|
|
83
|
+
if (!detail)
|
|
84
|
+
return;
|
|
85
|
+
baseDecimals = parseDecimals(detail.baseDecimals, baseDecimals);
|
|
86
|
+
quoteDecimals = parseDecimals(detail.quoteDecimals, quoteDecimals);
|
|
87
|
+
resolvedPoolId = String(detail.poolId ?? poolId ?? resolvedPoolId);
|
|
88
|
+
};
|
|
89
|
+
if (resolvedPoolId) {
|
|
90
|
+
try {
|
|
91
|
+
await hydrateFromPoolDetail(resolvedPoolId);
|
|
92
|
+
return { baseDecimals, quoteDecimals, poolId: resolvedPoolId };
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
resolvedPoolId = "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const marketListRes = await client.api.getMarketList();
|
|
100
|
+
const rows = collectRows(marketListRes?.data ?? marketListRes);
|
|
101
|
+
const targetMarketId = normalizeIdentifier(marketId);
|
|
102
|
+
const row = rows.find((item) => normalizeIdentifier(item?.marketId ?? item?.market_id) === targetMarketId);
|
|
103
|
+
if (row) {
|
|
104
|
+
baseDecimals = parseDecimals(row?.baseDecimals ?? row?.base_decimals, baseDecimals);
|
|
105
|
+
quoteDecimals = parseDecimals(row?.quoteDecimals ?? row?.quote_decimals, quoteDecimals);
|
|
106
|
+
const fromRowPoolId = String(row?.poolId ?? row?.pool_id ?? "").trim();
|
|
107
|
+
if (fromRowPoolId) {
|
|
108
|
+
resolvedPoolId = fromRowPoolId;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
}
|
|
114
|
+
if (resolvedPoolId) {
|
|
115
|
+
try {
|
|
116
|
+
await hydrateFromPoolDetail(resolvedPoolId);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { baseDecimals, quoteDecimals, poolId: resolvedPoolId || undefined };
|
|
122
|
+
}
|
|
54
123
|
/**
|
|
55
124
|
* 开仓 / 加仓
|
|
56
125
|
*/
|
|
@@ -249,9 +318,25 @@ export async function setPositionTpSl(client, address, args) {
|
|
|
249
318
|
export async function adjustMargin(client, address, args) {
|
|
250
319
|
const chainId = getChainId();
|
|
251
320
|
const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
|
|
252
|
-
const
|
|
321
|
+
const adjustAmountInput = String(args.adjustAmount ?? "").trim();
|
|
322
|
+
if (!adjustAmountInput) {
|
|
323
|
+
throw new Error("adjustAmount is required.");
|
|
324
|
+
}
|
|
325
|
+
let quoteDecimals = getQuoteDecimals();
|
|
326
|
+
try {
|
|
327
|
+
const detailRes = await client.markets.getMarketDetail({ chainId, poolId: args.poolId });
|
|
328
|
+
const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
|
|
329
|
+
const parsed = Number(detail?.quoteDecimals);
|
|
330
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
331
|
+
quoteDecimals = parsed;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Fallback to env quote decimals if market detail is unavailable.
|
|
336
|
+
}
|
|
337
|
+
const adjustAmount = ensureUnits(adjustAmountInput, quoteDecimals, "adjustAmount");
|
|
253
338
|
if (!/^-?\d+$/.test(adjustAmount)) {
|
|
254
|
-
throw new Error("adjustAmount must
|
|
339
|
+
throw new Error("adjustAmount must resolve to an integer string (raw units).");
|
|
255
340
|
}
|
|
256
341
|
const params = {
|
|
257
342
|
poolId: args.poolId,
|
|
@@ -319,16 +404,36 @@ export async function closeAllPositions(client, address) {
|
|
|
319
404
|
export async function updateOrderTpSl(client, address, args) {
|
|
320
405
|
const chainId = getChainId();
|
|
321
406
|
const quoteToken = normalizeAddress(args.quoteToken, "quoteToken");
|
|
407
|
+
const marketId = String(args.marketId ?? "").trim();
|
|
408
|
+
const isTpSlOrder = typeof args.isTpSlOrder === "boolean" ? args.isTpSlOrder : true;
|
|
409
|
+
const { baseDecimals } = await resolveDecimalsForUpdateOrder(client, chainId, marketId, args.poolId);
|
|
322
410
|
const params = {
|
|
323
411
|
orderId: args.orderId,
|
|
324
|
-
tpSize: args.tpSize,
|
|
325
|
-
tpPrice: args.tpPrice,
|
|
326
|
-
slSize: args.slSize,
|
|
327
|
-
slPrice: args.slPrice,
|
|
328
|
-
useOrderCollateral: args.useOrderCollateral,
|
|
412
|
+
tpSize: ensureUnits(args.tpSize, baseDecimals, "tpSize"),
|
|
413
|
+
tpPrice: ensureUnits(args.tpPrice, 30, "tpPrice"),
|
|
414
|
+
slSize: ensureUnits(args.slSize, baseDecimals, "slSize"),
|
|
415
|
+
slPrice: ensureUnits(args.slPrice, 30, "slPrice"),
|
|
416
|
+
useOrderCollateral: Boolean(args.useOrderCollateral),
|
|
329
417
|
executionFeeToken: quoteToken,
|
|
330
|
-
size: args.size,
|
|
331
|
-
price: args.price,
|
|
418
|
+
size: ensureUnits(args.size, baseDecimals, "size"),
|
|
419
|
+
price: ensureUnits(args.price, 30, "price"),
|
|
332
420
|
};
|
|
333
|
-
|
|
421
|
+
try {
|
|
422
|
+
const result = await client.order.updateOrderTpSl(params, quoteToken, chainId, address, marketId, isTpSlOrder);
|
|
423
|
+
if (Number(result?.code) === 0) {
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
const message = extractErrorMessage(result, "Failed to update order");
|
|
427
|
+
if (/failed to update order/i.test(message)) {
|
|
428
|
+
throw new Error(`Failed to update TP/SL for order ${args.orderId}. If this is a pending LIMIT/STOP order, wait for fill and then use set_tp_sl on the position.`);
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`update_order_tp_sl failed: ${message}`);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
const message = extractErrorMessage(error, "Failed to update order");
|
|
434
|
+
if (/failed to update order/i.test(message)) {
|
|
435
|
+
throw new Error(`Failed to update TP/SL for order ${args.orderId}. If this is a pending LIMIT/STOP order, wait for fill and then use set_tp_sl on the position.`);
|
|
436
|
+
}
|
|
437
|
+
throw new Error(message);
|
|
438
|
+
}
|
|
334
439
|
}
|
|
@@ -8,7 +8,7 @@ export const adjustMarginTool = {
|
|
|
8
8
|
schema: {
|
|
9
9
|
poolId: z.string().describe("Pool ID"),
|
|
10
10
|
positionId: z.string().describe("Position ID"),
|
|
11
|
-
adjustAmount: z.
|
|
11
|
+
adjustAmount: z.union([z.string(), z.number()]).describe("Adjust amount. Human units are supported (e.g. '1' = 1 USDC). Use 'raw:<int>' for exact raw units."),
|
|
12
12
|
quoteToken: z.string().optional().describe("Quote token address"),
|
|
13
13
|
poolOracleType: z.coerce.number().optional().describe("Oracle type: 1 for Chainlink, 2 for Pyth"),
|
|
14
14
|
},
|
|
@@ -1,17 +1,49 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
4
|
+
function normalizeOrderIds(input) {
|
|
5
|
+
if (Array.isArray(input)) {
|
|
6
|
+
return input.map((id) => String(id).trim()).filter(Boolean);
|
|
7
|
+
}
|
|
8
|
+
if (typeof input === "string") {
|
|
9
|
+
const text = input.trim();
|
|
10
|
+
if (!text)
|
|
11
|
+
return [];
|
|
12
|
+
// Support toolchains that serialize arrays as JSON strings.
|
|
13
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(text);
|
|
16
|
+
if (Array.isArray(parsed)) {
|
|
17
|
+
return parsed.map((id) => String(id).trim()).filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Fallback to comma/single parsing below.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (text.includes(",")) {
|
|
25
|
+
return text.split(",").map((id) => id.trim()).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
return [text];
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
4
31
|
export const cancelAllOrdersTool = {
|
|
5
32
|
name: "cancel_all_orders",
|
|
6
|
-
description: "Cancel
|
|
33
|
+
description: "Cancel open orders by orderIds. Accepts array, JSON-array string, comma-separated string, or single orderId.",
|
|
7
34
|
schema: {
|
|
8
|
-
orderIds: z
|
|
35
|
+
orderIds: z
|
|
36
|
+
.union([z.array(z.string()).min(1), z.string().min(1)])
|
|
37
|
+
.describe("Order IDs to cancel. Supports array or string (single/comma/JSON-array)."),
|
|
9
38
|
},
|
|
10
39
|
handler: async (args) => {
|
|
11
40
|
try {
|
|
12
41
|
const { client, signer } = await resolveClient();
|
|
13
42
|
const chainId = getChainId();
|
|
14
|
-
const orderIds = args.orderIds;
|
|
43
|
+
const orderIds = normalizeOrderIds(args.orderIds);
|
|
44
|
+
if (orderIds.length === 0) {
|
|
45
|
+
throw new Error("orderIds is required and must include at least one non-empty order id.");
|
|
46
|
+
}
|
|
15
47
|
const raw = await client.order.cancelAllOrders(orderIds, chainId);
|
|
16
48
|
const result = await finalizeMutationResult(raw, signer, "cancel_all_orders");
|
|
17
49
|
return {
|
|
@@ -3,6 +3,7 @@ import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } f
|
|
|
3
3
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
4
4
|
import { resolvePool } from "../services/marketService.js";
|
|
5
5
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
6
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
6
7
|
export const manageLiquidityTool = {
|
|
7
8
|
name: "manage_liquidity",
|
|
8
9
|
description: "Add or withdraw liquidity from a BASE or QUOTE pool.",
|
|
@@ -40,11 +41,14 @@ export const manageLiquidityTool = {
|
|
|
40
41
|
if (!raw) {
|
|
41
42
|
throw new Error(`SDK returned an empty result for liquidity ${action}. This usually occurs if the pool is not in an Active state (state: 2) or if there is a contract-level restriction. Please check pool_info.`);
|
|
42
43
|
}
|
|
44
|
+
if (raw && typeof raw === "object" && "code" in raw && Number(raw.code) !== 0) {
|
|
45
|
+
throw new Error(`Liquidity ${action} failed: ${extractErrorMessage(raw)}`);
|
|
46
|
+
}
|
|
43
47
|
const data = await finalizeMutationResult(raw, signer, "manage_liquidity");
|
|
44
48
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
45
49
|
}
|
|
46
50
|
catch (error) {
|
|
47
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
51
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
48
52
|
}
|
|
49
53
|
},
|
|
50
54
|
};
|
|
@@ -62,7 +66,7 @@ export const getLpPriceTool = {
|
|
|
62
66
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
63
67
|
}
|
|
64
68
|
catch (error) {
|
|
65
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
69
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
66
70
|
}
|
|
67
71
|
},
|
|
68
72
|
};
|
package/dist/tools/marketInfo.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { getMarketDetail, resolvePool } from "../services/marketService.js";
|
|
4
4
|
import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
|
|
5
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
5
6
|
export const getMarketDetailTool = {
|
|
6
7
|
name: "get_market_detail",
|
|
7
8
|
description: "Get detailed information for a specific trading pool (fee rates, open interest, etc.). Supports PoolId, Token Address, or Keywords.",
|
|
@@ -20,7 +21,7 @@ export const getMarketDetailTool = {
|
|
|
20
21
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
21
22
|
}
|
|
22
23
|
catch (error) {
|
|
23
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
24
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
24
25
|
}
|
|
25
26
|
},
|
|
26
27
|
};
|
|
@@ -36,13 +37,13 @@ export const getPoolInfoTool = {
|
|
|
36
37
|
const { client } = await resolveClient();
|
|
37
38
|
const poolId = await resolvePool(client, args.poolId);
|
|
38
39
|
const chainId = args.chainId ?? getChainId();
|
|
39
|
-
const data = await getPoolInfo(poolId, chainId);
|
|
40
|
+
const data = await getPoolInfo(poolId, chainId, client);
|
|
40
41
|
if (!data)
|
|
41
42
|
throw new Error(`Pool info for ${poolId} returned undefined. The pool may not exist on chainId ${chainId}.`);
|
|
42
43
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
43
44
|
}
|
|
44
45
|
catch (error) {
|
|
45
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
46
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
46
47
|
}
|
|
47
48
|
},
|
|
48
49
|
};
|
|
@@ -65,7 +66,7 @@ export const getLiquidityInfoTool = {
|
|
|
65
66
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
66
67
|
}
|
|
67
68
|
catch (error) {
|
|
68
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
69
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
69
70
|
}
|
|
70
71
|
},
|
|
71
72
|
};
|
|
@@ -2,20 +2,22 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { updateOrderTpSl } from "../services/tradeService.js";
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
5
6
|
export const updateOrderTpSlTool = {
|
|
6
7
|
name: "update_order_tp_sl",
|
|
7
8
|
description: "Update an existing take profit or stop loss order.",
|
|
8
9
|
schema: {
|
|
9
10
|
orderId: z.string().describe("The ID of the order to update"),
|
|
10
11
|
marketId: z.string().describe("The market ID (config hash) for the order"),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
poolId: z.string().optional().describe("Optional poolId hint for decimal resolution."),
|
|
13
|
+
size: z.union([z.string(), z.number()]).describe("Order size (raw or human-readable)"),
|
|
14
|
+
price: z.union([z.string(), z.number()]).describe("Order price (raw or human-readable, 30 decimals)"),
|
|
15
|
+
tpPrice: z.union([z.string(), z.number()]).describe("TP price (raw or human-readable, 30 decimals)"),
|
|
16
|
+
tpSize: z.union([z.string(), z.number()]).describe("TP size (raw or human-readable)"),
|
|
17
|
+
slPrice: z.union([z.string(), z.number()]).describe("SL price (raw or human-readable, 30 decimals)"),
|
|
18
|
+
slSize: z.union([z.string(), z.number()]).describe("SL size (raw or human-readable)"),
|
|
19
|
+
useOrderCollateral: z.coerce.boolean().describe("Whether to use order collateral"),
|
|
20
|
+
isTpSlOrder: z.coerce.boolean().optional().describe("Whether this is a TP/SL order"),
|
|
19
21
|
quoteToken: z.string().describe("Quote token address"),
|
|
20
22
|
},
|
|
21
23
|
handler: async (args) => {
|
|
@@ -26,7 +28,7 @@ export const updateOrderTpSlTool = {
|
|
|
26
28
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
27
29
|
}
|
|
28
30
|
catch (error) {
|
|
29
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
31
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
30
32
|
}
|
|
31
33
|
},
|
|
32
34
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const USELESS_MESSAGE_SET = new Set([
|
|
2
|
+
"",
|
|
3
|
+
"undefined",
|
|
4
|
+
"null",
|
|
5
|
+
"[object object]",
|
|
6
|
+
"{}",
|
|
7
|
+
]);
|
|
8
|
+
function cleanMessage(input) {
|
|
9
|
+
if (typeof input !== "string")
|
|
10
|
+
return null;
|
|
11
|
+
const text = input.trim();
|
|
12
|
+
if (!text)
|
|
13
|
+
return null;
|
|
14
|
+
if (USELESS_MESSAGE_SET.has(text.toLowerCase()))
|
|
15
|
+
return null;
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return !!value && typeof value === "object";
|
|
20
|
+
}
|
|
21
|
+
function safeStringify(value) {
|
|
22
|
+
try {
|
|
23
|
+
return cleanMessage(JSON.stringify(value, (_, item) => (typeof item === "bigint" ? item.toString() : item)));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return cleanMessage(String(value ?? ""));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function isMeaningfulErrorMessage(message) {
|
|
30
|
+
return cleanMessage(message) !== null;
|
|
31
|
+
}
|
|
32
|
+
export function extractErrorMessage(error, fallback = "Unknown error") {
|
|
33
|
+
const visited = new Set();
|
|
34
|
+
const read = (value, depth) => {
|
|
35
|
+
if (depth > 6 || value === null || value === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
if (typeof value === "string")
|
|
38
|
+
return cleanMessage(value);
|
|
39
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
40
|
+
return cleanMessage(String(value));
|
|
41
|
+
}
|
|
42
|
+
if (value instanceof Error) {
|
|
43
|
+
const message = cleanMessage(value.message);
|
|
44
|
+
if (message)
|
|
45
|
+
return message;
|
|
46
|
+
return read(value.cause, depth + 1);
|
|
47
|
+
}
|
|
48
|
+
if (!isRecord(value)) {
|
|
49
|
+
return cleanMessage(String(value));
|
|
50
|
+
}
|
|
51
|
+
if (visited.has(value))
|
|
52
|
+
return null;
|
|
53
|
+
visited.add(value);
|
|
54
|
+
const directKeys = ["message", "reason", "shortMessage", "msg", "detail", "error_description"];
|
|
55
|
+
for (const key of directKeys) {
|
|
56
|
+
const message = read(value[key], depth + 1);
|
|
57
|
+
if (message)
|
|
58
|
+
return message;
|
|
59
|
+
}
|
|
60
|
+
const nestedKeys = ["error", "data", "cause", "response", "info"];
|
|
61
|
+
for (const key of nestedKeys) {
|
|
62
|
+
const message = read(value[key], depth + 1);
|
|
63
|
+
if (message)
|
|
64
|
+
return message;
|
|
65
|
+
}
|
|
66
|
+
return safeStringify(value);
|
|
67
|
+
};
|
|
68
|
+
return read(error, 0) ?? fallback;
|
|
69
|
+
}
|