@michaleffffff/mcp-trading-server 3.0.16 → 3.0.21

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 CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.21 - 2026-03-19
4
+
5
+ ### Fixed
6
+ - Hardened `get_pool_metadata` funding-rate formatting:
7
+ - `fundingInfo.nextFundingRate` now preserves readable `%/秒` and `%/天` output for negative rates too.
8
+ - Regression coverage now asserts both numeric funding-rate views and display strings.
9
+
10
+ ### Changed
11
+ - Refreshed operator-facing docs and prompts to match the latest trading safety behavior:
12
+ - `README.md` now documents Oracle-only execution, exact-approval defaults, base-size semantics, TP/SL semantic checks, and LP preview fail-close.
13
+ - `mcp_config_guide.md` now includes required `BROKER_ADDRESS` configuration and testnet `MYXBroker` references.
14
+ - `TOOL_EXAMPLES.md` now reflects fresh-Oracle execution, live-direction validation, human-price TP/SL parsing, and trading `slippagePct` conventions.
15
+ - `trading_best_practices` prompt now aligns with the current MCP safety constraints and testnet broker references.
16
+
17
+ ## 3.0.19 - 2026-03-19
18
+
19
+ ### Fixed
20
+ - Refined `fundingInfo.nextFundingRate` formatting in `get_pool_metadata`:
21
+ - Displays percent per second as `%/秒`
22
+ - Displays derived percent per day as `%/天`
23
+ - Keeps raw integer and comma-separated views for audit/debug use
24
+
25
+ ## 3.0.18 - 2026-03-19
26
+
27
+ ### Fixed
28
+ - Further improved `fundingInfo` / `ioTracker` readability in `get_pool_metadata`:
29
+ - `fundingInfo.nextEpochTime` now includes UTC timestamp and seconds-until-next-epoch.
30
+ - `fundingInfo.nextFundingRate` and `lastFundingFeeTracker` now include comma-separated raw views.
31
+ - `ioTracker` now includes derived notional-at-entry views based on `poolEntryPrice`.
32
+
33
+ ## 3.0.17 - 2026-03-19
34
+
35
+ ### Fixed
36
+ - Improved precision handling for liquidity and pool read tools:
37
+ - `get_pool_metadata` now adds `poolInfoFormatted` for exchange rate, LP token price, LP supply, debt, collateral, reserves, and open interest.
38
+ - `get_lp_price` now returns both raw and human-readable formatted values.
39
+ - Added regression coverage to ensure formatted precision fields are present.
40
+
3
41
  ## 3.0.16 - 2026-03-18
4
42
 
5
43
  ### Fixed
package/README.md CHANGED
@@ -6,10 +6,11 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
6
6
 
7
7
  # Release Notes
8
8
 
9
- - **Current release: 3.0.16**
9
+ - **Current release: 3.0.20**
10
10
  - **SDK baseline**: `@myx-trade/sdk@^1.0.2` compatibility completed.
11
11
  - **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
12
12
  - **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
13
+ - **Safety refresh**: Docs and prompt guidance now reflect Oracle-only execution, exact-approval defaults, notional-based fee checks, TP/SL semantic validation, and LP preview fail-close behavior.
13
14
  - **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
14
15
 
15
16
  ---
@@ -20,6 +21,7 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
20
21
  * **AI-First Design**: Automated Pool ID resolution and flexible unit handling (`human:` vs `raw:`).
21
22
  * **Deep Liquidity Support**: Tools for both traders and liquidity providers.
22
23
  * **Production Ready**: Robust error handling with actionable hints for LLMs.
24
+ * **Precision-Aware Reads**: Pool and LP read tools expose human-readable formatted values alongside raw on-chain integers.
23
25
  * **Compliant**: Full Model Context Protocol (MCP) support.
24
26
 
25
27
  ---
@@ -30,13 +32,20 @@ Copy `.env.example` to `.env` and configure your trading wallet:
30
32
 
31
33
  ```bash
32
34
  PRIVATE_KEY=0x...
33
- RPC_URL=https://bsc-dataseed.bnbchain.org
34
- CHAIN_ID=56
35
+ RPC_URL=https://your-testnet-or-mainnet-rpc
36
+ CHAIN_ID=...
35
37
  BROKER_ADDRESS=0x...
36
38
  QUOTE_TOKEN_ADDRESS=0x...
37
- QUOTE_TOKEN_DECIMALS=18
39
+ QUOTE_TOKEN_DECIMALS=...
38
40
  ```
