@michaleffffff/mcp-trading-server 2.9.9 → 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/dist/server.js CHANGED
@@ -4,13 +4,14 @@ import "dotenv/config";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
7
- import { z } from "zod";
7
+ import { ZodError, z } from "zod";
8
8
  // Tools & Modules
9
9
  import * as baseTools from "./tools/index.js";
10
10
  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) => {
@@ -33,6 +34,176 @@ process.on('SIGTERM', shutdown);
33
34
  const allTools = Object.values(baseTools);
34
35
  const allResources = Object.values(baseResources);
35
36
  const allPrompts = Object.values(basePrompts);
37
+ function safeJsonStringify(value) {
38
+ return JSON.stringify(value, (_, item) => (typeof item === "bigint" ? item.toString() : item), 2);
39
+ }
40
+ function inferToolErrorCode(message) {
41
+ const lower = message.toLowerCase();
42
+ if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized")) {
43
+ return "INVALID_PARAM";
44
+ }
45
+ if (lower.includes("insufficient") && (lower.includes("allowance") || lower.includes("approval"))) {
46
+ return "INSUFFICIENT_ALLOWANCE";
47
+ }
48
+ if (lower.includes("insufficient") || lower.includes("not enough balance") || lower.includes("insufficientbalance")) {
49
+ return "INSUFFICIENT_BALANCE";
50
+ }
51
+ if (lower.includes("not found") || lower.includes("unknown tool")) {
52
+ return "NOT_FOUND";
53
+ }
54
+ if (lower.includes("timeout") || lower.includes("not confirmed")) {
55
+ return "TIMEOUT";
56
+ }
57
+ if (lower.includes("network") || lower.includes("rpc")) {
58
+ return "NETWORK_ERROR";
59
+ }
60
+ return "TOOL_EXECUTION_ERROR";
61
+ }
62
+ function defaultHintForErrorCode(code, toolName) {
63
+ if (code === "INVALID_PARAM") {
64
+ return `Check required fields/types for "${toolName}" and retry.`;
65
+ }
66
+ if (code === "INSUFFICIENT_ALLOWANCE") {
67
+ return `Run "check_approval" before retrying "${toolName}".`;
68
+ }
69
+ if (code === "INSUFFICIENT_BALANCE") {
70
+ return "Top up wallet/trading-account balance or reduce order size.";
71
+ }
72
+ if (code === "NOT_FOUND") {
73
+ return "Verify identifiers (poolId/orderId/marketId) and retry.";
74
+ }
75
+ if (code === "TIMEOUT") {
76
+ return "Query order/position status first, then retry only if needed.";
77
+ }
78
+ if (code === "NETWORK_ERROR") {
79
+ return "Check RPC/network health and retry.";
80
+ }
81
+ return `Check prerequisites for "${toolName}" and retry.`;
82
+ }
83
+ function errorResult(payload) {
84
+ const body = {
85
+ status: "error",
86
+ error: {
87
+ tool: payload.tool,
88
+ code: payload.code,
89
+ message: payload.message,
90
+ hint: payload.hint,
91
+ action: payload.action,
92
+ details: payload.details,
93
+ },
94
+ };
95
+ return {
96
+ content: [{ type: "text", text: safeJsonStringify(body) }],
97
+ isError: true,
98
+ };
99
+ }
100
+ function parseFirstText(result) {
101
+ if (!result || !Array.isArray(result.content))
102
+ return null;
103
+ const firstText = result.content.find((item) => item?.type === "text" && typeof item.text === "string");
104
+ return firstText?.text ?? null;
105
+ }
106
+ function normalizeToolErrorResult(toolName, result) {
107
+ const rawText = parseFirstText(result);
108
+ if (!rawText) {
109
+ return errorResult({
110
+ tool: toolName,
111
+ code: "TOOL_EXECUTION_ERROR",
112
+ message: `Tool "${toolName}" returned an empty error.`,
113
+ hint: defaultHintForErrorCode("TOOL_EXECUTION_ERROR", toolName),
114
+ action: "Inspect server logs for details.",
115
+ });
116
+ }
117
+ let parsed = null;
118
+ try {
119
+ parsed = JSON.parse(rawText);
120
+ }
121
+ catch {
122
+ parsed = null;
123
+ }
124
+ if (parsed &&
125
+ parsed.status === "error" &&
126
+ parsed.error &&
127
+ typeof parsed.error.code === "string" &&
128
+ isMeaningfulErrorMessage(parsed.error.message)) {
129
+ return result;
130
+ }
131
+ const plainMessage = extractErrorMessage(typeof parsed?.error?.message === "string"
132
+ ? parsed.error.message
133
+ : typeof parsed?.message === "string"
134
+ ? parsed.message
135
+ : rawText.replace(/^Error:\s*/i, "").trim(), `Tool "${toolName}" failed.`);
136
+ const code = typeof parsed?.error?.code === "string"
137
+ ? parsed.error.code
138
+ : inferToolErrorCode(plainMessage);
139
+ return errorResult({
140
+ tool: toolName,
141
+ code,
142
+ message: plainMessage,
143
+ hint: defaultHintForErrorCode(code, toolName),
144
+ action: "Adjust parameters/prerequisites and retry.",
145
+ });
146
+ }
147
+ function validationErrorResult(toolName, error) {
148
+ const issues = error.issues.map((issue) => {
149
+ const pathValue = issue.path.length > 0 ? issue.path.map(String).join(".") : "input";
150
+ return {
151
+ field: pathValue,
152
+ code: issue.code,
153
+ message: issue.message,
154
+ };
155
+ });
156
+ const firstIssue = issues[0];
157
+ const firstHint = firstIssue
158
+ ? `Fix "${firstIssue.field}": ${firstIssue.message}`
159
+ : `Check input schema for "${toolName}".`;
160
+ return errorResult({
161
+ tool: toolName,
162
+ code: "INVALID_PARAM",
163
+ message: `Invalid arguments for tool "${toolName}".`,
164
+ hint: firstHint,
165
+ action: "Call list_tools and resend valid arguments.",
166
+ details: { issues },
167
+ });
168
+ }
169
+ function normalizeSdkReadFailure(toolName, result) {
170
+ const rawText = parseFirstText(result);
171
+ if (!rawText)
172
+ return result;
173
+ let parsed = null;
174
+ try {
175
+ parsed = JSON.parse(rawText);
176
+ }
177
+ catch {
178
+ return result;
179
+ }
180
+ if (!parsed || parsed.status !== "success")
181
+ return result;
182
+ const data = parsed.data;
183
+ const hasCode = !!data && typeof data === "object" && !Array.isArray(data) && Object.prototype.hasOwnProperty.call(data, "code");
184
+ if (!hasCode)
185
+ return result;
186
+ const code = Number(data.code);
187
+ if (!Number.isFinite(code) || code === 0)
188
+ return result;
189
+ const sdkPayload = data.data;
190
+ const hasPayload = sdkPayload !== null &&
191
+ sdkPayload !== undefined &&
192
+ (!Array.isArray(sdkPayload) || sdkPayload.length > 0);
193
+ if (code > 0 && hasPayload) {
194
+ // Some SDK APIs return non-zero positive codes with usable payloads.
195
+ return result;
196
+ }
197
+ const sdkMessage = String(data.msg ?? data.message ?? `SDK read failed with code=${code}.`);
198
+ return errorResult({
199
+ tool: toolName,
200
+ code: "SDK_READ_ERROR",
201
+ message: `${toolName} failed: code=${code}, msg=${sdkMessage}`,
202
+ hint: "Check chain/account context and required params, then retry.",
203
+ action: "Retry or call prerequisite tools to refresh context.",
204
+ details: { sdk: data },
205
+ });
206
+ }
36
207
  // 将 Zod schema 转换为 JSON Schema (tool listing 用)
