@michaleffffff/mcp-trading-server 3.0.7 → 3.0.15

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.
@@ -1,10 +1,12 @@
1
1
  import { Direction, OrderType, TriggerType } from "@myx-trade/sdk";
2
+ import { formatUnits } from "ethers";
2
3
  import { getChainId, getQuoteToken, getQuoteDecimals } from "../auth/resolveClient.js";
3
4
  import { ensureUnits } from "../utils/units.js";
4
5
  import { normalizeAddress } from "../utils/address.js";
5
6
  import { normalizeSlippagePct4dp } from "../utils/slippage.js";
6
7
  import { finalizeMutationResult } from "../utils/mutationResult.js";
7
8
  import { extractErrorMessage } from "../utils/errorMessage.js";
9
+ import { mapTimeInForce } from "../utils/mappings.js";
8
10
  function resolveDirection(direction) {
9
11
  if (typeof direction === "string") {
10
12
  const text = direction.trim().toUpperCase();
@@ -104,6 +106,54 @@ function toBigIntOrZero(value) {
104
106
  return 0n;
105
107
  }
106
108
  }
109
+ const ORDER_VALUE_SCALE = 1000000n;
110
+ const DECIMAL_INPUT_RE = /^\d+(\.\d+)?$/;
111
+ function parseScaledDecimal(value, scale, label) {
112
+ const text = String(value ?? "").trim();
113
+ if (!text)
114
+ throw new Error(`${label} is required.`);
115
+ if (!DECIMAL_INPUT_RE.test(text)) {
116
+ throw new Error(`${label} must be numeric.`);
117
+ }
118
+ const [integerPart, fractionPart = ""] = text.split(".");
119
+ const normalizedFraction = fractionPart.slice(0, scale).padEnd(scale, "0");
120
+ return BigInt(`${integerPart}${normalizedFraction}`);
121
+ }
122
+ function absBigInt(value) {
123
+ return value < 0n ? -value : value;
124
+ }
125
+ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimals) {
126
+ const numerator = sizeRaw * priceRaw30 * (10n ** BigInt(quoteDecimals));
127
+ const denominator = 10n ** BigInt(baseDecimals + 30);
128
+ return numerator / denominator;
129
+ }
130
+ function computeRecommendedSizeRaw(targetQuoteRaw, priceRaw30, baseDecimals, quoteDecimals) {
131
+ const numerator = targetQuoteRaw * (10n ** BigInt(baseDecimals + 30));
132
+ const denominator = priceRaw30 * (10n ** BigInt(quoteDecimals));
133
+ return numerator / denominator;
134
+ }
135
+ function validateIncreaseOrderEconomics(args) {
136
+ const collateralRawBig = BigInt(args.collateralRaw);
137
+ const sizeRawBig = BigInt(args.sizeRaw);
138
+ const priceRawBig = BigInt(args.priceRaw30);
139
+ if (collateralRawBig <= 0n || sizeRawBig <= 0n || priceRawBig <= 0n)
140
+ return;
141
+ const leverageScaled = parseScaledDecimal(args.leverage, 6, "leverage");
142
+ const targetQuoteRaw = (collateralRawBig * leverageScaled) / ORDER_VALUE_SCALE;
143
+ const actualQuoteRaw = computeQuoteNotionalRaw(sizeRawBig, priceRawBig, args.baseDecimals, args.quoteDecimals);
144
+ const deltaRaw = absBigInt(actualQuoteRaw - targetQuoteRaw);
145
+ const minToleranceRaw = args.quoteDecimals >= 2 ? 10n ** BigInt(args.quoteDecimals - 2) : 1n;
146
+ const pctToleranceRaw = targetQuoteRaw / 100n;
147
+ const toleranceRaw = pctToleranceRaw > minToleranceRaw ? pctToleranceRaw : minToleranceRaw;
148
+ if (deltaRaw <= toleranceRaw)
149
+ return;
150
+ const recommendedSizeRaw = computeRecommendedSizeRaw(targetQuoteRaw, priceRawBig, args.baseDecimals, args.quoteDecimals);
151
+ const targetHuman = formatUnits(targetQuoteRaw, args.quoteDecimals);
152
+ const actualHuman = formatUnits(actualQuoteRaw, args.quoteDecimals);
153
+ const recommendedSizeHuman = formatUnits(recommendedSizeRaw, args.baseDecimals);
154
+ const priceHuman = formatUnits(priceRawBig, 30);
155
+ throw new Error(`Invalid size semantics: size is BASE quantity, not USD notional. collateralAmount*leverage implies ≈${targetHuman} quote, but size*price implies ≈${actualHuman} quote. At price ${priceHuman}, recommended size is ≈${recommendedSizeHuman}.`);
156
+ }
107
157
  async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