39
41
 
42
+ ## Testnet `MYXBroker` Reference
43
+
44
+ - Arbitrum test: `0x895C4ae2A22bB26851011d733A9355f663a1F939`
45
+ - Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
46
+
47
+ Use the broker that matches your active RPC and chain configuration.
48
+
40
49
  ---
41
50
 
42
51
  # Core Tools Reference
@@ -76,6 +85,17 @@ QUOTE_TOKEN_DECIMALS=18
76
85
 
77
86
  ---
78
87
 
88
+ # Safety Defaults
89
+
90
+ - **Oracle-only execution**: trading paths now require a fresh Oracle price and reject stale / missing execution prices.
91
+ - **Exact approvals by default**: local fallback flows now prefer exact approvals instead of implicit unlimited approval.
92
+ - **Size semantics**: `size` always means base-asset quantity, not USD notional.
93
+ - **Direction validation**: when a tool operates on an existing `positionId`, the supplied `direction` must match the live position.
94
+ - **TP/SL semantics**: LONG requires `tpPrice > entryPrice` and `slPrice < entryPrice`; SHORT uses the inverse.
95
+ - **LP safety**: LP preview failures are fail-close and no longer downgrade to `minAmountOut=0`.
96
+
97
+ ---
98
+
79
99
  # Tool Discovery
80
100
 
81
101
  When a client or LLM is unsure which tool to call:
@@ -110,6 +130,7 @@ Use these conventions when generating tool arguments:
110
130
  - `timeInForce`: SDK `v1.0.2` currently supports `IOC` only, so use `0` or `"IOC"`
111
131
  - `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
112
132
  - `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
133
+ - `slippagePct`: trading tools use 4-decimal raw units where `100 = 1.00%` and `50 = 0.50%`
113
134
  - Human units: `"100"` means 100 USDC or 100 token units depending on field
114
135
  - Raw units: `"raw:1000000"` means exact on-chain integer units
115
136
 
package/TOOL_EXAMPLES.md CHANGED
@@ -1,4 +1,4 @@
1
- # MYX MCP Tool Examples Handbook (v3.0.15)
1
+ # MYX MCP Tool Examples Handbook (v3.0.20)
2
2
 
3
3
  This guide provides practical MCP payload examples for the current unified toolset.
4
4
  All examples use the MCP format:
@@ -7,6 +7,13 @@ All examples use the MCP format:
7
7
  { "name": "tool_name", "arguments": { "...": "..." } }
8
8
  ```
9
9
 
10
+ ## Environment Notes
11
+
12
+ - Common testnet brokers:
13
+ - Arbitrum test: `0x895C4ae2A22bB26851011d733A9355f663a1F939`
14
+ - Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
15
+ - Keep `RPC_URL`, `CHAIN_ID`, and `BROKER_ADDRESS` on the same network.
16
+
10
17
  ---
11
18
 
12
19
  ## Discovery First
@@ -75,6 +82,8 @@ Recommended high-level entry tool.
75
82
  `marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
76
83
  `size` is always the base-asset quantity, not the USD notional. For example, a 500 USD order at price 1200 implies `size ≈ 0.416666...`.
77
84
  `collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
85
+ If `price` is omitted, the tool uses a fresh Oracle price only; stale or unresolved execution prices are rejected.
86
+ Auto-computed `tradingFee` follows notional semantics rather than raw collateral-only estimation.
78
87
 
79
88
  Raw-units example:
80
89
 
@@ -113,6 +122,7 @@ Low-level increase-order tool when you want full control.
113
122
 
114
123
  `timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.2`.
115
124
  `executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
125
+ If `positionId` is supplied on increase flows, `direction` must remain consistent with the live position.
116
126
 
117
127
  ### `close_position`
118
128
  Close or reduce a position. Use `ALL` for a full close.
@@ -137,6 +147,8 @@ Close or reduce a position. Use `ALL` for a full close.
137
147
  }
138
148
  ```
139
149
 
150
+ `direction` must match the live position. MCP now validates live direction before sending the close request.
151
+
140
152
  ### `manage_tp_sl`
141
153
  Create or update TP/SL on an open position.
142
154
 
@@ -154,6 +166,9 @@ Create or update TP/SL on an open position.
154
166
  }
155
167
  ```
156
168
 