37
208
  function zodSchemaToJsonSchema(zodSchema) {
38
209
  const toPropSchema = (value) => {
@@ -81,7 +252,7 @@ function zodSchemaToJsonSchema(zodSchema) {
81
252
  };
82
253
  }
83
254
  // ─── MCP Server ───
84
- const server = new Server({ name: "myx-mcp-trading-server", version: "2.9.9" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
255
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
85
256
  // List tools
86
257
  server.setRequestHandler(ListToolsRequestSchema, async () => {
87
258
  return {
@@ -97,30 +268,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
97
268
  const { name, arguments: args } = request.params;
98
269
  const tool = allTools.find((t) => t.name === name);
99
270
  if (!tool) {
100
- return {
101
- content: [{ type: "text", text: `Error: Unknown tool "${name}"` }],
102
- isError: true,
103
- };
271
+ return errorResult({
272
+ tool: name,
273
+ code: "NOT_FOUND",
274
+ message: `Unknown tool "${name}".`,
275
+ hint: "Use list_tools to discover available tool names.",
276
+ action: "Retry with a valid tool name.",
277
+ });
104
278
  }
105
279
  try {
106
280
  let validatedArgs = args ?? {};
107
281
  if (tool.schema) {
108
- validatedArgs = z.object(tool.schema).strict().parse(args ?? {});
282
+ try {
283
+ validatedArgs = z.object(tool.schema).strict().parse(args ?? {});
284
+ }
285
+ catch (validationError) {
286
+ if (validationError instanceof ZodError) {
287
+ return validationErrorResult(name, validationError);
288
+ }
289
+ throw validationError;
290
+ }
109
291
  }
110
292
  logger.toolExecution(name, validatedArgs);
111
293
  const result = await tool.handler(validatedArgs);
112
- if (result && result.content)
113
- return result;
294
+ if (result?.isError) {
295
+ return normalizeToolErrorResult(name, result);
296
+ }
297
+ if (result && result.content) {
298
+ return normalizeSdkReadFailure(name, result);
299
+ }
114
300
  return {
115
- content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }],
301
+ content: [{ type: "text", text: safeJsonStringify({ status: "success", data: result }) }],
116
302
  };
117
303
  }
118
304
  catch (error) {
119
305
  logger.error(`Error executing tool: ${name}`, error);
120
- return {
121
- content: [{ type: "text", text: `Error: ${error.message}` }],
122
- isError: true,
123
- };
306
+ const message = extractErrorMessage(error);
307
+ const code = inferToolErrorCode(message);
308
+ return errorResult({
309
+ tool: name,
310
+ code,
311
+ message,
312
+ hint: defaultHintForErrorCode(code, name),
313
+ action: "Fix inputs/prerequisites and retry.",
314
+ });
124
315
  }
125
316
  });
126
317
  // Resources Handlers
@@ -181,7 +372,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
181
372
  async function main() {
182
373
  const transport = new StdioServerTransport();
183
374
  await server.connect(transport);
184
- logger.info("🚀 MYX Trading MCP Server v2.9.9 running (stdio, pure on-chain, prod ready)");
375
+ logger.info("🚀 MYX Trading MCP Server v3.0.1 running (stdio, pure on-chain, prod ready)");
185
376
  }
186
377
  main().catch((err) => {
187
378
  logger.error("Fatal Server Startup Error", err);
@@ -1,5 +1,65 @@
1
1
  import { getChainId } from "../auth/resolveClient.js";
2
2
  import { getMarketStateDesc } from "../utils/mappings.js";
3
+ function collectRows(input) {
4
+ if (Array.isArray(input))
5
+ return input.flatMap(collectRows);
6
+ if (!input || typeof input !== "object")
7
+ return [];
8
+ if (input.poolId || input.pool_id)
9
+ return [input];
10
+ return Object.values(input).flatMap(collectRows);
11
+ }
12
+ function extractMarketRows(raw, chainId) {
13
+ if (raw?.contractInfo && Array.isArray(raw.contractInfo.list)) {
14
+ return raw.contractInfo.list;
15
+ }
16
+ if (Array.isArray(raw?.data)) {
17
+ return raw.data;
18
+ }
19
+ if (raw?.data && Array.isArray(raw.data[chainId])) {
20
+ return raw.data[chainId];
21
+ }
22
+ if (Array.isArray(raw)) {
23
+ return raw;
24
+ }
25
+ return collectRows(raw?.data ?? raw);
26
+ }
27
+ function normalizeMarketState(value) {
28
+ if (value === undefined || value === null || value === "")
29
+ return null;
30
+ const casted = Number(value);
31
+ return Number.isFinite(casted) ? casted : null;
32
+ }
33
+ function normalizePoolId(row) {
34
+ const poolId = row?.poolId ?? row?.pool_id ?? "";
35
+ return String(poolId);
36
+ }
37
+ function matchesKeyword(row, keywordUpper) {
38
+ const poolId = normalizePoolId(row);
39
+ const haystack = [
40
+ row?.baseSymbol,
41
+ row?.quoteSymbol,
42
+ row?.baseQuoteSymbol,
43
+ row?.symbolName,
44
+ row?.name,
45
+ poolId,
46
+ row?.baseToken,
47
+ row?.quoteToken,
48
+ row?.marketId,
49
+ ]
50
+ .map((value) => String(value ?? "").toUpperCase())
51
+ .join("|");
52
+ return haystack.includes(keywordUpper);
53
+ }
54
+ async function fetchApiMarketRows(client) {
55
+ const marketListRes = await client.api.getMarketList().catch(() => null);
56
+ const marketRows = extractMarketRows(marketListRes, getChainId());
57
+ const marketRowsWithPoolId = marketRows.filter((row) => normalizePoolId(row));
58
+ if (marketRowsWithPoolId.length > 0)
59
+ return marketRowsWithPoolId;
60
+ const poolListRes = await client.api.getPoolList().catch(() => null);
61
+ return collectRows(poolListRes?.data ?? poolListRes);
62
+ }
3
63
  export async function getMarketPrice(client, poolId, chainIdOverride) {
4
64
  const chainId = chainIdOverride ?? getChainId();
5
65
  const tickerRes = await client.markets.getTickerList({
@@ -15,28 +75,45 @@ export async function getOraclePrice(client, poolId, chainIdOverride) {
15
75
  }
16
76
  export async function searchMarket(client, keyword, limit = 1000) {
17
77
  const chainId = getChainId();
18
- const searchRes = await client.markets.searchMarket({ chainId, keyword, limit });
19
- const raw = searchRes;
78
+ const normalizedKeyword = String(keyword ?? "").trim();
79
+ const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
20
80
  let dataList = [];
21
- if (raw && raw.contractInfo && Array.isArray(raw.contractInfo.list)) {
22
- dataList = raw.contractInfo.list;
81
+ try {
82
+ const searchRes = await client.markets.searchMarket({ chainId, keyword: normalizedKeyword, limit: requestedLimit });
83
+ dataList = extractMarketRows(searchRes, chainId);
23
84
  }
24
- else if (raw && Array.isArray(raw.data)) {
25
- dataList = raw.data;
85
+ catch {
86
+ dataList = [];
26
87
  }
27
- else if (raw && raw.data && Array.isArray(raw.data[chainId])) {
28
- dataList = raw.data[chainId];
88
+ if (dataList.length === 0 || normalizedKeyword.length === 0) {
89
+ const fallbackRows = await fetchApiMarketRows(client);
90
+ if (fallbackRows.length > 0) {
91
+ dataList = fallbackRows;
92
+ }
29
93
  }
30
- else if (raw && Array.isArray(raw)) {
31
- dataList = raw;
94
+ const filteredRows = normalizedKeyword
95
+ ? dataList.filter((row) => matchesKeyword(row, normalizedKeyword.toUpperCase()))
96
+ : dataList;
97
+ const dedupedByPoolId = new Map();
98
+ for (const row of filteredRows) {
99
+ const poolId = normalizePoolId(row);
100
+ if (!poolId)
101
+ continue;
102
+ if (!dedupedByPoolId.has(poolId)) {
103
+ dedupedByPoolId.set(poolId, row);
104
+ }
32
105
  }
33
- // Filter tradable markets (state=2 => Active)
34
- const activeMarkets = dataList.filter(m => Number(m.state) === 2);
106
+ const activeMarkets = Array.from(dedupedByPoolId.values())
107
+ .filter((row) => {
108
+ const state = normalizeMarketState(row?.state ?? row?.poolState);
109
+ return state === null || state === 2;
110
+ })
111
+ .slice(0, requestedLimit);
35
112
  // Get tickers for these pools to get price and change24h
36
113
  let tickers = [];
37
114
  if (activeMarkets.length > 0) {
38
115
  try {
39
- const poolIds = activeMarkets.map(m => m.poolId);
116
+ const poolIds = activeMarkets.map((market) => normalizePoolId(market));
40
117
  const tickerRes = await client.markets.getTickerList({ chainId, poolIds });
41
118
  tickers = Array.isArray(tickerRes) ? tickerRes : (tickerRes?.data || []);
42
119
  }
@@ -44,17 +121,20 @@ export async function searchMarket(client, keyword, limit = 1000) {
44
121
  console.error("Failed to fetch tickers:", e);
45
122
  }
46
123
  }
47
- return activeMarkets.map(m => {
48
- const ticker = tickers.find(t => String(t.poolId).toLowerCase() === String(m.poolId).toLowerCase());
124
+ return activeMarkets.map((market) => {
125
+ const poolId = normalizePoolId(market);
126
+ const state = normalizeMarketState(market?.state ?? market?.poolState);
127
+ const ticker = tickers.find((t) => String(t.poolId).toLowerCase() === poolId.toLowerCase());
128
+ const symbol = market?.baseQuoteSymbol || [market?.baseSymbol, market?.quoteSymbol].filter(Boolean).join("/");
49
129
  return {
50
- symbol: m.baseQuoteSymbol || `${m.baseSymbol}/${m.quoteSymbol}`,
51
- name: m.symbolName || m.name,
52
- poolId: m.poolId,
130
+ symbol: symbol || market?.symbolName || poolId,
131
+ name: market?.symbolName || market?.name || symbol || poolId,
132
+ poolId,
53
133
  price: ticker ? ticker.price : "0",
54
134
  change24h: ticker ? ticker.change : "0",
55
- tvl: m.tvl || "0",
56
- state: m.state,
57
- stateDescription: getMarketStateDesc(m.state)
135
+ tvl: market?.tvl || "0",
136
+ state: state ?? "unknown",
137
+ stateDescription: state === null ? "Unknown" : getMarketStateDesc(state)
58
138
  };
59
139
  });
60
140
  }
@@ -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
- return pool.getPoolInfo(chainId, poolId);
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 adjustAmount = String(args.adjustAmount ?? "").trim();
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 be an integer string in quote token raw units.");
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
- return client.order.updateOrderTpSl(params, quoteToken, chainId, address, args.marketId, args.isTpSlOrder);
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
  }