@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/CHANGELOG.md +32 -0
- package/README.md +107 -8
- package/TOOL_EXAMPLES.md +593 -0
- package/dist/prompts/tradingGuide.js +10 -4
- package/dist/server.js +206 -15
- package/dist/services/marketService.js +101 -21
- package/dist/services/poolService.js +76 -2
- package/dist/services/tradeService.js +116 -11
- package/dist/tools/accountInfo.js +119 -49
- package/dist/tools/adjustMargin.js +1 -1
- package/dist/tools/cancelAllOrders.js +35 -3
- package/dist/tools/index.js +1 -2
- package/dist/tools/manageLiquidity.js +6 -2
- package/dist/tools/marketInfo.js +5 -4
- package/dist/tools/searchMarket.js +3 -3
- package/dist/tools/updateOrderTpSl.js +11 -9
- package/dist/utils/errorMessage.js +69 -0
- package/package.json +5 -2
- package/dist/tools/getBalances.js +0 -17
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: "
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
19
|
-
const
|
|
78
|
+
const normalizedKeyword = String(keyword ?? "").trim();
|
|
79
|
+
const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
|
|
20
80
|
let dataList = [];
|
|
21
|
-
|
|
22
|
-
|
|
81
|
+
try {
|
|
82
|
+
const searchRes = await client.markets.searchMarket({ chainId, keyword: normalizedKeyword, limit: requestedLimit });
|
|
83
|
+
dataList = extractMarketRows(searchRes, chainId);
|
|
23
84
|
}
|
|
24
|
-
|
|
25
|
-
dataList =
|
|
85
|
+
catch {
|
|
86
|
+
dataList = [];
|
|
26
87
|
}
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
dataList
|
|
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
|
-
|
|
34
|
-
|
|
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(
|
|
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(
|
|
48
|
-
const
|
|
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:
|
|
51
|
-
name:
|
|
52
|
-
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:
|
|
56
|
-
state:
|
|
57
|
-
stateDescription: getMarketStateDesc(
|
|
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
|
-
|
|
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
|
}
|