169
+ Plain integer prices such as `"2800"` are treated as human prices, not implicit raw 30-decimal values.
170
+ For LONG positions, use `tpPrice > entryPrice` and `slPrice < entryPrice`. For SHORT positions, use the inverse.
171
+
157
172
  Delete both TP/SL orders:
158
173
 
159
174
  ```json
@@ -233,6 +248,8 @@ Unified pool detail, config, and liquidity info.
233
248
  }
234
249
  ```
235
250
 
251
+ `get_pool_metadata` returns raw values in `poolInfo` and precision-safe human-readable values in `poolInfoFormatted`, including readable funding epoch timestamps, funding-rate `%/秒` and `%/天`, and IO notional-at-entry views.
252
+
236
253
  ### `get_kline`
237
254
  Read chart data. Use `limit: 1` for the latest bar.
238
255
 
@@ -282,6 +299,9 @@ Alias-friendly form also works:
282
299
  }
283
300
  ```
284
301
 
302
+ LP preview failures now fail closed; the server no longer downgrades to `minAmountOut=0`.
303
+ Oracle-backed LP pricing requires a fresh price snapshot before execution.
304
+
285
305
  ### `get_lp_price`
286
306
  Read LP NAV price for BASE or QUOTE side.
287
307
 
@@ -295,6 +315,8 @@ Read LP NAV price for BASE or QUOTE side.
295
315
  }
296
316
  ```
297
317
 
318
+ `get_lp_price` returns both `raw` and `formatted` NAV price values.
319
+
298
320
  ### `get_my_lp_holdings`
299
321
  Read current LP balances across pools.
300
322
 
@@ -387,3 +409,5 @@ Read open positions, history, or both.
387
409
  4. Canonical enums are still preferred:
388
410
  `OPEN|HISTORY|ALL`, `BASE|QUOTE`, `LONG|SHORT`, `MARKET|LIMIT|STOP`.
389
411
  5. The server tolerates common lowercase and alias forms for better AI compatibility.
412
+ 6. Trading `slippagePct` uses 4-decimal raw units, so `100 = 1.00%` and `50 = 0.50%`.
413
+ 7. High-risk execution paths prefer fresh Oracle pricing, exact approval sizing, and fail-close behavior on missing previews or invalid units.
@@ -12,7 +12,7 @@ export const tradingGuidePrompt = {
12
12
  content: {
13
13
  type: "text",
14
14
  text: `
15
- # MYX Trading MCP Best Practices (v3.0.12)
15
+ # MYX Trading MCP Best Practices (v3.0.21)
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
 
@@ -28,21 +28,27 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
28
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
29
  - **Discovery**: \`search_tools\` understands legacy names like \`get_open_orders\` and intent phrases like \`add base lp\`.
30
30
  - **Unit Prefixes**: Prefer \`human:\` for readable amounts (e.g., "100" USDC) and \`raw:\` for exact on-chain units.
31
- - **Slippage**: Default is 100 (1%). For volatile tokens, consider 200-300 (2-3%).
31
+ - **Slippage**: Trading tools use 4-decimal raw units where \`100 = 1.00%\` and \`50 = 0.50%\`. Keep it tight unless the market is genuinely illiquid.
32
32
  - **Fees**: Use \`get_pool_metadata\` to view current fee tiers and pool configuration.
33
33
  - **LP Strategy**: Use \`get_my_lp_holdings\` to monitor liquidity positions. Naming follows \`mBASE.QUOTE\` (e.g., \`mBTC.USDC\`).
34
34
  - **Enum Tolerance**: The server tolerates common lowercase or alias inputs such as \`open\`, \`base\`, \`buy\`, and \`add\`, but canonical forms are still preferred in documentation.
35
+ - **Oracle Safety**: Execution flows now require a fresh Oracle price. Do not fall back to stale ticker or user-supplied execution prices when the Oracle is unavailable.
36
+ - **Approval Safety**: Local fallback flows prefer exact approval sizing. Do not assume unlimited approvals are necessary.
37
+ - **Position Semantics**: \`size\` is BASE quantity, not USD notional. If a \`positionId\` is supplied, \`direction\` must match the live position.
38
+ - **TP/SL Semantics**: LONG should use \`tpPrice > entryPrice\` and \`slPrice < entryPrice\`; SHORT uses the inverse. Plain integer strings like \`"65000"\` are treated as human prices, not implicit raw 30-decimal values.
39
+ - **LP Safety**: LP execution requires a fresh price snapshot and preview success; do not continue after preview failure.
40
+
41
+ ## 3. Testnet Broker Reference
42
+ - Arbitrum test: \`0x895C4ae2A22bB26851011d733A9355f663a1F939\`
43
+ - Linea test: \`0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2\`
44
+ - Always keep \`RPC_URL\`, \`CHAIN_ID\`, and \`BROKER_ADDRESS\` on the same network.
35
45
 
36
46
  Current Session:
37
47
  - Wallet: ${address}
38
48
  - Chain ID: ${chainId}
39
49
 
40
- ## 3. Self-Healing
50
+ ## 4. Self-Healing
41
51
  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.
42
-
43
- Current Session:
44
- - Wallet: ${address}
45
- - Chain ID: ${chainId}
46
52
  `
47
53
  }
48
54
  }