108
158
  let baseDecimals = 18;
109
159
  let quoteDecimals = getQuoteDecimals();
@@ -199,6 +249,15 @@ export async function openPosition(client, address, args) {
199
249
  const sizeRaw = ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false });
200
250
  const priceRaw = ensureUnits(args.price, 30, "price", { allowImplicitRaw: false });
201
251
  const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee", { allowImplicitRaw: false });
252
+ validateIncreaseOrderEconomics({
253
+ collateralRaw,
254
+ sizeRaw,
255
+ priceRaw30: priceRaw,
256
+ leverage: args.leverage,
257
+ baseDecimals,
258
+ quoteDecimals,
259
+ });
260
+ const timeInForce = mapTimeInForce(args.timeInForce);
202
261
  // --- Auto-Deposit Logic (Strict) ---
203
262
  const allowAutoDeposit = args.autoDeposit !== false;
204
263
  console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
@@ -247,7 +306,7 @@ export async function openPosition(client, address, args) {
247
306
  collateralAmount: collateralRaw,
248
307
  size: sizeRaw,
249
308
  price: priceRaw,
250
- timeInForce: args.timeInForce,
309
+ timeInForce,
251
310
  postOnly: args.postOnly,
252
311
  slippagePct: normalizeSlippagePct4dp(args.slippagePct),
253
312
  executionFeeToken,
@@ -282,6 +341,7 @@ export async function closePosition(client, address, args) {
282
341
  }
283
342
  const baseDecimals = poolData.baseDecimals || 18;
284
343
  const quoteDecimals = poolData.quoteDecimals || 6;
344
+ const timeInForce = mapTimeInForce(args.timeInForce);
285
345
  return client.order.createDecreaseOrder({
286
346
  chainId,
287
347
  address,
@@ -293,7 +353,7 @@ export async function closePosition(client, address, args) {
293
353
  collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount", { allowImplicitRaw: false }),
294
354
  size: ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false }),
295
355
  price: ensureUnits(args.price, 30, "price", { allowImplicitRaw: false }),
296
- timeInForce: args.timeInForce,
356
+ timeInForce,
297
357
  postOnly: args.postOnly,
298
358
  slippagePct: normalizeSlippagePct4dp(args.slippagePct),
299
359
  executionFeeToken,
@@ -41,7 +41,7 @@ export const closePositionTool = {
41
41
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human/raw). Also supports ALL/FULL/MAX to use live position collateral raw."),
42
42
  size: z.union([z.string(), z.number()]).describe("Position size (human/raw). Also supports ALL/FULL/MAX for exact full-close raw size."),
43
43
  price: z.union([z.string(), z.number()]).describe("Price (human or 30-dec raw units)"),
44
- timeInForce: z.coerce.number().int().describe("TimeInForce: 0=GTC, 1=IOC, 2=FOK"),
44
+ timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.2 supports IOC only. Use 0 or 'IOC'."),
45
45
  postOnly: z.coerce.boolean().describe("Post-only flag"),
46
46
  slippagePct: z.coerce.string().default("50").describe(SLIPPAGE_PCT_4DP_DESC),
47
47
  executionFeeToken: z.string().describe("Execution fee token address"),
@@ -1,7 +1,115 @@
1
1
  import { z } from "zod";
2
- import { createPool } from "../services/poolService.js";
3
- import { resolveClient } from "../auth/resolveClient.js";
2
+ import { createPool, getMarketPoolByBaseToken } from "../services/poolService.js";
3
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
+ import { normalizeAddress } from "../utils/address.js";
6
+ import { extractErrorMessage } from "../utils/errorMessage.js";
7
+ import { decodeErrorSelector } from "../utils/errors.js";
8
+ const MARKET_ID_RE = /^0x[0-9a-fA-F]{64}$/;
9
+ function compactMessage(message) {
10
+ const flat = String(message ?? "").replace(/\s+/g, " ").trim();
11
+ if (!flat)
12
+ return "Unknown create_perp_market error.";
13
+ if (flat.length <= 420)
14
+ return flat;
15
+ return `${flat.slice(0, 420)}...`;
16
+ }
17
+ function extractSelectorCandidate(input) {
18
+ const queue = [input];
19
+ const visited = new Set();
20
+ const LONG_HEX_RE = /0x[0-9a-fA-F]{8,}/g;
21
+ const EXACT_SELECTOR_RE = /0x[0-9a-fA-F]{8}\b/g;
22
+ const KEYED_SELECTOR_RE = /\b(?:data|selector|error|revert)\s*[:=]\s*["']?(0x[0-9a-fA-F]{8,})/gi;
23
+ let fallback = null;
24
+ while (queue.length > 0) {
25
+ const current = queue.shift();
26
+ if (current === null || current === undefined)
27
+ continue;
28
+ if (visited.has(current))
29
+ continue;
30
+ visited.add(current);
31
+ if (typeof current === "string") {
32
+ const exactMatches = current.match(EXACT_SELECTOR_RE) ?? [];
33
+ for (const candidate of exactMatches) {
34
+ const normalized = candidate.toLowerCase();
35
+ if (decodeErrorSelector(normalized))
36
+ return normalized;
37
+ if (!fallback)
38
+ fallback = normalized;
39
+ }
40
+ for (const match of current.matchAll(KEYED_SELECTOR_RE)) {
41
+ const raw = String(match[1] ?? "").toLowerCase();
42
+ if (raw.length < 10)
43
+ continue;
44
+ const selector = `0x${raw.slice(2, 10)}`;
45
+ if (decodeErrorSelector(selector))
46
+ return selector;
47
+ if (!fallback)
48
+ fallback = selector;
49
+ }
50
+ const matches = current.match(LONG_HEX_RE) ?? [];
51
+ for (const hex of matches) {
52
+ const normalized = hex.toLowerCase();
53
+ if (normalized.length === 42)
54
+ continue; // likely plain address
55
+ if (normalized.length < 10)
56
+ continue;
57
+ const selector = `0x${normalized.slice(2, 10)}`;
58
+ if (decodeErrorSelector(selector)) {
59
+ return selector;
60
+ }
61
+ if (!fallback)
62
+ fallback = selector;
63
+ }
64
+ continue;
65
+ }
66
+ if (typeof current !== "object")
67
+ continue;
68
+ const record = current;
69
+ for (const value of Object.values(record)) {
70
+ queue.push(value);
71
+ }
72
+ }
73
+ return fallback;
74
+ }
75
+ function buildCreateMarketErrorPayload(args, messageLike) {
76
+ const message = compactMessage(extractErrorMessage(messageLike, "create_perp_market failed."));
77
+ const selectorCandidate = extractSelectorCandidate(messageLike);
78
+ const decodedSelector = selectorCandidate ? decodeErrorSelector(selectorCandidate) : null;
79
+ const lower = message.toLowerCase();
80
+ const code = lower.includes("marketid") && lower.includes("66")
81
+ ? "INVALID_PARAM"
82
+ : lower.includes("not a valid evm address")
83
+ ? "INVALID_PARAM"
84
+ : lower.includes("abi") && lower.includes("size")
85
+ ? "INVALID_PARAM"
86
+ : lower.includes("reverted")
87
+ ? "CONTRACT_REVERT"
88
+ : "TOOL_EXECUTION_ERROR";
89
+ const decoratedMessage = decodedSelector
90
+ ? `${message} (Decoded Contract Error: ${decodedSelector})`
91
+ : message;
92
+ const hint = code === "INVALID_PARAM"
93
+ ? "Use a valid baseToken address and a 66-char marketId config hash (0x + 64 hex)."
94
+ : "Check market allocation / permissions / duplicate pool status, then retry.";
95
+ return {
96
+ status: "error",
97
+ error: {
98
+ tool: "create_perp_market",
99
+ code,
100
+ message: decoratedMessage,
101
+ hint,
102
+ action: "Adjust parameters or prerequisites and retry.",
103
+ details: {
104
+ chainId: getChainId(),
105
+ baseToken: args?.baseToken ?? null,
106
+ marketId: args?.marketId ?? null,
107
+ selector: selectorCandidate,
108
+ decodedSelector,
109
+ },
110
+ },
111
+ };
112
+ }
5
113
  export const createPerpMarketTool = {
6
114
  name: "create_perp_market",
7
115
  description: "[LIQUIDITY] Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use get_pool_metadata (after find_pool/list_pools) to fetch an existing marketId if you don't have a specific newly allocated one.",
@@ -13,13 +121,32 @@ export const createPerpMarketTool = {
13
121
  try {
14
122
  if (!args.baseToken || !args.marketId)
15
123
  throw new Error("baseToken and marketId are required.");
124
+ const baseToken = normalizeAddress(args.baseToken, "baseToken");
125
+ const marketId = String(args.marketId).trim();
126
+ if (!MARKET_ID_RE.test(marketId)) {
127
+ throw new Error(`marketId must be a 66-character config hash (0x + 64 hex). Received: ${marketId}`);
128
+ }
16
129
  const { signer } = await resolveClient();
17
- const raw = await createPool(args.baseToken, args.marketId);
130
+ const raw = await createPool(baseToken, marketId);
18
131
  const data = await finalizeMutationResult(raw, signer, "create_perp_market");
19
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
132
+ let onChainPool = null;
133
+ try {
134
+ const resolved = await getMarketPoolByBaseToken(marketId, baseToken, getChainId());
135
+ if (resolved?.poolId && !/^0x0{64}$/i.test(String(resolved.poolId))) {
136
+ onChainPool = resolved;
137
+ }
138
+ }
139
+ catch {
140
+ onChainPool = null;
141
+ }
142
+ const payload = onChainPool
143
+ ? { ...data, onChainPool }
144
+ : data;
145
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
20
146
  }
21
147
  catch (error) {
22
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
148
+ const payload = buildCreateMarketErrorPayload(args, error);
149
+ return { content: [{ type: "text", text: JSON.stringify(payload, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
23
150
  }
24
151
  },
25
152
  };
@@ -23,7 +23,7 @@ export const executeTradeTool = {
23
23
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
24
24
  size: z.union([z.string(), z.number()]).describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
25
25
  price: z.union([z.string(), z.number()]).describe("Execution or Limit price. e.g. '65000' or 'raw:...'"),
26
- timeInForce: z.coerce.number().int().describe("0=GTC, 1=IOC, 2=FOK"),
26
+ timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.2 supports IOC only. Use 0 or 'IOC'."),
27
27
  postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
28
28
  slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
29
29
  executionFeeToken: z.string().optional().describe("Address of token to pay gas/execution fees (typically USDC). Default is pool quoteToken."),
@@ -50,6 +50,11 @@ export const executeTradeTool = {
50
50
  const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
51
51
  if (!poolData)
52
52
  throw new Error(`Could not find pool metadata for ID: ${poolId}`);
53
+ const resolvedMarketId = String(poolData.marketId ?? "").trim();
54
+ const requestedMarketId = String(args.marketId ?? "").trim();
55
+ if (resolvedMarketId && requestedMarketId && resolvedMarketId.toLowerCase() !== requestedMarketId.toLowerCase()) {
56
+ throw new Error(`Invalid marketId: marketId mismatch for poolId=${poolId}. Provided=${requestedMarketId}, resolved=${resolvedMarketId}.`);
57
+ }
53
58
  const baseDecimals = Number(poolData.baseDecimals ?? 18);
54
59
  const quoteDecimals = Number(poolData.quoteDecimals ?? 6);
55
60
  const mappedDirection = mapDirection(args.direction);
@@ -1,5 +1,106 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { normalizeAddress } from "../utils/address.js";
4
+ import { extractErrorMessage } from "../utils/errorMessage.js";
5
+ function collectRows(input) {
6
+ if (Array.isArray(input))
7
+ return input.flatMap(collectRows);
8
+ if (!input || typeof input !== "object")
9
+ return [];
10
+ if (input.poolId || input.pool_id || input.marketId || input.market_id)
11
+ return [input];
12
+ return Object.values(input).flatMap(collectRows);
13
+ }
14
+ function isNonEmptyObject(value) {
15
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
16
+ }
17
+ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
18
+ let rows = [];
19
+ try {
20
+ const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
21
+ rows = collectRows(searchRes?.data ?? searchRes);
22
+ }
23
+ catch {
24
+ rows = [];
25
+ }
26
+ if (rows.length === 0) {
27
+ try {
28
+ const poolListRes = await client.api?.getPoolList?.();
29
+ rows = collectRows(poolListRes?.data ?? poolListRes);
30
+ }
31
+ catch {
32
+ rows = [];
33
+ }
34
+ }
35
+ const normalized = baseAddress.toLowerCase();
36
+ const matched = rows.find((row) => String(row?.baseToken ?? row?.baseAddress ?? "").trim().toLowerCase() === normalized);
37
+ if (!matched)
38
+ return null;
39
+ const poolId = String(matched?.poolId ?? matched?.pool_id ?? "").trim();
40
+ if (!poolId) {
41
+ return {
42
+ chainId,
43
+ baseAddress,
44
+ baseToken: baseAddress,
45
+ baseSymbol: matched?.baseSymbol ?? null,
46
+ baseDecimals: matched?.baseDecimals ?? matched?.base_decimals ?? null,
47
+ baseTokenIcon: matched?.baseTokenIcon ?? matched?.base_token_icon ?? null,
48
+ marketId: matched?.marketId ?? matched?.market_id ?? null,
49
+ poolId: null,
50
+ source: "market_search_fallback",
51
+ };
52
+ }
53
+ try {
54
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
55
+ const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
56
+ if (detail) {
57
+ return {
58
+ chainId,
59
+ baseAddress,
60
+ baseToken: detail.baseToken ?? baseAddress,
61
+ baseSymbol: detail.baseSymbol ?? matched?.baseSymbol ?? null,
62
+ baseDecimals: detail.baseDecimals ?? matched?.baseDecimals ?? matched?.base_decimals ?? null,
63
+ baseTokenIcon: detail.baseTokenIcon ?? matched?.baseTokenIcon ?? null,
64
+ quoteSymbol: detail.quoteSymbol ?? matched?.quoteSymbol ?? null,
65
+ marketId: detail.marketId ?? matched?.marketId ?? null,
66
+ poolId: detail.poolId ?? poolId,
67
+ source: "market_detail_fallback",
68
+ };
69
+ }
70
+ }
71
+ catch {
72
+ }
73
+ return {
74
+ chainId,
75
+ baseAddress,
76
+ baseToken: baseAddress,
77
+ baseSymbol: matched?.baseSymbol ?? null,
78
+ baseDecimals: matched?.baseDecimals ?? matched?.base_decimals ?? null,
79
+ baseTokenIcon: matched?.baseTokenIcon ?? matched?.base_token_icon ?? null,
80
+ quoteSymbol: matched?.quoteSymbol ?? null,
81
+ marketId: matched?.marketId ?? matched?.market_id ?? null,
82
+ poolId,
83
+ source: "market_search_fallback",
84
+ };
85
+ }
86
+ function buildReadErrorPayload(args, messageLike, code = "SDK_READ_ERROR") {
87
+ const chainId = args.chainId ?? getChainId();
88
+ const message = extractErrorMessage(messageLike, "Failed to read base token detail.");
89
+ return {
90
+ status: "error",
91
+ error: {
92
+ tool: "get_base_detail",
93
+ code,
94
+ message,
95
+ hint: "Check baseAddress validity and market availability on current chain.",
96
+ action: "Use list_pools/find_pool/get_pool_metadata to confirm base token, then retry.",
97
+ details: {
98
+ chainId,
99
+ baseAddress: args.baseAddress ?? null,
100
+ },
101
+ },
102
+ };
103
+ }
3
104
  export const getBaseDetailTool = {
4
105
  name: "get_base_detail",
5
106
  description: "[MARKET] Get base token details.",
@@ -11,11 +112,36 @@ export const getBaseDetailTool = {
11
112
  try {
12
113
  const { client } = await resolveClient();
13
114
  const chainId = args.chainId ?? getChainId();
14
- const result = await client.markets.getBaseDetail({ chainId, baseAddress: args.baseAddress });
15
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
115
+ const baseAddress = normalizeAddress(args.baseAddress, "baseAddress");
116
+ const result = await client.markets.getBaseDetail({ chainId, baseAddress });
117
+ const hasCode = !!result && typeof result === "object" && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, "code");
118
+ const code = hasCode ? Number(result.code) : 0;
119
+ const payload = hasCode ? result.data : result;
120
+ if (hasCode && Number.isFinite(code) && code !== 0) {
121
+ const body = buildReadErrorPayload({ ...args, baseAddress }, result.msg ?? result.message ?? result, "SDK_READ_ERROR");
122
+ return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
123
+ }
124
+ if (payload === null || payload === undefined) {
125
+ const fallback = await findBaseDetailFromMarkets(client, chainId, baseAddress);
126
+ if (fallback) {
127
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: fallback }, null, 2) }] };
128
+ }
129
+ const body = buildReadErrorPayload({ ...args, baseAddress }, "get_base_detail returned empty data.", "NOT_FOUND");
130
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
131
+ }
132
+ if (typeof payload === "object" && !Array.isArray(payload) && !isNonEmptyObject(payload)) {
133
+ const fallback = await findBaseDetailFromMarkets(client, chainId, baseAddress);
134
+ if (fallback) {
135
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: fallback }, null, 2) }] };
136
+ }
137
+ const body = buildReadErrorPayload({ ...args, baseAddress }, "get_base_detail returned an empty object.", "NOT_FOUND");
138
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
139
+ }
140
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
16
141
  }
