@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +42 -6
  2. package/README.md +49 -414
  3. package/TOOL_EXAMPLES.md +63 -559
  4. package/dist/auth/resolveClient.js +19 -2
  5. package/dist/prompts/tradingGuide.js +16 -23
  6. package/dist/server.js +20 -2
  7. package/dist/services/balanceService.js +4 -4
  8. package/dist/services/marketService.js +48 -12
  9. package/dist/services/poolService.js +45 -44
  10. package/dist/services/tradeService.js +51 -23
  11. package/dist/tools/accountInfo.js +5 -134
  12. package/dist/tools/accountTransfer.js +7 -12
  13. package/dist/tools/adjustMargin.js +1 -1
  14. package/dist/tools/cancelOrders.js +71 -0
  15. package/dist/tools/checkAccountReady.js +75 -0
  16. package/dist/tools/checkApproval.js +1 -1
  17. package/dist/tools/closeAllPositions.js +9 -7
  18. package/dist/tools/closePosition.js +18 -11
  19. package/dist/tools/createPerpMarket.js +1 -1
  20. package/dist/tools/executeTrade.js +6 -6
  21. package/dist/tools/findPool.js +26 -0
  22. package/dist/tools/getAccountSnapshot.js +47 -0
  23. package/dist/tools/getAllTickers.js +5 -4
  24. package/dist/tools/getBaseDetail.js +1 -1
  25. package/dist/tools/getKline.js +7 -4
  26. package/dist/tools/getMyLpHoldings.js +3 -2
  27. package/dist/tools/getNetworkFee.js +1 -1
  28. package/dist/tools/getOrders.js +46 -0
  29. package/dist/tools/getPoolMetadata.js +95 -0
  30. package/dist/tools/getPositionsAll.js +80 -0
  31. package/dist/tools/{getMarketPrice.js → getPrice.js} +8 -5
  32. package/dist/tools/getUserTradingFeeRate.js +66 -3
  33. package/dist/tools/index.js +15 -19
  34. package/dist/tools/listPools.js +53 -0
  35. package/dist/tools/manageLiquidity.js +4 -4
  36. package/dist/tools/manageTpSl.js +234 -0
  37. package/dist/tools/openPositionSimple.js +27 -8
  38. package/dist/tools/searchTools.js +35 -0
  39. package/dist/utils/mappings.js +10 -7
  40. package/package.json +2 -2
  41. package/dist/tools/cancelAllOrders.js +0 -57
  42. package/dist/tools/cancelOrder.js +0 -22
  43. package/dist/tools/getAccountVipInfo.js +0 -20
  44. package/dist/tools/getKlineLatestBar.js +0 -28
  45. package/dist/tools/getOraclePrice.js +0 -22
  46. package/dist/tools/getPoolList.js +0 -17
  47. package/dist/tools/getPoolSymbolAll.js +0 -16
  48. package/dist/tools/getPositions.js +0 -77
  49. package/dist/tools/marketInfo.js +0 -88
  50. package/dist/tools/orderQueries.js +0 -51
  51. package/dist/tools/poolConfig.js +0 -22
  52. package/dist/tools/positionHistory.js +0 -28
  53. package/dist/tools/searchMarket.js +0 -21
  54. package/dist/tools/setTpSl.js +0 -50
  55. 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: process.env.IS_TESTNET !== "false",
50
- walletClient: { transport: globalThis.window.ethereum }
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.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 \`search_market\` with a keyword (or empty keyword) to find active \`poolId\` values. If needed, fallback to \`get_pool_list\`.
21
- 2. **Context**: Use \`get_market_price\` and \`get_account\` (with \`poolId\`) to check market state, wallet balance, and trading-account margin.
22
- 3. **Execution**: Prefer \`open_position_simple\` for new trades. It handles unit conversions and pool resolution automatically.
23
- 4. **Validation**: Always check the \`verification.verified\` flag in the output. If \`false\`, read the \`cancelReason\` to explain the failure to the user.
24
- 5. **Limit Order Loop**: For limit orders, always follow \`execute_trade -> get_open_orders -> get_order_history -> cancel_order/cancel_all_orders\`.
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
- - **Position IDs**: When opening a NEW position, \`positionId\` MUST be an empty string \`""\`.
28
- - **Decimals**: Human-readable units (e.g., "0.1" BTC) are default for \`open_position_simple\`. SDK-native tools often require raw units; use the \`raw:\` prefix if you need forced precision.
29
- - **Unit Prefixes**: For mutation tools, prefer explicit prefixes: \`human:\` for readable amounts, \`raw:\` for exact on-chain units.
30
- - **Slippage**: Default is 100 (1%). For volatile meme tokens, consider 200-300 (2-3%).
31
- - **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
32
- - **execute_trade**: Has built-in preflight normalization and can auto-compute \`tradingFee\` when omitted.
33
- - **Balances**: Use \`get_account\` to clearly separate wallet balance vs trading-account balance (pass \`poolId\` for trading-account metrics).
34
- - **LP Holdings**: Use \`get_my_lp_holdings\` to scan base/quote LP token balances across pools on the current chain; naming convention is \`baseLpAssetName=mBASE.QUOTE\`, \`quoteLpAssetName=mQUOTE.BASE\` (e.g., \`mBTC.USDC\`, \`mUSDC.BTC\`).
35
- - **Liquidity Mutation Output**: \`manage_liquidity\` success output includes \`data.lpAssetNames\` with \`baseLpAssetName\`, \`quoteLpAssetName\`, and \`operatedLpAssetName\`.
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.4" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
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.4 running (stdio, pure on-chain, prod ready)");
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
- const marketListRes = await client.api.getMarketList().catch(() => null);
56
- const marketRows = extractMarketRows(marketListRes, getChainId());
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.getPoolList().catch(() => null);
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
- return client.api.getPoolList();
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 client.api.getPoolList().catch(() => null);
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
- let needOracleFallback = false;
64
+ const client = clientOverride ?? (await resolveClient()).client;
36
65
  try {
37
- const direct = await pool.getPoolInfo(chainId, poolId);
38
- if (direct)
39
- return direct;
40
- needOracleFallback = true;
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 oracle = await client.utils.getOraclePrice(poolId, chainId);
58
- const byValue = toPositiveBigint(oracle?.value);
59
- const byPrice = toPositiveBigint(oracle?.price);
60
- oracleRaw = byValue ?? byPrice ?? 0n;
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
- 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.`);
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
- return retried;
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 0 (LONG) or 1 (SHORT).");
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); // Using availableMargin as wallet balance per user
175
- if (marginInfo?.code === 0) {
176
- if (Array.isArray(marginInfo.data)) {
177
- marginBalanceRaw = BigInt(marginInfo.data[0] || "0");
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 user's availableMargin is truly empty
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()}) + availableMargin (as wallet: ${walletBalanceRaw.toString()}) + real wallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.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 (availableMargin)...`);
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, args.direction, args.tpTriggerType),
302
- slTriggerType: resolveTpSlTriggerType(false, args.direction, args.slTriggerType),
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 set_tp_sl on the position.`);
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 set_tp_sl on the position.`);
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
  }