package/dist/server.js CHANGED
@@ -461,7 +461,7 @@ function zodSchemaToJsonSchema(zodSchema) {
461
461
  };
462
462
  }
463
463
  // ─── MCP Server ───
464
- const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.16" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
464
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.21" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
465
465
  // List tools
466
466
  server.setRequestHandler(ListToolsRequestSchema, async () => {
467
467
  return {
@@ -582,7 +582,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
582
582
  async function main() {
583
583
  const transport = new StdioServerTransport();
584
584
  await server.connect(transport);
585
- logger.info("🚀 MYX Trading MCP Server v3.0.16 running (stdio, pure on-chain, prod ready)");
585
+ logger.info("🚀 MYX Trading MCP Server v3.0.20 running (stdio, pure on-chain, prod ready)");
586
586
  }
587
587
  main().catch((err) => {
588
588
  logger.error("Fatal Server Startup Error", err);
@@ -1,5 +1,6 @@
1
1
  import { getChainId } from "../auth/resolveClient.js";
2
2
  import { getMarketStateDesc } from "../utils/mappings.js";
3
+ export const DEFAULT_ORACLE_MAX_AGE_SEC = Number(process.env.ORACLE_MAX_AGE_SEC ?? 90);
3
4
  function collectRows(input) {
4
5
  if (Array.isArray(input))
5
6
  return input.flatMap(collectRows);
@@ -92,6 +93,46 @@ export async function getOraclePrice(client, poolId, chainIdOverride) {
92
93
  const chainId = chainIdOverride ?? getChainId();
93
94
  return client.utils.getOraclePrice(poolId, chainId);
94
95
  }
96
+ function parseOraclePublishTime(value, poolId) {
97
+ const text = String(value ?? "").trim();
98
+ if (!/^\d+$/.test(text)) {
99
+ throw new Error(`Oracle publishTime missing or invalid for poolId=${poolId}.`);
100
+ }
101
+ const parsed = BigInt(text);
102
+ if (parsed <= 0n) {
103
+ throw new Error(`Oracle publishTime must be positive for poolId=${poolId}.`);
104
+ }
105
+ return parsed;
106
+ }
107
+ export function assertOracleFreshness(publishTimeValue, poolId, maxAgeSec = DEFAULT_ORACLE_MAX_AGE_SEC) {
108
+ if (!Number.isFinite(maxAgeSec) || maxAgeSec <= 0) {
109
+ throw new Error(`Invalid oracle max age configuration: ${maxAgeSec}`);
110
+ }
111
+ const publishTime = parseOraclePublishTime(publishTimeValue, poolId);
112
+ const nowSec = BigInt(Math.floor(Date.now() / 1000));
113
+ const maxAge = BigInt(Math.floor(maxAgeSec));
114
+ if (publishTime > nowSec + 5n) {
115
+ throw new Error(`Oracle publishTime is in the future for poolId=${poolId}.`);
116
+ }
117
+ const age = nowSec - publishTime;
118
+ if (age > maxAge) {
119
+ throw new Error(`Oracle price expired for poolId=${poolId}: age=${age.toString()}s exceeds maxAge=${maxAge.toString()}s.`);
120
+ }
121
+ return publishTime;
122
+ }
123
+ export async function getFreshOraclePrice(client, poolId, chainIdOverride, maxAgeSec = DEFAULT_ORACLE_MAX_AGE_SEC) {
124
+ const oracle = await getOraclePrice(client, poolId, chainIdOverride);
125
+ const price = String(oracle?.price ?? "").trim();
126
+ if (!price) {
127
+ throw new Error(`Oracle price missing for poolId=${poolId}.`);
128
+ }
129
+ const publishTime = assertOracleFreshness(oracle?.publishTime, poolId, maxAgeSec);
130
+ return {
131
+ ...oracle,
132
+ price,
133
+ publishTime: publishTime.toString(),
134
+ };
135
+ }
95
136
  export async function searchMarket(client, keyword, limit = 1000, chainIdOverride) {
96
137
  const chainId = chainIdOverride ?? getChainId();
97
138
  const normalizedKeyword = String(keyword ?? "").trim();
@@ -1,10 +1,11 @@
1
1
  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
- import { ensureUnits } from "../utils/units.js";
4
+ import { ensureUnits, parseUserUnits } from "../utils/units.js";
5
5
  import { normalizeAddress } from "../utils/address.js";
6
- import { Contract, parseUnits, MaxUint256 } from "ethers";
6
+ import { Contract } from "ethers";
7
7
  import { logger } from "../utils/logger.js";
8
+ import { assertOracleFreshness } from "./marketService.js";
8
9
  const LP_DECIMALS = 18;
9
10
  const POOL_MANAGER_BY_CHAIN = {
10
11
  421614: "0xf268D9FeD3Bd56fd9aBdb4FeEb993338613678A8",
@@ -202,10 +203,13 @@ async function buildOraclePricePayload(client, chainId, poolId, fallbackOracleTy
202
203
  if (!vaa || !vaa.startsWith("0x")) {
203
204
  throw new Error(`Oracle VAA unavailable for pool ${poolId}.`);
204
205
  }
205
- const publishTime = toPositiveBigint(oracle?.publishTime) ?? BigInt(Math.floor(Date.now() / 1000));
206
+ const publishTime = assertOracleFreshness(oracle?.publishTime, poolId);
206
207
  const oracleType = Number.isFinite(Number(oracle?.oracleType)) ? Number(oracle.oracleType) : fallbackOracleType;
207
208
  const value = toPositiveBigint(oracle?.value) ?? 0n;
208
209
  const referencePrice30 = BigInt(ensureUnits(String(oracle?.price ?? "0"), 30, "oracle price"));
210
+ if (referencePrice30 <= 0n) {
211
+ throw new Error(`Oracle price must be positive for pool ${poolId}.`);
212
+ }
209
213
  return {
210
214
  prices: [[poolId, oracleType, publishTime, vaa]],
211
215
  value,
@@ -229,8 +233,7 @@ async function previewAmountOutForLiquidity(signer, chainId, poolId, poolType, a
229
233
  return toPositiveBigint(out) ?? 0n;
230
234
  }
231
235
  catch (error) {
232
- logger.warn("LP preview call failed; fallback to minAmountOut=0.", extractErrorMessage(error));
233
- return 0n;
236
+ throw new Error(`LP preview failed: ${extractErrorMessage(error)}`);
234
237
  }
235
238
  }
236
239
  async function executeLiquidityTxViaRouter(params) {
@@ -244,7 +247,7 @@ async function executeLiquidityTxViaRouter(params) {
244
247
  if (!Number.isFinite(decimals) || decimals < 0) {
245
248
  throw new Error(`Invalid decimals while preparing ${poolType} ${action} transaction.`);
246
249
  }
247
- const amountIn = parseUnits(String(amount), decimals);
250
+ const amountIn = BigInt(parseUserUnits(String(amount), decimals, "amount"));
248
251
  if (amountIn <= 0n) {
249
252
  throw new Error(`Liquidity ${poolType.toLowerCase()} ${action} amount must be > 0.`);
250
253
  }
@@ -255,7 +258,7 @@ async function executeLiquidityTxViaRouter(params) {
255
258
  const allowance = toPositiveBigint(await tokenContract.allowance(address, addresses.router)) ?? 0n;
256
259
  if (allowance < amountIn) {
257
260
  logger.info(`[LP fallback] allowance insufficient for ${poolType} deposit, approving router. required=${amountIn.toString()}, current=${allowance.toString()}`);
258
- const approveTx = await tokenContract.approve(addresses.router, MaxUint256);
261
+ const approveTx = await tokenContract.approve(addresses.router, amountIn);
259
262
  approvalTxHash = String(approveTx?.hash ?? "").trim() || null;
260
263
  const approveReceipt = await approveTx?.wait?.();
261
264
  if (approveReceipt && approveReceipt.status !== 1) {
@@ -351,10 +354,8 @@ async function resolvePositiveMarketPrice30(client, poolId, chainId) {
351
354
  return null;
352
355
  try {
353
356
  const oracle = await client.utils?.getOraclePrice?.(poolId, chainId);
354
- const byValue = toPositiveBigint(oracle?.value);
357
+ assertOracleFreshness(oracle?.publishTime, poolId);
355
358
  const byPrice = toPositiveBigint(oracle?.price);
356
- if (byValue)
357
- return byValue;
358
359
  if (byPrice)
359
360
  return byPrice;
360
361
  }
@@ -481,80 +482,28 @@ export async function getLiquidityInfo(client, poolId, marketPrice, chainIdOverr
481
482
  */
482
483
  export async function quoteDeposit(poolId, amount, slippage, chainIdOverride) {
483
484
  const chainId = chainIdOverride ?? getChainId();
484
- const txHashBefore = readLastMockTxHash();
485
- try {
486
- return await withMutedSdkAbiMismatchLogs(() => quote.deposit({ chainId, poolId, amount, slippage }));
487
- }
488
- catch (error) {
489
- const message = extractErrorMessage(error);
490
- const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "QUOTE", action: "deposit" });
491
- if (recovered)
492
- return recovered;
493
- if (!isAbiLengthMismatchError(message))
494
- throw error;
495
- logger.warn("quote.deposit hit SDK ABI mismatch; switching to explicit router path.");
496
- return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "deposit", amount, slippage });
497
- }
485
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "deposit", amount, slippage });
498
486
  }
499
487
  /**
500
488
  * Quote 池 withdraw
501
489
  */
502
490
  export async function quoteWithdraw(poolId, amount, slippage, chainIdOverride) {
503
491
  const chainId = chainIdOverride ?? getChainId();
504
- const txHashBefore = readLastMockTxHash();
505
- try {
506
- return await withMutedSdkAbiMismatchLogs(() => quote.withdraw({ chainId, poolId, amount, slippage }));
507
- }
508
- catch (error) {
509
- const message = extractErrorMessage(error);
510
- const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "QUOTE", action: "withdraw" });
511
- if (recovered)
512
- return recovered;
513
- if (!isAbiLengthMismatchError(message))
514
- throw error;
515
- logger.warn("quote.withdraw hit SDK ABI mismatch; switching to explicit router path.");
516
- return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "withdraw", amount, slippage });
517
- }
492
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "withdraw", amount, slippage });
518
493
  }
519
494
  /**
520
495
  * Base 池 deposit
521
496
  */
522
497
  export async function baseDeposit(poolId, amount, slippage, chainIdOverride) {
523
498
  const chainId = chainIdOverride ?? getChainId();
524
- const txHashBefore = readLastMockTxHash();
525
- try {
526
- return await withMutedSdkAbiMismatchLogs(() => base.deposit({ chainId, poolId, amount, slippage }));
527
- }
528
- catch (error) {
529
- const message = extractErrorMessage(error);
530
- const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "BASE", action: "deposit" });
531
- if (recovered)
532
- return recovered;
533
- if (!isAbiLengthMismatchError(message))
534
- throw error;
535
- logger.warn("base.deposit hit SDK ABI mismatch; switching to explicit router path.");
536
- return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "deposit", amount, slippage });
537
- }
499
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "deposit", amount, slippage });
538
500
  }
