@michaleffffff/mcp-trading-server 3.0.4 → 3.0.7
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 +42 -6
- package/README.md +49 -414
- package/TOOL_EXAMPLES.md +63 -559
- package/dist/auth/resolveClient.js +19 -2
- package/dist/prompts/tradingGuide.js +16 -23
- package/dist/server.js +20 -2
- package/dist/services/balanceService.js +4 -4
- package/dist/services/marketService.js +48 -12
- package/dist/services/poolService.js +45 -44
- package/dist/services/tradeService.js +51 -23
- package/dist/tools/accountInfo.js +5 -134
- package/dist/tools/accountTransfer.js +7 -12
- package/dist/tools/adjustMargin.js +1 -1
- package/dist/tools/cancelOrders.js +71 -0
- package/dist/tools/checkAccountReady.js +75 -0
- package/dist/tools/checkApproval.js +1 -1
- package/dist/tools/closeAllPositions.js +9 -7
- package/dist/tools/closePosition.js +18 -11
- package/dist/tools/createPerpMarket.js +1 -1
- package/dist/tools/executeTrade.js +6 -6
- package/dist/tools/findPool.js +26 -0
- package/dist/tools/getAccountSnapshot.js +47 -0
- package/dist/tools/getAllTickers.js +5 -4
- package/dist/tools/getBaseDetail.js +1 -1
- package/dist/tools/getKline.js +7 -4
- package/dist/tools/getMyLpHoldings.js +3 -2
- package/dist/tools/getNetworkFee.js +1 -1
- package/dist/tools/getOrders.js +46 -0
- package/dist/tools/getPoolMetadata.js +95 -0
- package/dist/tools/getPositionsAll.js +80 -0
- package/dist/tools/{getMarketPrice.js → getPrice.js} +8 -5
- package/dist/tools/getUserTradingFeeRate.js +66 -3
- package/dist/tools/index.js +15 -19
- package/dist/tools/listPools.js +53 -0
- package/dist/tools/manageLiquidity.js +4 -4
- package/dist/tools/manageTpSl.js +234 -0
- package/dist/tools/openPositionSimple.js +27 -8
- package/dist/tools/searchTools.js +35 -0
- package/dist/utils/mappings.js +10 -7
- package/package.json +2 -2
- package/dist/tools/cancelAllOrders.js +0 -57
- package/dist/tools/cancelOrder.js +0 -22
- package/dist/tools/getAccountVipInfo.js +0 -20
- package/dist/tools/getKlineLatestBar.js +0 -28
- package/dist/tools/getOraclePrice.js +0 -22
- package/dist/tools/getPoolList.js +0 -17
- package/dist/tools/getPoolSymbolAll.js +0 -16
- package/dist/tools/getPositions.js +0 -77
- package/dist/tools/marketInfo.js +0 -88
- package/dist/tools/orderQueries.js +0 -51
- package/dist/tools/poolConfig.js +0 -22
- package/dist/tools/positionHistory.js +0 -28
- package/dist/tools/searchMarket.js +0 -21
- package/dist/tools/setTpSl.js +0 -50
- package/dist/tools/updateOrderTpSl.js +0 -34
|
@@ -24,6 +24,8 @@ export async function resolveClient() {
|
|
|
24
24
|
const rpcUrl = process.env.RPC_URL || "https://rpc.sepolia.linea.build";
|
|
25
25
|
const privateKey = process.env.PRIVATE_KEY;
|
|
26
26
|
const chainId = Number(process.env.CHAIN_ID) || 59141;
|
|
27
|
+
const isTestnet = process.env.IS_TESTNET !== "false";
|
|
28
|
+
const isBetaMode = String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
|
|
27
29
|
const brokerAddressRaw = process.env.BROKER_ADDRESS || getDefaultBrokerByChainId(chainId);
|
|
28
30
|
const quoteTokenRaw = process.env.QUOTE_TOKEN_ADDRESS || getDefaultQuoteTokenByChainId(chainId);
|
|
29
31
|
const quoteDecimals = Number(process.env.QUOTE_TOKEN_DECIMALS) || 6;
|
|
@@ -42,12 +44,27 @@ export async function resolveClient() {
|
|
|
42
44
|
// Inject the EIP-1193 mock so SDK can sign transactions seamlessly
|
|
43
45
|
const { injectBrowserProviderMock } = await import("../utils/injectProvider.js");
|
|
44
46
|
injectBrowserProviderMock(chainId, provider, signer);
|
|
47
|
+
const ethereumProvider = globalThis.window.ethereum;
|
|
48
|
+
const walletClient = {
|
|
49
|
+
transport: ethereumProvider,
|
|
50
|
+
chain: { id: chainId },
|
|
51
|
+
account: { address: signer.address, type: "json-rpc" },
|
|
52
|
+
getAddresses: async () => [signer.address],
|
|
53
|
+
request: async (args) => ethereumProvider.request(args),
|
|
54
|
+
signMessage: async ({ message }) => {
|
|
55
|
+
const payload = typeof message === "string"
|
|
56
|
+
? message
|
|
57
|
+
: (message?.raw ?? message?.message ?? "");
|
|
58
|
+
return signer.signMessage(payload);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
45
61
|
const client = new MyxClient({
|
|
46
62
|
chainId,
|
|
47
63
|
signer: signer,
|
|
48
64
|
brokerAddress,
|
|
49
|
-
isTestnet
|
|
50
|
-
|
|
65
|
+
isTestnet,
|
|
66
|
+
isBetaMode,
|
|
67
|
+
walletClient: walletClient
|
|
51
68
|
});
|
|
52
69
|
cached = { client, address: signer.address, signer, chainId, quoteToken, quoteDecimals };
|
|
53
70
|
return cached;
|
|
@@ -12,35 +12,28 @@ export const tradingGuidePrompt = {
|
|
|
12
12
|
content: {
|
|
13
13
|
type: "text",
|
|
14
14
|
text: `
|
|
15
|
-
# MYX Trading MCP Best Practices (v3.0.
|
|
15
|
+
# MYX Trading MCP Best Practices (v3.0.4)
|
|
16
16
|
|
|
17
17
|
You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
|
|
18
18
|
|
|
19
19
|
## 1. The Standard Workflow
|
|
20
|
-
1. **Discovery**: Use \`
|
|
21
|
-
2. **Context**: Use \`
|
|
22
|
-
3. **
|
|
23
|
-
4. **
|
|
24
|
-
5. **
|
|
20
|
+
1. **Discovery**: Use \`find_pool\` with a keyword (e.g. "BTC") to get the \`poolId\`.
|
|
21
|
+
2. **Context**: Use \`get_account_snapshot\` (with \`poolId\`) to check balances, trading metrics, and VIP tier. Use \`get_price\` for real-time market/oracle prices.
|
|
22
|
+
3. **Pre-check**: Use \`check_account_ready\` to ensure the trading account has enough collateral.
|
|
23
|
+
4. **Execution**: Prefer \`open_position_simple\` for entry. It supports Stop-Loss (\`slPrice\`) and Take-Profit (\`tpPrice\`) in one call.
|
|
24
|
+
5. **Monitoring**: Use \`get_positions_all\` to track active trades and \`get_orders\` for pending/filled history.
|
|
25
|
+
6. **Unified Operations**: Use \`cancel_orders\` for targeted or global撤单, and \`manage_tp_sl\` to update protection orders.
|
|
25
26
|
|
|
26
27
|
## 2. Parameter Tips
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- **Adjust Margin**: \`adjust_margin.adjustAmount\` supports human amount (e.g. "1"), and also supports exact raw amount via \`raw:\` prefix.
|
|
37
|
-
- **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\`.
|
|
38
|
-
- **Pool Reads**: \`get_pool_info\` auto-retries with oracle/ticker price when direct on-chain read returns divide-by-zero.
|
|
39
|
-
- **Liquidity Price Input**: \`get_liquidity_info.marketPrice\` accepts raw 30-dec price and human price strings (e.g. \`2172.5\`, \`human:2172.5\`).
|
|
40
|
-
- **close_all_positions Slippage**: accepts both raw 4dp and human percent strings (e.g. \`"100"\`, \`"1.0"\`, \`"1%"\`).
|
|
41
|
-
- **close_position Full Close**: \`size\`/\`collateralAmount\` can use \`ALL\`/\`FULL\`/\`MAX\` to auto-fill exact live raw values from the current position snapshot.
|
|
42
|
-
- **Deposits & Approval**: \`account_deposit\` supports \`autoApprove\` + \`approveMax\`; without auto-approve, allowance errors should be handled via \`check_approval\`.
|
|
43
|
-
- **Examples**: Follow \`TOOL_EXAMPLES.md\` payload patterns when building tool arguments.
|
|
28
|
+
- **Consolidated Tools**: Many legacy tools have been merged. Always use the high-level versions (e.g., \`get_price\` instead of \`get_market_price\`).
|
|
29
|
+
- **Unit Prefixes**: Prefer \`human:\` for readable amounts (e.g., "100" USDC) and \`raw:\` for exact on-chain units.
|
|
30
|
+
- **Slippage**: Default is 100 (1%). For volatile tokens, consider 200-300 (2-3%).
|
|
31
|
+
- **Fees**: Use \`get_pool_metadata\` to view current fee tiers and pool configuration.
|
|
32
|
+
- **LP Strategy**: Use \`get_my_lp_holdings\` to monitor liquidity positions. Naming follows \`mBASE.QUOTE\` (e.g., \`mBTC.USDC\`).
|
|
33
|
+
|
|
34
|
+
Current Session:
|
|
35
|
+
- Wallet: ${address}
|
|
36
|
+
- Chain ID: ${chainId}
|
|
44
37
|
|
|
45
38
|
## 3. Self-Healing
|
|
46
39
|
If a transaction reverts with a hex code, the server will attempt to decode it (e.g., "AccountInsufficientFreeAmount"). Error payloads now include structured \`code/hint/action\` fields; use them to provide concrete next steps.
|
package/dist/server.js
CHANGED
|
@@ -57,6 +57,12 @@ function inferToolErrorCode(message) {
|
|
|
57
57
|
if (lower.includes("network") || lower.includes("rpc")) {
|
|
58
58
|
return "NETWORK_ERROR";
|
|
59
59
|
}
|
|
60
|
+
if (lower.includes("order size out of range") || lower.includes("minordersize")) {
|
|
61
|
+
return "ORDER_SIZE_TOO_SMALL";
|
|
62
|
+
}
|
|
63
|
+
if (lower.includes("marketid missing") || lower.includes("could not find pool metadata")) {
|
|
64
|
+
return "POOL_NOT_FOUND";
|
|
65
|
+
}
|
|
60
66
|
return "TOOL_EXECUTION_ERROR";
|
|
61
67
|
}
|
|
62
68
|
function defaultHintForErrorCode(code, toolName) {
|
|
@@ -78,6 +84,12 @@ function defaultHintForErrorCode(code, toolName) {
|
|
|
78
84
|
if (code === "NETWORK_ERROR") {
|
|
79
85
|
return "Check RPC/network health and retry.";
|
|
80
86
|
}
|
|
87
|
+
if (code === "ORDER_SIZE_TOO_SMALL") {
|
|
88
|
+
return "Increase collateralAmount or leverage to meet the minimum order size requirement.";
|
|
89
|
+
}
|
|
90
|
+
if (code === "POOL_NOT_FOUND") {
|
|
91
|
+
return "The pool ID seems invalid or not supported. Use 'find_pool' or 'list_pools' to find the correct Pool ID.";
|
|
92
|
+
}
|
|
81
93
|
return `Check prerequisites for "${toolName}" and retry.`;
|
|
82
94
|
}
|
|
83
95
|
function errorResult(payload) {
|
|
@@ -313,6 +325,12 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
313
325
|
}
|
|
314
326
|
if (zodType === "string")
|
|
315
327
|
return { type: "string" };
|
|
328
|
+
if (zodType === "enum") {
|
|
329
|
+
return { type: "string", enum: def?.values || [] };
|
|
330
|
+
}
|
|
331
|
+
if (zodType === "nativeEnum") {
|
|
332
|
+
return { type: "string", enum: Object.values(def?.values || {}) };
|
|
333
|
+
}
|
|
316
334
|
if (zodType === "number")
|
|
317
335
|
return { type: "number" };
|
|
318
336
|
if (zodType === "boolean")
|
|
@@ -352,7 +370,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
352
370
|
};
|
|
353
371
|
}
|
|
354
372
|
// ─── MCP Server ───
|
|
355
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.
|
|
373
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.7" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
356
374
|
// List tools
|
|
357
375
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
358
376
|
return {
|
|
@@ -473,7 +491,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
473
491
|
async function main() {
|
|
474
492
|
const transport = new StdioServerTransport();
|
|
475
493
|
await server.connect(transport);
|
|
476
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
494
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.7 running (stdio, pure on-chain, prod ready)");
|
|
477
495
|
}
|
|
478
496
|
main().catch((err) => {
|
|
479
497
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getChainId } from "../auth/resolveClient.js";
|
|
2
|
-
export async function getBalances(client, address) {
|
|
3
|
-
const chainId = getChainId();
|
|
2
|
+
export async function getBalances(client, address, chainIdOverride) {
|
|
3
|
+
const chainId = chainIdOverride ?? getChainId();
|
|
4
4
|
return client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
5
5
|
}
|
|
6
|
-
export async function getMarginBalance(client, address, poolId) {
|
|
7
|
-
const chainId = getChainId();
|
|
6
|
+
export async function getMarginBalance(client, address, poolId, chainIdOverride) {
|
|
7
|
+
const chainId = chainIdOverride ?? getChainId();
|
|
8
8
|
return client.account.getAvailableMarginBalance({ poolId, chainId, address });
|
|
9
9
|
}
|
|
@@ -51,14 +51,33 @@ function matchesKeyword(row, keywordUpper) {
|
|
|
51
51
|
.join("|");
|
|
52
52
|
return haystack.includes(keywordUpper);
|
|
53
53
|
}
|
|
54
|
-
async function fetchApiMarketRows(client) {
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
async function fetchApiMarketRows(client, chainId) {
|
|
55
|
+
// Preferred path: documented markets.searchMarket
|
|
56
|
+
try {
|
|
57
|
+
const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
|
|
58
|
+
const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
|
|
59
|
+
if (searchRows.length > 0)
|
|
60
|
+
return searchRows;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
// Secondary path: documented markets.getPoolSymbolAll
|
|
65
|
+
try {
|
|
66
|
+
const symbolsRes = await client.markets.getPoolSymbolAll();
|
|
67
|
+
const symbolRows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
|
|
68
|
+
if (symbolRows.length > 0)
|
|
69
|
+
return symbolRows;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
}
|
|
73
|
+
// Legacy fallback: internal api namespace (for backward compatibility)
|
|
74
|
+
const marketListRes = await client.api?.getMarketList?.().catch(() => null);
|
|
75
|
+
const marketRows = extractMarketRows(marketListRes, chainId);
|
|
57
76
|
const marketRowsWithPoolId = marketRows.filter((row) => normalizePoolId(row));
|
|
58
77
|
if (marketRowsWithPoolId.length > 0)
|
|
59
78
|
return marketRowsWithPoolId;
|
|
60
|
-
const poolListRes = await client.api
|
|
61
|
-
return collectRows(poolListRes?.data ?? poolListRes);
|
|
79
|
+
const poolListRes = await client.api?.getPoolList?.().catch(() => null);
|
|
80
|
+
return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row));
|
|
62
81
|
}
|
|
63
82
|
export async function getMarketPrice(client, poolId, chainIdOverride) {
|
|
64
83
|
const chainId = chainIdOverride ?? getChainId();
|
|
@@ -73,8 +92,8 @@ export async function getOraclePrice(client, poolId, chainIdOverride) {
|
|
|
73
92
|
const chainId = chainIdOverride ?? getChainId();
|
|
74
93
|
return client.utils.getOraclePrice(poolId, chainId);
|
|
75
94
|
}
|
|
76
|
-
export async function searchMarket(client, keyword, limit = 1000) {
|
|
77
|
-
const chainId = getChainId();
|
|
95
|
+
export async function searchMarket(client, keyword, limit = 1000, chainIdOverride) {
|
|
96
|
+
const chainId = chainIdOverride ?? getChainId();
|
|
78
97
|
const normalizedKeyword = String(keyword ?? "").trim();
|
|
79
98
|
const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
|
|
80
99
|
let dataList = [];
|
|
@@ -86,7 +105,7 @@ export async function searchMarket(client, keyword, limit = 1000) {
|
|
|
86
105
|
dataList = [];
|
|
87
106
|
}
|
|
88
107
|
if (dataList.length === 0 || normalizedKeyword.length === 0) {
|
|
89
|
-
const fallbackRows = await fetchApiMarketRows(client);
|
|
108
|
+
const fallbackRows = await fetchApiMarketRows(client, chainId);
|
|
90
109
|
if (fallbackRows.length > 0) {
|
|
91
110
|
dataList = fallbackRows;
|
|
92
111
|
}
|
|
@@ -149,8 +168,25 @@ export async function getMarketDetail(client, poolId, chainIdOverride) {
|
|
|
149
168
|
/**
|
|
150
169
|
* 获取所有池子列表
|
|
151
170
|
*/
|
|
152
|
-
export async function getPoolList(client) {
|
|
153
|
-
|
|
171
|
+
export async function getPoolList(client, chainIdOverride) {
|
|
172
|
+
const chainId = chainIdOverride ?? getChainId();
|
|
173
|
+
try {
|
|
174
|
+
const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
|
|
175
|
+
const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
|
|
176
|
+
if (rows.length > 0)
|
|
177
|
+
return rows;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const symbolsRes = await client.markets.getPoolSymbolAll();
|
|
183
|
+
const rows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
|
|
184
|
+
if (rows.length > 0)
|
|
185
|
+
return rows;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
}
|
|
189
|
+
return client.api?.getPoolList?.();
|
|
154
190
|
}
|
|
155
191
|
/**
|
|
156
192
|
* 获取池子分级配置
|
|
@@ -181,14 +217,14 @@ export async function resolvePool(client, poolId, keyword, chainIdOverride) {
|
|
|
181
217
|
// 2. 如果提供了 keyword,执行搜索
|
|
182
218
|
const kw = keyword?.trim();
|
|
183
219
|
if (kw) {
|
|
184
|
-
const markets = await searchMarket(client, kw, 10);
|
|
220
|
+
const markets = await searchMarket(client, kw, 10, chainId);
|
|
185
221
|
if (markets.length > 0) {
|
|
186
222
|
return markets[0].poolId;
|
|
187
223
|
}
|
|
188
224
|
}
|
|
189
225
|
// 3. 最后手段:遍历全量活跃池列表
|
|
190
226
|
if (kw || pid) {
|
|
191
|
-
const poolListRes = await
|
|
227
|
+
const poolListRes = await getPoolList(client, chainId).catch(() => null);
|
|
192
228
|
if (poolListRes) {
|
|
193
229
|
// 这里的逻辑参考 openPositionSimple 中的 collectPoolRows
|
|
194
230
|
const collect = (input) => {
|
|
@@ -2,6 +2,7 @@ import { pool, quote, base } from "@myx-trade/sdk";
|
|
|
2
2
|
import { getChainId, resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
4
4
|
import { ensureUnits } from "../utils/units.js";
|
|
5
|
+
import { normalizeAddress } from "../utils/address.js";
|
|
5
6
|
function isDivideByZeroError(message) {
|
|
6
7
|
const lower = message.toLowerCase();
|
|
7
8
|
return (lower.includes("divide_by_zero") ||
|
|
@@ -19,74 +20,74 @@ function toPositiveBigint(input) {
|
|
|
19
20
|
return null;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
async function resolvePositiveMarketPrice30(client, poolId, chainId) {
|
|
24
|
+
if (!client)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const oracle = await client.utils?.getOraclePrice?.(poolId, chainId);
|
|
28
|
+
const byValue = toPositiveBigint(oracle?.value);
|
|
29
|
+
const byPrice = toPositiveBigint(oracle?.price);
|
|
30
|
+
if (byValue)
|
|
31
|
+
return byValue;
|
|
32
|
+
if (byPrice)
|
|
33
|
+
return byPrice;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const tickerRes = await client.markets?.getTickerList?.({ chainId, poolIds: [poolId] });
|
|
39
|
+
const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
|
|
40
|
+
if (row?.price) {
|
|
41
|
+
const tickerRaw = ensureUnits(row.price, 30, "ticker price");
|
|
42
|
+
const byTicker = toPositiveBigint(tickerRaw);
|
|
43
|
+
if (byTicker)
|
|
44
|
+
return byTicker;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
22
51
|
/**
|
|
23
52
|
* 创建合约市场池子
|
|
24
53
|
*/
|
|
25
54
|
export async function createPool(baseToken, marketId) {
|
|
26
55
|
await resolveClient();
|
|
27
56
|
const chainId = getChainId();
|
|
28
|
-
return pool.createPool({ chainId, baseToken, marketId });
|
|
57
|
+
return pool.createPool({ chainId, baseToken: normalizeAddress(baseToken, "baseToken"), marketId });
|
|
29
58
|
}
|
|
30
59
|
/**
|
|
31
60
|
* 获取池子信息
|
|
32
61
|
*/
|
|
33
62
|
export async function getPoolInfo(poolId, chainIdOverride, clientOverride) {
|
|
34
63
|
const chainId = chainIdOverride ?? getChainId();
|
|
35
|
-
|
|
64
|
+
const client = clientOverride ?? (await resolveClient()).client;
|
|
36
65
|
try {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
const marketPrice30 = await resolvePositiveMarketPrice30(client, poolId, chainId);
|
|
67
|
+
if (marketPrice30 && marketPrice30 > 0n) {
|
|
68
|
+
const withPrice = await pool.getPoolInfo(chainId, poolId, marketPrice30);
|
|
69
|
+
if (withPrice)
|
|
70
|
+
return withPrice;
|
|
71
|
+
}
|
|
41
72
|
}
|
|
42
73
|
catch (error) {
|
|
43
74
|
const message = extractErrorMessage(error);
|
|
44
75
|
if (!isDivideByZeroError(message)) {
|
|
45
76
|
throw new Error(`get_pool_info failed: ${message}`);
|
|
46
77
|
}
|
|
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
78
|
}
|
|
55
|
-
let oracleRaw = 0n;
|
|
56
79
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
const direct = await pool.getPoolInfo(chainId, poolId);
|
|
81
|
+
if (direct)
|
|
82
|
+
return direct;
|
|
83
|
+
throw new Error(`Pool info for ${poolId} returned undefined.`);
|
|
61
84
|
}
|
|
62
85
|
catch (error) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.`);
|
|
86
|
+
const message = extractErrorMessage(error);
|
|
87
|
+
if (isDivideByZeroError(message)) {
|
|
88
|
+
throw new Error("get_pool_info unavailable: pool reserves are currently empty or market price context is unresolved.");
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
throw new Error(`get_pool_info failed after oracle-price retry: ${extractErrorMessage(error)}`);
|
|
90
|
+
throw new Error(`get_pool_info failed: ${message}`);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
/**
|
|
@@ -6,11 +6,33 @@ import { normalizeSlippagePct4dp } from "../utils/slippage.js";
|
|
|
6
6
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
7
7
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
8
8
|
function resolveDirection(direction) {
|
|
9
|
+
if (typeof direction === "string") {
|
|
10
|
+
const text = direction.trim().toUpperCase();
|
|
11
|
+
if (text === "0" || text === "LONG" || text === "BUY")
|
|
12
|
+
return Direction.LONG;
|
|
13
|
+
if (text === "1" || text === "SHORT" || text === "SELL")
|
|
14
|
+
return Direction.SHORT;
|
|
15
|
+
throw new Error("direction must be LONG/SHORT or 0/1.");
|
|
16
|
+
}
|
|
9
17
|
if (direction !== 0 && direction !== 1) {
|
|
10
|
-
throw new Error("direction must be
|
|
18
|
+
throw new Error("direction must be LONG/SHORT or 0/1.");
|
|
11
19
|
}
|
|
12
20
|
return direction === 0 ? Direction.LONG : Direction.SHORT;
|
|
13
21
|
}
|
|
22
|
+
function resolveDirectionIndex(direction) {
|
|
23
|
+
if (typeof direction === "string") {
|
|
24
|
+
const text = direction.trim().toUpperCase();
|
|
25
|
+
if (text === "0" || text === "LONG" || text === "BUY")
|
|
26
|
+
return 0;
|
|
27
|
+
if (text === "1" || text === "SHORT" || text === "SELL")
|
|
28
|
+
return 1;
|
|
29
|
+
throw new Error("direction must be LONG/SHORT or 0/1.");
|
|
30
|
+
}
|
|
31
|
+
if (direction !== 0 && direction !== 1) {
|
|
32
|
+
throw new Error("direction must be LONG/SHORT or 0/1.");
|
|
33
|
+
}
|
|
34
|
+
return direction;
|
|
35
|
+
}
|
|
14
36
|
/**
|
|
15
37
|
* 自动推断开启订单的触发类型 (Limit/Stop)
|
|
16
38
|
*/
|
|
@@ -71,6 +93,17 @@ function parseDecimals(value, fallback) {
|
|
|
71
93
|
function normalizeIdentifier(value) {
|
|
72
94
|
return String(value ?? "").trim().toLowerCase();
|
|
73
95
|
}
|
|
96
|
+
function toBigIntOrZero(value) {
|
|
97
|
+
try {
|
|
98
|
+
const text = String(value ?? "").trim();
|
|
99
|
+
if (!text)
|
|
100
|
+
return 0n;
|
|
101
|
+
return BigInt(text);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return 0n;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
74
107
|
async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
|
|
75
108
|
let baseDecimals = 18;
|
|
76
109
|
let quoteDecimals = getQuoteDecimals();
|
|
@@ -171,16 +204,10 @@ export async function openPosition(client, address, args) {
|
|
|
171
204
|
console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
|
|
172
205
|
const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
|
|
173
206
|
let marginBalanceRaw = BigInt(0);
|
|
174
|
-
let walletBalanceRaw = BigInt(0);
|
|
175
|
-
if (marginInfo?.code === 0) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
walletBalanceRaw = BigInt(marginInfo.data[1] || "0");
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
marginBalanceRaw = BigInt(marginInfo.data?.marginBalance || "0");
|
|
182
|
-
walletBalanceRaw = BigInt(marginInfo.data?.availableMargin || "0");
|
|
183
|
-
}
|
|
207
|
+
let walletBalanceRaw = BigInt(0);
|
|
208
|
+
if (marginInfo?.code === 0 && marginInfo?.data) {
|
|
209
|
+
marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
|
|
210
|
+
walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
184
211
|
}
|
|
185
212
|
const requiredRaw = BigInt(collateralRaw);
|
|
186
213
|
if (marginBalanceRaw < requiredRaw) {
|
|
@@ -191,15 +218,15 @@ export async function openPosition(client, address, args) {
|
|
|
191
218
|
const neededRaw = requiredRaw - marginBalanceRaw;
|
|
192
219
|
console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
|
|
193
220
|
if (walletBalanceRaw < neededRaw) {
|
|
194
|
-
// Also check real wallet balance just in case
|
|
221
|
+
// Also check real wallet balance just in case account info wallet field is stale.
|
|
195
222
|
const realWalletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
196
223
|
const realWalletRaw = BigInt(realWalletRes?.data || "0");
|
|
197
224
|
if (realWalletRaw < neededRaw) {
|
|
198
|
-
throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) +
|
|
225
|
+
throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + walletBalance (${walletBalanceRaw.toString()}) + realWallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
|
|
199
226
|
}
|
|
200
227
|
walletBalanceRaw = realWalletRaw;
|
|
201
228
|
}
|
|
202
|
-
console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet
|
|
229
|
+
console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet...`);
|
|
203
230
|
const depositRaw = await client.account.deposit({
|
|
204
231
|
amount: neededRaw.toString(),
|
|
205
232
|
tokenAddress: poolData.quoteToken,
|
|
@@ -276,13 +303,13 @@ export async function closePosition(client, address, args) {
|
|
|
276
303
|
/**
|
|
277
304
|
* 设置止盈止损
|
|
278
305
|
*/
|
|
279
|
-
export async function setPositionTpSl(client, address, args) {
|
|
280
|
-
const chainId = getChainId();
|
|
281
|
-
const executionFeeToken = normalizeAddress(args.executionFeeToken, "executionFeeToken");
|
|
306
|
+
export async function setPositionTpSl(client, address, args, chainIdOverride) {
|
|
307
|
+
const chainId = chainIdOverride ?? getChainId();
|
|
282
308
|
if (!args.tpPrice && !args.slPrice) {
|
|
283
309
|
throw new Error("At least one of tpPrice or slPrice must be provided.");
|
|
284
310
|
}
|
|
285
311
|
const dir = resolveDirection(args.direction);
|
|
312
|
+
const dirIndex = resolveDirectionIndex(args.direction);
|
|
286
313
|
// Fetch pool detail for decimals
|
|
287
314
|
const poolResponse = await client.markets.getMarketDetail({ chainId, poolId: args.poolId });
|
|
288
315
|
const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
|
|
@@ -290,6 +317,7 @@ export async function setPositionTpSl(client, address, args) {
|
|
|
290
317
|
throw new Error(`Could not find pool metadata for ID: ${args.poolId}`);
|
|
291
318
|
}
|
|
292
319
|
const baseDecimals = poolData.baseDecimals || 18;
|
|
320
|
+
const executionFeeToken = normalizeAddress(args.executionFeeToken || poolData.quoteToken || getQuoteToken(), "executionFeeToken");
|
|
293
321
|
const params = {
|
|
294
322
|
chainId,
|
|
295
323
|
address,
|
|
@@ -298,8 +326,8 @@ export async function setPositionTpSl(client, address, args) {
|
|
|
298
326
|
direction: dir,
|
|
299
327
|
leverage: args.leverage,
|
|
300
328
|
executionFeeToken,
|
|
301
|
-
tpTriggerType: resolveTpSlTriggerType(true,
|
|
302
|
-
slTriggerType: resolveTpSlTriggerType(false,
|
|
329
|
+
tpTriggerType: resolveTpSlTriggerType(true, dirIndex, args.tpTriggerType),
|
|
330
|
+
slTriggerType: resolveTpSlTriggerType(false, dirIndex, args.slTriggerType),
|
|
303
331
|
slippagePct: normalizeSlippagePct4dp(args.slippagePct),
|
|
304
332
|
};
|
|
305
333
|
if (args.tpPrice)
|
|
@@ -408,8 +436,8 @@ export async function closeAllPositions(client, address) {
|
|
|
408
436
|
/**
|
|
409
437
|
* 更新止盈止损订单
|
|
410
438
|
*/
|
|
411
|
-
export async function updateOrderTpSl(client, address, args) {
|
|
412
|
-
const chainId = getChainId();
|
|
439
|
+
export async function updateOrderTpSl(client, address, args, chainIdOverride) {
|
|
440
|
+
const chainId = chainIdOverride ?? getChainId();
|
|
413
441
|
const quoteToken = normalizeAddress(args.quoteToken, "quoteToken");
|
|
414
442
|
const marketId = String(args.marketId ?? "").trim();
|
|
415
443
|
const isTpSlOrder = typeof args.isTpSlOrder === "boolean" ? args.isTpSlOrder : true;
|
|
@@ -432,14 +460,14 @@ export async function updateOrderTpSl(client, address, args) {
|
|
|
432
460
|
}
|
|
433
461
|
const message = extractErrorMessage(result, "Failed to update order");
|
|
434
462
|
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
|
|
463
|
+
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 manage_tp_sl on the position.`);
|
|
436
464
|
}
|
|
437
465
|
throw new Error(`update_order_tp_sl failed: ${message}`);
|
|
438
466
|
}
|
|
439
467
|
catch (error) {
|
|
440
468
|
const message = extractErrorMessage(error, "Failed to update order");
|
|
441
469
|
if (/failed to update order/i.test(message)) {
|
|
442
|
-
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
|
|
470
|
+
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 manage_tp_sl on the position.`);
|
|
443
471
|
}
|
|
444
472
|
throw new Error(message);
|
|
445
473
|
}
|