17
142
  catch (error) {
18
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
143
+ const body = buildReadErrorPayload(args, error, "TOOL_EXECUTION_ERROR");
144
+ return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
19
145
  }
20
146
  },
21
147
  };
@@ -1,6 +1,34 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getOrderTypeDesc, getOrderStatusDesc, getDirectionDesc, getHistoryOrderStatusDesc, getExecTypeDesc } from "../utils/mappings.js";
4
+ function pickFirstDefined(...values) {
5
+ for (const value of values) {
6
+ if (value !== undefined && value !== null && String(value).trim() !== "") {
7
+ return value;
8
+ }
9
+ }
10
+ return undefined;
11
+ }
12
+ function toMaybeNumber(value) {
13
+ if (value === undefined || value === null || value === "")
14
+ return undefined;
15
+ const numeric = Number(value);
16
+ return Number.isFinite(numeric) ? numeric : undefined;
17
+ }
18
+ function resolveOpenOrderStatus(order) {
19
+ const statusRaw = toMaybeNumber(pickFirstDefined(order?.status, order?.orderStatus, order?.order_status));
20
+ if (statusRaw === undefined) {
21
+ return { statusRaw: undefined, statusDesc: "Open" };
22
+ }
23
+ return { statusRaw, statusDesc: getOrderStatusDesc(statusRaw) };
24
+ }
25
+ function resolveHistoryOrderStatus(order) {
26
+ const orderStatusRaw = toMaybeNumber(pickFirstDefined(order?.orderStatus, order?.order_status, order?.status));
27
+ return {
28
+ orderStatusRaw,
29
+ orderStatusDesc: getHistoryOrderStatusDesc(orderStatusRaw),
30
+ };
31
+ }
4
32
  export const getOrdersTool = {
5
33
  name: "get_orders",
6
34
  description: "[ACCOUNT] Get orders (open or history) with optional filters.",
@@ -19,7 +47,7 @@ export const getOrdersTool = {
19
47
  results.open = (openRes?.data || []).map((order) => ({
20
48
  ...order,
21
49
  orderTypeDesc: getOrderTypeDesc(order.orderType),
22
- statusDesc: getOrderStatusDesc(order.status),
50
+ ...resolveOpenOrderStatus(order),
23
51
  directionDesc: getDirectionDesc(order.direction)
24
52
  }));
25
53
  if (args.poolId) {
@@ -32,7 +60,7 @@ export const getOrdersTool = {
32
60
  results.history = (historyRes?.data || []).map((order) => ({
33
61
  ...order,
34
62
  orderTypeDesc: getOrderTypeDesc(order.orderType),
35
- orderStatusDesc: getHistoryOrderStatusDesc(order.orderStatus),
63
+ ...resolveHistoryOrderStatus(order),
36
64
  directionDesc: getDirectionDesc(order.direction),
37
65
  execTypeDesc: getExecTypeDesc(order.execType)
38
66
  }));