539
501
  /**
540
502
  * Base 池 withdraw
541
503
  */
542
504
  export async function baseWithdraw(poolId, amount, slippage, chainIdOverride) {
543
505
  const chainId = chainIdOverride ?? getChainId();
544
- const txHashBefore = readLastMockTxHash();
545
- try {
546
- return await withMutedSdkAbiMismatchLogs(() => base.withdraw({ chainId, poolId, amount, slippage }));
547
- }
548
- catch (error) {
549
- const message = extractErrorMessage(error);
550
- const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "BASE", action: "withdraw" });
551
- if (recovered)
552
- return recovered;
553
- if (!isAbiLengthMismatchError(message))
554
- throw error;
555
- logger.warn("base.withdraw hit SDK ABI mismatch; switching to explicit router path.");
556
- return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "withdraw", amount, slippage });
557
- }
506
+ return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "withdraw", amount, slippage });
558
507
  }
559
508
  /**
560
509
  * 获取 LP 价格
@@ -7,6 +7,7 @@ import { normalizeSlippagePct4dp } from "../utils/slippage.js";
7
7
  import { finalizeMutationResult } from "../utils/mutationResult.js";
8
8
  import { extractErrorMessage } from "../utils/errorMessage.js";
9
9
  import { mapTimeInForce } from "../utils/mappings.js";
10
+ import { getFreshOraclePrice } from "./marketService.js";
10
11
  function resolveDirection(direction) {
11
12
  if (typeof direction === "string") {
12
13
  const text = direction.trim().toUpperCase();
@@ -154,6 +155,37 @@ function validateIncreaseOrderEconomics(args) {
154
155
  const priceHuman = formatUnits(priceRawBig, 30);
155
156
  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
157
  }
158
+ function countAdditionalExecutionOrders(args) {
159
+ let count = 1;
160
+ if (String(args.tpPrice ?? "").trim()) {
161
+ count += 1;
162
+ }
163
+ if (String(args.slPrice ?? "").trim()) {
164
+ count += 1;
165
+ }
166
+ return count;
167
+ }
168
+ async function getRequiredIncreaseSpendRaw(client, marketId, args, chainId) {
169
+ const collateralRaw = BigInt(args.collateralRaw);
170
+ const tradingFeeRaw = BigInt(args.tradingFeeRaw);
171
+ const executionOrderCount = countAdditionalExecutionOrders(args);
172
+ const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
173
+ if (!/^\d+$/.test(networkFeeText)) {
174
+ throw new Error(`Failed to resolve networkFee for marketId=${marketId}.`);
175
+ }
176
+ const baseNetworkFeeRaw = BigInt(networkFeeText);
177
+ if (baseNetworkFeeRaw <= 0n) {
178
+ throw new Error(`networkFee must be > 0 for marketId=${marketId}.`);
179
+ }
180
+ const networkFeeRaw = baseNetworkFeeRaw * BigInt(executionOrderCount);
181
+ return {
182
+ collateralRaw,
183
+ tradingFeeRaw,
184
+ networkFeeRaw,
185
+ executionOrderCount,
186
+ totalSpendRaw: collateralRaw + tradingFeeRaw + networkFeeRaw,
187
+ };
188
+ }
157
189
  async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
158
190
  let baseDecimals = 18;
159
191
  let quoteDecimals = getQuoteDecimals();
@@ -249,6 +281,18 @@ export async function openPosition(client, address, args) {
249
281
  const sizeRaw = ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false });
250
282
  const priceRaw = ensureUnits(args.price, 30, "price", { allowImplicitRaw: false });
251
283
  const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee", { allowImplicitRaw: false });
284
+ const resolvedMarketId = String(args.marketId ?? poolData.marketId ?? "").trim();
285
+ if (!resolvedMarketId) {
286
+ throw new Error(`marketId is required to compute networkFee for poolId=${args.poolId}.`);
287
+ }
288
+ const spend = await getRequiredIncreaseSpendRaw(client, resolvedMarketId, {
289
+ collateralRaw,
290
+ tradingFeeRaw,
291
+ tpPrice: args.tpPrice,
292
+ tpSize: args.tpSize,
293
+ slPrice: args.slPrice,
294
+ slSize: args.slSize,
295
+ }, chainId);
252
296
  validateIncreaseOrderEconomics({
253
297
  collateralRaw,
254
298
  sizeRaw,
@@ -268,7 +312,7 @@ export async function openPosition(client, address, args) {
268
312
  marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
269
313
  walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
270
314
  }
271
- const requiredRaw = BigInt(collateralRaw);
315
+ const requiredRaw = spend.totalSpendRaw;
272
316
  if (marginBalanceRaw < requiredRaw) {
273
317
  if (!allowAutoDeposit) {
274
318
  throw new Error(`Insufficient marginBalance (${marginBalanceRaw.toString()}) for required collateral (${requiredRaw.toString()}). ` +
@@ -459,9 +503,14 @@ export async function closeAllPositions(client, address) {
459
503
  const results = [];
460
504
  for (const pos of positions) {
461
505
  const dir = pos.direction === 0 ? Direction.LONG : Direction.SHORT;
462
- // Get current price for slippage protection
463
- const oracleData = await client.utils.getOraclePrice(pos.poolId, chainId);
464
- const currentPrice30 = ensureUnits(oracleData.price, 30, "oracle price");
506
+ const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId: pos.poolId });
507
+ const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
508
+ if (!marketDetail?.marketId) {
509
+ throw new Error(`Could not resolve market metadata for poolId=${pos.poolId}.`);
510
+ }
511
+ const baseDecimals = Number(marketDetail.baseDecimals ?? 18);
512
+ const oracleData = await getFreshOraclePrice(client, pos.poolId, chainId);
513
+ const currentPrice30 = ensureUnits(oracleData.price, 30, "oracle price", { allowImplicitRaw: false });
465
514
  // For LONG close (Decrease LONG): Price should be lower (e.g. 90% of current)
466
515
  // For SHORT close (Decrease SHORT): Price should be higher (e.g. 110% of current)
467
516
  // Here we use a safe 10% slippage price
@@ -472,7 +521,13 @@ export async function closeAllPositions(client, address) {
472
521
  else {
473
522
  slippagePrice30 = (BigInt(currentPrice30) * 110n) / 100n;
474
523
  }
475
- const sizeWei = ensureUnits(pos.size, 18, "size");
524
+ const sizeInput = /^\d+$/.test(String(pos.sizeRaw ?? pos.positionSizeRaw ?? "").trim())
525
+ ? `raw:${String(pos.sizeRaw ?? pos.positionSizeRaw).trim()}`
526
+ : String(pos.size ?? pos.positionSize ?? "").trim();
527
+ if (!sizeInput) {
528
+ throw new Error(`Position size missing for positionId=${String(pos.positionId ?? "").trim()}.`);
529
+ }
530
+ const sizeWei = ensureUnits(sizeInput, baseDecimals, "size", { allowImplicitRaw: false });
476
531
  const res = await client.order.createDecreaseOrder({
477
532
  chainId,
478
533
  address,
@@ -485,7 +540,7 @@ export async function closeAllPositions(client, address) {
485
540
  size: sizeWei,
486
541
  price: slippagePrice30.toString(),
487
542
  postOnly: false,
488
- slippagePct: "1000", // 10%
543
+ slippagePct: "100", // 1%
489
544
  executionFeeToken: getQuoteToken(),
490
545
  leverage: pos.userLeverage,
491
546
  });
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
+ import { fetchErc20Decimals } from "../utils/token.js";
5
6
  const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
6
7
  function asBigintOrNull(value) {
7
8
  try {
@@ -52,11 +53,9 @@ export const accountDepositTool = {
52
53
  const chainId = getChainId();
53
54
  const tokenAddressInput = String(args.tokenAddress ?? "").trim() || getQuoteToken();
54
55
  const tokenAddress = normalizeAddress(tokenAddressInput, "tokenAddress");
55
- // For deposit, we default to quote decimals (6) as it's the most common use case.
56
- // ensureUnits handles 'raw:' prefix if absolute precision is needed.
57
56
  const { ensureUnits } = await import("../utils/units.js");
58
- const { getQuoteDecimals } = await import("../auth/resolveClient.js");
59
- const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount", { allowImplicitRaw: false });
57
+ const tokenDecimals = await fetchErc20Decimals(signer.provider ?? signer, tokenAddress, "deposit token");
58
+ const amount = ensureUnits(args.amount, tokenDecimals, "amount", { allowImplicitRaw: false });
60
59
  let approval = null;
61
60
  const needApproval = await client.utils.needsApproval(address, chainId, tokenAddress, amount);
62
61
  if (needApproval) {
@@ -102,9 +101,17 @@ export const accountWithdrawTool = {
102
101
  const { client, address, signer } = await resolveClient();
103
102
  const chainId = getChainId();
104
103
  const { ensureUnits } = await import("../utils/units.js");
105
- const { getQuoteDecimals } = await import("../auth/resolveClient.js");
106
- // Assuming 18 decimals for base and quoteDecimals for quote
107
- const decimals = args.isQuoteToken ? getQuoteDecimals() : 18;
104
+ const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId: args.poolId });
105
+ const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
106
+ if (!marketDetail?.marketId) {
107
+ throw new Error(`Could not resolve market metadata for poolId=${args.poolId}.`);
108
+ }
109
+ const decimals = Number(Boolean(args.isQuoteToken)
110
+ ? marketDetail.quoteDecimals
111
+ : marketDetail.baseDecimals);
112
+ if (!Number.isFinite(decimals) || decimals < 0) {
113
+ throw new Error(`Invalid token decimals for withdraw on poolId=${args.poolId}.`);
114
+ }
108
115
  const amount = ensureUnits(args.amount, decimals, "amount", { allowImplicitRaw: false });
109
116
  const amountRaw = asBigintOrNull(amount);
110
117
  if (amountRaw === null || amountRaw <= 0n) {