@michaleffffff/mcp-trading-server 3.0.21 → 3.0.26
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 +72 -0
- package/README.md +18 -9
- package/TOOL_EXAMPLES.md +11 -4
- package/dist/auth/resolveClient.js +16 -4
- package/dist/prompts/tradingGuide.js +9 -3
- package/dist/server.js +2 -2
- package/dist/services/poolService.js +84 -35
- package/dist/services/tradeService.js +2 -90
- package/dist/tools/checkAccountReady.js +27 -7
- package/dist/tools/closePosition.js +1 -1
- package/dist/tools/executeTrade.js +1 -1
- package/dist/tools/getMyLpHoldings.js +11 -10
- package/dist/tools/getPoolMetadata.js +11 -19
- package/dist/tools/manageLiquidity.js +1 -1
- package/dist/tools/manageTpSl.js +5 -3
- package/dist/tools/openPositionSimple.js +12 -14
- package/dist/tools/searchTools.js +1 -0
- package/dist/utils/errorMessage.js +40 -1
- package/dist/utils/errors.js +61 -0
- package/dist/utils/mappings.js +2 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.26 - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Removed MCP-side increase-order margin reconciliation that could conflict with SDK `createIncreaseOrder`:
|
|
7
|
+
- `open_position_simple` / `execute_trade` now validate parameters locally but delegate deposit-delta handling entirely to the SDK write path.
|
|
8
|
+
- Deprecated `autoDeposit` as a compatibility-only flag in `open_position_simple`.
|
|
9
|
+
- Realigned `check_account_ready` with SDK trading semantics:
|
|
10
|
+
- Uses SDK `getAvailableMarginBalance` as the primary trading-account source instead of trusting raw `freeMargin`.
|
|
11
|
+
- Returns degraded diagnostics (`summary.degraded`, `diagnostics.availableMarginError`) when SDK margin reads fail.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Refreshed operator docs and examples to match current execution semantics:
|
|
15
|
+
- `README.md` now documents explicit `price` in the quick-start trade example, SDK-delegated increase-order funding deltas, and the new `check_account_ready` degraded diagnostics.
|
|
16
|
+
- `TOOL_EXAMPLES.md` now marks `autoDeposit` as deprecated and updates `MARKET` examples to provide `price`.
|
|
17
|
+
- `trading_best_practices` prompt now explains that pre-checks use SDK `availableMarginBalance` and that increase-order funding reconciliation lives in the SDK.
|
|
18
|
+
- Updated stale tests to match current MCP semantics:
|
|
19
|
+
- `tests/test_trading.ts` now provides `price` for `open_position_simple` market dry runs.
|
|
20
|
+
- `tests/verify_tp_sl_close_invalid_param.mjs` no longer relies on legacy `autoDeposit` semantics and now supplies `price` when opening a market position.
|
|
21
|
+
|
|
22
|
+
## 3.0.25 - 2026-03-19
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Improved MCP compatibility and diagnostics for current testnet brokers:
|
|
26
|
+
- `resolveClient` now auto-detects beta mode for Arbitrum Sepolia broker `0x895C4ae2A22bB26851011d733A9355f663a1F939`
|
|
27
|
+
- `resolveClient` now auto-detects beta mode for Linea Sepolia broker `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
28
|
+
- nested viem / SDK revert payloads now decode common selectors like `AccountInsufficientTradableAmount(uint256,uint256)`
|
|
29
|
+
- `open_position_simple` now returns decoded nested contract errors instead of only raw error text
|
|
30
|
+
|
|
31
|
+
## 3.0.24 - 2026-03-19
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- Hardened LP MCP behavior:
|
|
35
|
+
- `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
36
|
+
- `get_pool_info` MCP-side market price resolution no longer falls back from fresh Oracle to ticker-derived price.
|
|
37
|
+
- Beta LP router / `PoolManager` fallback mappings were completed for MCP-managed LP and create-market paths.
|
|
38
|
+
- `get_my_lp_holdings` no longer ranks rows by mixed BASE/QUOTE LP raw balances.
|
|
39
|
+
- `open_position_simple` no longer auto-fills a fresh Oracle price for `MARKET` orders; callers must provide `price` explicitly.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Synced release metadata and operator docs for `v3.0.24`:
|
|
43
|
+
- Updated `README.md`, `TOOL_EXAMPLES.md`, and `trading_best_practices` prompt version strings.
|
|
44
|
+
- Refreshed LP safety docs to reflect Oracle-only liquidity metadata and inventory-only LP holdings semantics.
|
|
45
|
+
|
|
46
|
+
## 3.0.23 - 2026-03-19
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
- Upgraded SDK dependency to `@myx-trade/sdk@^1.0.4-beta.4`.
|
|
50
|
+
- Refreshed operator-facing docs and runtime hints to match the new SDK baseline:
|
|
51
|
+
- `README.md` now references `@myx-trade/sdk@^1.0.4-beta.4`.
|
|
52
|
+
- `TOOL_EXAMPLES.md`, `execute_trade`, `close_position`, and `mapTimeInForce` now document the current IOC-only `timeInForce` behavior using `v1.0.4-beta.4`.
|
|
53
|
+
- `trading_best_practices` prompt now aligns with release `v3.0.23`.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- Hardened LP MCP behavior and synchronized docs:
|
|
57
|
+
- `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
58
|
+
- `get_pool_info` MCP-side price resolution no longer falls back from fresh Oracle to ticker-derived market price.
|
|
59
|
+
- Beta LP router / `PoolManager` chain mappings were completed for the MCP fallback path.
|
|
60
|
+
- `get_my_lp_holdings` no longer ranks rows by mixed BASE/QUOTE LP raw balances.
|
|
61
|
+
- `README.md`, `TOOL_EXAMPLES.md`, and `mcp_config_guide.md` now document these LP-specific MCP semantics.
|
|
62
|
+
|
|
63
|
+
## 3.0.22 - 2026-03-19
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
- Upgraded SDK dependency to `@myx-trade/sdk@^1.0.4-beta.1`.
|
|
67
|
+
- Refreshed operator-facing docs and runtime hints to match the new SDK baseline:
|
|
68
|
+
- `README.md` now references `@myx-trade/sdk@^1.0.4-beta.1`.
|
|
69
|
+
- `TOOL_EXAMPLES.md`, `execute_trade`, `close_position`, and `mapTimeInForce` now document the current IOC-only `timeInForce` behavior using the new SDK version.
|
|
70
|
+
- `trading_best_practices` prompt now aligns with release `v3.0.22`.
|
|
71
|
+
|
|
3
72
|
## 3.0.21 - 2026-03-19
|
|
4
73
|
|
|
5
74
|
### Fixed
|
|
@@ -13,6 +82,9 @@
|
|
|
13
82
|
- `mcp_config_guide.md` now includes required `BROKER_ADDRESS` configuration and testnet `MYXBroker` references.
|
|
14
83
|
- `TOOL_EXAMPLES.md` now reflects fresh-Oracle execution, live-direction validation, human-price TP/SL parsing, and trading `slippagePct` conventions.
|
|
15
84
|
- `trading_best_practices` prompt now aligns with the current MCP safety constraints and testnet broker references.
|
|
85
|
+
- Updated testnet broker defaults:
|
|
86
|
+
- Arbitrum Sepolia `BROKER_ADDRESS` -> `0x895C4ae2A22bB26851011d733A9355f663a1F939`
|
|
87
|
+
- Linea Sepolia `BROKER_ADDRESS` -> `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
16
88
|
|
|
17
89
|
## 3.0.19 - 2026-03-19
|
|
18
90
|
|
package/README.md
CHANGED
|
@@ -6,11 +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.
|
|
10
|
-
- **SDK baseline**: `@myx-trade/sdk@^1.0.
|
|
9
|
+
- **Current release: 3.0.24**
|
|
10
|
+
- **SDK baseline**: `@myx-trade/sdk@^1.0.4-beta.4` 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
|
|
13
|
+
- **Safety refresh**: Docs and prompt guidance now reflect explicit-price execution for `open_position_simple`, exact-approval defaults, notional-based fee checks, TP/SL semantic validation, and LP preview fail-close behavior.
|
|
14
14
|
- **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
|
|
15
15
|
|
|
16
16
|
---
|
|
@@ -37,6 +37,7 @@ CHAIN_ID=...
|
|
|
37
37
|
BROKER_ADDRESS=0x...
|
|
38
38
|
QUOTE_TOKEN_ADDRESS=0x...
|
|
39
39
|
QUOTE_TOKEN_DECIMALS=...
|
|
40
|
+
IS_BETA_MODE=true
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
## Testnet `MYXBroker` Reference
|
|
@@ -45,6 +46,7 @@ QUOTE_TOKEN_DECIMALS=...
|
|
|
45
46
|
- Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
46
47
|
|
|
47
48
|
Use the broker that matches your active RPC and chain configuration.
|
|
49
|
+
If `IS_BETA_MODE` is omitted, MCP now auto-detects beta mode for the two testnet brokers above.
|
|
48
50
|
|
|
49
51
|
---
|
|
50
52
|
|
|
@@ -55,7 +57,7 @@ Use the broker that matches your active RPC and chain configuration.
|
|
|
55
57
|
* **`list_pools`**: List all tradable assets on the current chain.
|
|
56
58
|
* **`search_tools`**: Discover the right MCP tool by keyword, legacy tool name, or intent phrase.
|
|
57
59
|
* **`get_price`**: Fetch real-time prices (Impact Market Price or Oracle Price).
|
|
58
|
-
* **`get_pool_metadata`**: Comprehensive metrics (Fees, Open Interest, Liquidity Depth).
|
|
60
|
+
* **`get_pool_metadata`**: Comprehensive metrics (Fees, Open Interest, Liquidity Depth via fresh Oracle price).
|
|
59
61
|
* **`get_kline`**: Fetch candlestick data for technical analysis.
|
|
60
62
|
|
|
61
63
|
### ⚔️ Trading Operations
|
|
@@ -72,7 +74,7 @@ Use the broker that matches your active RPC and chain configuration.
|
|
|
72
74
|
* **`get_orders`**: Historical and active order ledger.
|
|
73
75
|
* **`get_positions_all`**: Currently open and recently closed positions.
|
|
74
76
|
* **`get_trade_flow`**: Granular transaction history.
|
|
75
|
-
* **`check_account_ready`**: Pre-trade balance validator with
|
|
77
|
+
* **`check_account_ready`**: Pre-trade balance validator aligned with SDK `availableMarginBalance` semantics.
|
|
76
78
|
|
|
77
79
|
---
|
|
78
80
|
|
|
@@ -80,19 +82,24 @@ Use the broker that matches your active RPC and chain configuration.
|
|
|
80
82
|
|
|
81
83
|
1. **Find Target**: `find_pool(keyword="BTC")`
|
|
82
84
|
2. **Check State**: `get_account_snapshot(poolId="...")`
|
|
83
|
-
3. **Execute**: `open_position_simple(poolId="...", direction="LONG", leverage=5, collateralAmount="100")`
|
|
85
|
+
3. **Execute**: `open_position_simple(poolId="...", direction="LONG", leverage=5, collateralAmount="100", price="2500")`
|
|
84
86
|
4. **Monitor**: `get_positions_all(status="OPEN")`
|
|
85
87
|
|
|
86
88
|
---
|
|
87
89
|
|
|
88
90
|
# Safety Defaults
|
|
89
91
|
|
|
90
|
-
- **
|
|
92
|
+
- **Explicit execution price**: `open_position_simple` no longer auto-fills a fresh Oracle price for `MARKET` orders; provide `price` explicitly when opening a position.
|
|
93
|
+
- **SDK-delegated funding delta**: MCP no longer performs its own increase-order margin/deposit reconciliation. New increase orders delegate collateral shortfall handling to SDK `createIncreaseOrder`.
|
|
94
|
+
- **Beta broker compatibility**: current Arbitrum Sepolia / Linea Sepolia test brokers auto-enable beta mode when `IS_BETA_MODE` is unset.
|
|
91
95
|
- **Exact approvals by default**: local fallback flows now prefer exact approvals instead of implicit unlimited approval.
|
|
92
96
|
- **Size semantics**: `size` always means base-asset quantity, not USD notional.
|
|
97
|
+
- **Pre-check semantics**: `check_account_ready` now uses SDK `getAvailableMarginBalance`. When that read fails, the response marks `summary.degraded=true` and includes diagnostics instead of silently trusting stale `freeMargin`.
|
|
93
98
|
- **Direction validation**: when a tool operates on an existing `positionId`, the supplied `direction` must match the live position.
|
|
94
99
|
- **TP/SL semantics**: LONG requires `tpPrice > entryPrice` and `slPrice < entryPrice`; SHORT uses the inverse.
|
|
95
100
|
- **LP safety**: LP preview failures are fail-close and no longer downgrade to `minAmountOut=0`.
|
|
101
|
+
- **LP metadata safety**: `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
102
|
+
- **LP holdings semantics**: `get_my_lp_holdings` is an inventory view; returned rows are no longer ranked by mixed BASE/QUOTE LP raw balances.
|
|
96
103
|
|
|
97
104
|
---
|
|
98
105
|
|
|
@@ -127,7 +134,7 @@ Use these conventions when generating tool arguments:
|
|
|
127
134
|
- `status`: `OPEN` / `HISTORY` / `ALL` are canonical; lowercase is tolerated
|
|
128
135
|
- `poolType`: `BASE` / `QUOTE` are canonical; lowercase is tolerated
|
|
129
136
|
- `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
|
|
130
|
-
- `timeInForce`: SDK `v1.0.
|
|
137
|
+
- `timeInForce`: SDK `v1.0.4-beta.4` currently supports `IOC` only, so use `0` or `"IOC"`
|
|
131
138
|
- `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
|
|
132
139
|
- `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
|
|
133
140
|
- `slippagePct`: trading tools use 4-decimal raw units where `100 = 1.00%` and `50 = 0.50%`
|
|
@@ -156,6 +163,8 @@ Examples:
|
|
|
156
163
|
}
|
|
157
164
|
```
|
|
158
165
|
|
|
166
|
+
For `get_pool_metadata(includeLiquidity=true)`, do not rely on a custom `marketPrice` override. MCP now ignores that field and uses a fresh Oracle price automatically.
|
|
167
|
+
|
|
159
168
|
```json
|
|
160
169
|
{
|
|
161
170
|
"name": "open_position_simple",
|
|
@@ -164,7 +173,7 @@ Examples:
|
|
|
164
173
|
"direction": "LONG",
|
|
165
174
|
"collateralAmount": "100",
|
|
166
175
|
"leverage": 5,
|
|
167
|
-
"orderType": "
|
|
176
|
+
"orderType": "MARKET",
|
|
168
177
|
"price": "2.5"
|
|
169
178
|
}
|
|
170
179
|
}
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# MYX MCP Tool Examples Handbook
|
|
1
|
+
# MYX MCP Tool Examples Handbook
|
|
2
2
|
|
|
3
3
|
This guide provides practical MCP payload examples for the current unified toolset.
|
|
4
4
|
All examples use the MCP format:
|
|
@@ -82,8 +82,9 @@ Recommended high-level entry tool.
|
|
|
82
82
|
`marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
|
|
83
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...`.
|
|
84
84
|
`collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
|
|
85
|
-
|
|
85
|
+
Provide `price` explicitly for both `MARKET` and `LIMIT/STOP` on `open_position_simple`. MCP no longer auto-fills a fresh Oracle price for `MARKET`.
|
|
86
86
|
Auto-computed `tradingFee` follows notional semantics rather than raw collateral-only estimation.
|
|
87
|
+
`autoDeposit` is now a deprecated compatibility flag. MCP delegates increase-order funding deltas to the SDK during `createIncreaseOrder`.
|
|
87
88
|
|
|
88
89
|
Raw-units example:
|
|
89
90
|
|
|
@@ -95,7 +96,8 @@ Raw-units example:
|
|
|
95
96
|
"direction": "SHORT",
|
|
96
97
|
"collateralAmount": "raw:100000000",
|
|
97
98
|
"leverage": 10,
|
|
98
|
-
"orderType": "MARKET"
|
|
99
|
+
"orderType": "MARKET",
|
|
100
|
+
"price": "raw:2000000000000000000000000000000000"
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
```
|
|
@@ -120,7 +122,7 @@ Low-level increase-order tool when you want full control.
|
|
|
120
122
|
}
|
|
121
123
|
```
|
|
122
124
|
|
|
123
|
-
`timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.
|
|
125
|
+
`timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.4-beta.4`.
|
|
124
126
|
`executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
|
|
125
127
|
If `positionId` is supplied on increase flows, `direction` must remain consistent with the live position.
|
|
126
128
|
|
|
@@ -249,6 +251,7 @@ Unified pool detail, config, and liquidity info.
|
|
|
249
251
|
```
|
|
250
252
|
|
|
251
253
|
`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.
|
|
254
|
+
When `includeLiquidity=true`, MCP derives liquidity depth from a fresh Oracle price automatically. Any caller-supplied `marketPrice` is now treated as deprecated and ignored.
|
|
252
255
|
|
|
253
256
|
### `get_kline`
|
|
254
257
|
Read chart data. Use `limit: 1` for the latest bar.
|
|
@@ -330,6 +333,8 @@ Read current LP balances across pools.
|
|
|
330
333
|
}
|
|
331
334
|
```
|
|
332
335
|
|
|
336
|
+
`get_my_lp_holdings` is an inventory view. Results are now stably ordered by symbol / pool id instead of summing BASE LP raw units with QUOTE LP raw units into a misleading mixed-unit ranking.
|
|
337
|
+
|
|
333
338
|
---
|
|
334
339
|
|
|
335
340
|
## Account And Monitoring
|
|
@@ -359,6 +364,8 @@ Pre-check whether collateral is available before trading.
|
|
|
359
364
|
}
|
|
360
365
|
```
|
|
361
366
|
|
|
367
|
+
`check_account_ready` now uses SDK `getAvailableMarginBalance` as its primary trading-account source. If the SDK read degrades, the response includes `summary.degraded=true` plus diagnostics such as `sdkAvailableMarginBalance`, `accountInfoFreeMargin`, and `availableMarginError`.
|
|
368
|
+
|
|
362
369
|
### `get_orders`
|
|
363
370
|
Read open orders, history, or both.
|
|
364
371
|
|
|
@@ -2,13 +2,17 @@ import { MyxClient } from "@myx-trade/sdk";
|
|
|
2
2
|
import { JsonRpcProvider, Wallet } from "ethers";
|
|
3
3
|
import { normalizeAddress } from "../utils/address.js";
|
|
4
4
|
let cached = null;
|
|
5
|
+
const BETA_BROKERS_BY_CHAIN = {
|
|
6
|
+
421614: ["0x895c4ae2a22bb26851011d733a9355f663a1f939"],
|
|
7
|
+
59141: ["0x634efdc9dc76d7abf6e49279875a31b02e9891e2"],
|
|
8
|
+
};
|
|
5
9
|
function getDefaultBrokerByChainId(chainId) {
|
|
6
10
|
// Testnet mappings
|
|
7
11
|
if (chainId === 421614)
|
|
8
|
-
return "
|
|
12
|
+
return "0x895C4ae2A22bB26851011d733A9355f663a1F939"; // Arbitrum Sepolia
|
|
9
13
|
if (chainId === 59141)
|
|
10
|
-
return "
|
|
11
|
-
return "
|
|
14
|
+
return "0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2"; // Linea Sepolia
|
|
15
|
+
return "0x895C4ae2A22bB26851011d733A9355f663a1F939";
|
|
12
16
|
}
|
|
13
17
|
function getDefaultQuoteTokenByChainId(chainId) {
|
|
14
18
|
// Testnet mappings
|
|
@@ -18,6 +22,14 @@ function getDefaultQuoteTokenByChainId(chainId) {
|
|
|
18
22
|
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b"; // Linea Sepolia
|
|
19
23
|
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b";
|
|
20
24
|
}
|
|
25
|
+
function resolveIsBetaMode(chainId, brokerAddressRaw) {
|
|
26
|
+
const explicit = String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase();
|
|
27
|
+
if (explicit === "true")
|
|
28
|
+
return true;
|
|
29
|
+
if (explicit === "false")
|
|
30
|
+
return false;
|
|
31
|
+
return Boolean(BETA_BROKERS_BY_CHAIN[chainId]?.includes(String(brokerAddressRaw ?? "").trim().toLowerCase()));
|
|
32
|
+
}
|
|
21
33
|
export async function resolveClient() {
|
|
22
34
|
if (cached)
|
|
23
35
|
return cached;
|
|
@@ -25,7 +37,6 @@ export async function resolveClient() {
|
|
|
25
37
|
const privateKey = process.env.PRIVATE_KEY;
|
|
26
38
|
const chainId = Number(process.env.CHAIN_ID) || 59141;
|
|
27
39
|
const isTestnet = process.env.IS_TESTNET !== "false";
|
|
28
|
-
const isBetaMode = String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
|
|
29
40
|
const brokerAddressRaw = process.env.BROKER_ADDRESS || getDefaultBrokerByChainId(chainId);
|
|
30
41
|
const quoteTokenRaw = process.env.QUOTE_TOKEN_ADDRESS || getDefaultQuoteTokenByChainId(chainId);
|
|
31
42
|
const quoteDecimals = Number(process.env.QUOTE_TOKEN_DECIMALS) || 6;
|
|
@@ -39,6 +50,7 @@ export async function resolveClient() {
|
|
|
39
50
|
throw new Error("QUOTE_TOKEN_ADDRESS env var is required.");
|
|
40
51
|
const brokerAddress = normalizeAddress(brokerAddressRaw, "BROKER_ADDRESS");
|
|
41
52
|
const quoteToken = normalizeAddress(quoteTokenRaw, "QUOTE_TOKEN_ADDRESS");
|
|
53
|
+
const isBetaMode = resolveIsBetaMode(chainId, brokerAddress);
|
|
42
54
|
const provider = new JsonRpcProvider(rpcUrl);
|
|
43
55
|
const signer = new Wallet(privateKey, provider);
|
|
44
56
|
// Inject the EIP-1193 mock so SDK can sign transactions seamlessly
|
|
@@ -12,14 +12,14 @@ 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.26)
|
|
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
20
|
1. **Discovery**: Use \`search_tools\` if the intent is unclear, then use \`find_pool\` with a keyword (e.g. "BTC") to get the \`poolId\`.
|
|
21
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
|
|
22
|
+
3. **Pre-check**: Use \`check_account_ready\` to inspect SDK-aligned \`availableMarginBalance\` before trading.
|
|
23
23
|
4. **Execution**: Prefer \`open_position_simple\` for entry. It supports Stop-Loss (\`slPrice\`) and Take-Profit (\`tpPrice\`) in one call.
|
|
24
24
|
5. **Monitoring**: Use \`get_positions_all\` to track active trades and \`get_orders\` for pending/filled history.
|
|
25
25
|
6. **Unified Operations**: Use \`cancel_orders\` for targeted or global撤单, and \`manage_tp_sl\` to update protection orders.
|
|
@@ -30,18 +30,24 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
30
30
|
- **Unit Prefixes**: Prefer \`human:\` for readable amounts (e.g., "100" USDC) and \`raw:\` for exact on-chain units.
|
|
31
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
|
+
- **Liquidity Metadata**: When calling \`get_pool_metadata(includeLiquidity=true)\`, MCP uses a fresh Oracle price automatically and ignores caller-supplied \`marketPrice\`.
|
|
33
34
|
- **LP Strategy**: Use \`get_my_lp_holdings\` to monitor liquidity positions. Naming follows \`mBASE.QUOTE\` (e.g., \`mBTC.USDC\`).
|
|
34
35
|
- **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
|
-
- **
|
|
36
|
+
- **Execution Price**: \`open_position_simple\` no longer auto-fills a fresh Oracle price for \`MARKET\`; provide \`price\` explicitly when you want MCP to compute size / fee previews.
|
|
37
|
+
- **Funding Delta Ownership**: MCP no longer performs its own increase-order margin/deposit reconciliation. SDK \`createIncreaseOrder\` owns deposit-delta handling for new increase orders.
|
|
38
|
+
- **Pre-check Diagnostics**: \`check_account_ready\` now reports SDK \`availableMarginBalance\` first. If that read degrades, inspect \`summary.degraded\` and \`diagnostics.availableMarginError\` before trusting fallback account fields.
|
|
39
|
+
- **Beta Broker Mode**: Current Arbitrum Sepolia / Linea Sepolia test brokers auto-enable beta mode when \`IS_BETA_MODE\` is omitted.
|
|
36
40
|
- **Approval Safety**: Local fallback flows prefer exact approval sizing. Do not assume unlimited approvals are necessary.
|
|
37
41
|
- **Position Semantics**: \`size\` is BASE quantity, not USD notional. If a \`positionId\` is supplied, \`direction\` must match the live position.
|
|
38
42
|
- **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
43
|
- **LP Safety**: LP execution requires a fresh price snapshot and preview success; do not continue after preview failure.
|
|
44
|
+
- **LP Read Semantics**: Treat \`get_my_lp_holdings\` as an inventory listing, not a portfolio ranking by economic value.
|
|
40
45
|
|
|
41
46
|
## 3. Testnet Broker Reference
|
|
42
47
|
- Arbitrum test: \`0x895C4ae2A22bB26851011d733A9355f663a1F939\`
|
|
43
48
|
- Linea test: \`0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2\`
|
|
44
49
|
- Always keep \`RPC_URL\`, \`CHAIN_ID\`, and \`BROKER_ADDRESS\` on the same network.
|
|
50
|
+
- If \`IS_BETA_MODE\` is unset, MCP auto-detects beta mode for those two testnet brokers.
|
|
45
51
|
|
|
46
52
|
Current Session:
|
|
47
53
|
- Wallet: ${address}
|
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.
|
|
464
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.26" }, { 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.
|
|
585
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.24 running (stdio, pure on-chain, prod ready)");
|
|
586
586
|
}
|
|
587
587
|
main().catch((err) => {
|
|
588
588
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -5,10 +5,19 @@ import { ensureUnits, parseUserUnits } from "../utils/units.js";
|
|
|
5
5
|
import { normalizeAddress } from "../utils/address.js";
|
|
6
6
|
import { Contract } from "ethers";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
-
import { assertOracleFreshness } from "./marketService.js";
|
|
8
|
+
import { assertOracleFreshness, getFreshOraclePrice } from "./marketService.js";
|
|
9
9
|
const LP_DECIMALS = 18;
|
|
10
10
|
const POOL_MANAGER_BY_CHAIN = {
|
|
11
11
|
421614: "0xf268D9FeD3Bd56fd9aBdb4FeEb993338613678A8",
|
|
12
|
+
59141: "0x85e869d98216221807A06636541Ec93C9c0a4B0c",
|
|
13
|
+
97: "0x4F917ef137b573D9790b87e3cF6dfb698cF00c9c",
|
|
14
|
+
56: "0x13F2130c2F3bfd612BBCBF35FB9E467dd32bAF3A",
|
|
15
|
+
};
|
|
16
|
+
const POOL_MANAGER_BY_CHAIN_BETA = {
|
|
17
|
+
421614: "0x05314a21Fc97B74f168730153b2B63A870D25dE5",
|
|
18
|
+
59141: "0xcf51a6895864c6D8E507fC31EF16b9011287c5f4",
|
|
19
|
+
97: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
20
|
+
56: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
12
21
|
};
|
|
13
22
|
const POOL_MANAGER_ABI = [
|
|
14
23
|
"function deployPool((bytes32 marketId,address baseToken))",
|
|
@@ -52,6 +61,11 @@ const LP_ROUTER_BY_CHAIN_BETA = {
|
|
|
52
61
|
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
53
62
|
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
54
63
|
},
|
|
64
|
+
56: {
|
|
65
|
+
router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
|
|
66
|
+
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
67
|
+
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
68
|
+
},
|
|
55
69
|
};
|
|
56
70
|
const PREVIEW_POOL_ABI = [
|
|
57
71
|
"function previewLpAmountOut(bytes32,uint256,uint256) view returns (uint256)",
|
|
@@ -151,9 +165,10 @@ function getPoolManagerAddress(chainId) {
|
|
|
151
165
|
if (envAddress) {
|
|
152
166
|
return normalizeAddress(envAddress, "POOL_MANAGER_ADDRESS");
|
|
153
167
|
}
|
|
154
|
-
const
|
|
168
|
+
const source = isBetaModeEnabled() ? POOL_MANAGER_BY_CHAIN_BETA : POOL_MANAGER_BY_CHAIN;
|
|
169
|
+
const mapped = source[chainId];
|
|
155
170
|
if (!mapped) {
|
|
156
|
-
throw new Error(`Pool manager address is not configured for chainId=${chainId}. Set POOL_MANAGER_ADDRESS env var.`);
|
|
171
|
+
throw new Error(`Pool manager address is not configured for chainId=${chainId} (beta=${isBetaModeEnabled()}). Set POOL_MANAGER_ADDRESS env var.`);
|
|
157
172
|
}
|
|
158
173
|
return mapped;
|
|
159
174
|
}
|
|
@@ -183,12 +198,6 @@ function applyMinOutBySlippage(amountOut, slippage) {
|
|
|
183
198
|
const effective = ratioScaled >= scale ? 0n : (scale - ratioScaled);
|
|
184
199
|
return (amountOut * effective) / scale;
|
|
185
200
|
}
|
|
186
|
-
function shouldUseOraclePriceByState(state) {
|
|
187
|
-
const numeric = Number(state);
|
|
188
|
-
if (!Number.isFinite(numeric))
|
|
189
|
-
return true;
|
|
190
|
-
return numeric !== 0 && numeric !== 1;
|
|
191
|
-
}
|
|
192
201
|
async function getMarketDetailOrThrow(client, chainId, poolId) {
|
|
193
202
|
const raw = await client.markets.getMarketDetail({ chainId, poolId });
|
|
194
203
|
const detail = raw?.data ?? raw;
|
|
@@ -197,14 +206,14 @@ async function getMarketDetailOrThrow(client, chainId, poolId) {
|
|
|
197
206
|
}
|
|
198
207
|
return detail;
|
|
199
208
|
}
|
|
200
|
-
async function buildOraclePricePayload(client, chainId, poolId
|
|
209
|
+
async function buildOraclePricePayload(client, chainId, poolId) {
|
|
201
210
|
const oracle = await client.utils.getOraclePrice(poolId, chainId);
|
|
202
211
|
const vaa = String(oracle?.vaa ?? "").trim();
|
|
203
212
|
if (!vaa || !vaa.startsWith("0x")) {
|
|
204
213
|
throw new Error(`Oracle VAA unavailable for pool ${poolId}.`);
|
|
205
214
|
}
|
|
206
215
|
const publishTime = assertOracleFreshness(oracle?.publishTime, poolId);
|
|
207
|
-
const oracleType = Number.isFinite(Number(oracle?.oracleType)) ? Number(oracle.oracleType) :
|
|
216
|
+
const oracleType = Number.isFinite(Number(oracle?.oracleType)) ? Number(oracle.oracleType) : 1;
|
|
208
217
|
const value = toPositiveBigint(oracle?.value) ?? 0n;
|
|
209
218
|
const referencePrice30 = BigInt(ensureUnits(String(oracle?.price ?? "0"), 30, "oracle price"));
|
|
210
219
|
if (referencePrice30 <= 0n) {
|
|
@@ -266,14 +275,22 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
266
275
|
}
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
|
-
const needOraclePrice = shouldUseOraclePriceByState(marketDetail.state);
|
|
270
278
|
let oraclePayload = {
|
|
271
279
|
prices: [],
|
|
272
280
|
value: 0n,
|
|
273
281
|
referencePrice30: 0n,
|
|
274
282
|
};
|
|
275
|
-
|
|
276
|
-
oraclePayload = await buildOraclePricePayload(client, chainId, poolId
|
|
283
|
+
try {
|
|
284
|
+
oraclePayload = await buildOraclePricePayload(client, chainId, poolId);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
const fallbackPrice = await resolvePositiveMarketPrice30(client, poolId, chainId);
|
|
288
|
+
if (fallbackPrice && fallbackPrice > 0n) {
|
|
289
|
+
oraclePayload.referencePrice30 = fallbackPrice;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (oraclePayload.referencePrice30 <= 0n) {
|
|
293
|
+
throw new Error(`Oracle price unavailable for LP preview on pool ${poolId}.`);
|
|
277
294
|
}
|
|
278
295
|
const amountOut = await previewAmountOutForLiquidity(signer, chainId, poolId, poolType, action, amountIn, oraclePayload.referencePrice30);
|
|
279
296
|
const minAmountOut = applyMinOutBySlippage(amountOut, slippage);
|
|
@@ -349,30 +366,53 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
349
366
|
},
|
|
350
367
|
};
|
|
351
368
|
}
|
|
369
|
+
async function executeLiquidityTx(params) {
|
|
370
|
+
const { chainId, poolId, poolType, action, amount, slippage } = params;
|
|
371
|
+
const sdkAmount = Number(String(amount ?? "").trim());
|
|
372
|
+
const beforeHash = readLastMockTxHash();
|
|
373
|
+
if (!Number.isFinite(sdkAmount) || sdkAmount <= 0) {
|
|
374
|
+
logger.warn(`[LP SDK] amount=${String(amount)} is not a finite positive human number; using explicit router path for ${poolType} ${action}.`);
|
|
375
|
+
return executeLiquidityTxViaRouter(params);
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
return await withMutedSdkAbiMismatchLogs(async () => {
|
|
379
|
+
if (poolType === "QUOTE" && action === "deposit") {
|
|
380
|
+
return await quote.deposit({ chainId, poolId, amount: sdkAmount, slippage });
|
|
381
|
+
}
|
|
382
|
+
if (poolType === "QUOTE" && action === "withdraw") {
|
|
383
|
+
return await quote.withdraw({ chainId, poolId, amount: sdkAmount, slippage });
|
|
384
|
+
}
|
|
385
|
+
if (poolType === "BASE" && action === "deposit") {
|
|
386
|
+
return await base.deposit({ chainId, poolId, amount: sdkAmount, slippage });
|
|
387
|
+
}
|
|
388
|
+
return await base.withdraw({ chainId, poolId, amount: sdkAmount, slippage });
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
const message = extractErrorMessage(error);
|
|
393
|
+
const recovered = recoverSdkSubmittedTxHash(beforeHash, message, { poolType, action });
|
|
394
|
+
if (recovered) {
|
|
395
|
+
return recovered;
|
|
396
|
+
}
|
|
397
|
+
if (isAbiLengthMismatchError(message)) {
|
|
398
|
+
logger.warn(`[LP SDK] ABI overload mismatch for ${poolType} ${action}; falling back to explicit router path.`);
|
|
399
|
+
return executeLiquidityTxViaRouter(params);
|
|
400
|
+
}
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
352
404
|
async function resolvePositiveMarketPrice30(client, poolId, chainId) {
|
|
353
405
|
if (!client)
|
|
354
406
|
return null;
|
|
355
407
|
try {
|
|
356
|
-
const oracle = await client
|
|
357
|
-
|
|
358
|
-
const byPrice = toPositiveBigint(
|
|
408
|
+
const oracle = await getFreshOraclePrice(client, poolId, chainId);
|
|
409
|
+
const oraclePrice30 = ensureUnits(String(oracle?.price ?? "").trim(), 30, "oracle price");
|
|
410
|
+
const byPrice = toPositiveBigint(oraclePrice30);
|
|
359
411
|
if (byPrice)
|
|
360
412
|
return byPrice;
|
|
361
413
|
}
|
|
362
414
|
catch {
|
|
363
415
|
}
|
|
364
|
-
try {
|
|
365
|
-
const tickerRes = await client.markets?.getTickerList?.({ chainId, poolIds: [poolId] });
|
|
366
|
-
const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
|
|
367
|
-
if (row?.price) {
|
|
368
|
-
const tickerRaw = ensureUnits(row.price, 30, "ticker price");
|
|
369
|
-
const byTicker = toPositiveBigint(tickerRaw);
|
|
370
|
-
if (byTicker)
|
|
371
|
-
return byTicker;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
}
|
|
376
416
|
return null;
|
|
377
417
|
}
|
|
378
418
|
/**
|
|
@@ -473,37 +513,46 @@ export async function getPoolDetail(poolId, chainIdOverride) {
|
|
|
473
513
|
/**
|
|
474
514
|
* 获取流动性信息
|
|
475
515
|
*/
|
|
476
|
-
export async function getLiquidityInfo(client, poolId,
|
|
516
|
+
export async function getLiquidityInfo(client, poolId, chainIdOverride) {
|
|
477
517
|
const chainId = chainIdOverride ?? getChainId();
|
|
478
|
-
|
|
518
|
+
const oracle = await getFreshOraclePrice(client, poolId, chainId);
|
|
519
|
+
const marketPrice = ensureUnits(String(oracle?.price ?? "").trim(), 30, "oracle price");
|
|
520
|
+
const liquidityInfo = await client.utils.getLiquidityInfo({ chainId, poolId, marketPrice });
|
|
521
|
+
return {
|
|
522
|
+
liquidityInfo,
|
|
523
|
+
marketPrice,
|
|
524
|
+
marketPriceSource: "oracle",
|
|
525
|
+
oraclePublishTime: String(oracle?.publishTime ?? ""),
|
|
526
|
+
oracleType: oracle?.oracleType ?? null,
|
|
527
|
+
};
|
|
479
528
|
}
|
|
480
529
|
/**
|
|
481
530
|
* Quote 池 deposit
|
|
482
531
|
*/
|
|
483
532
|
export async function quoteDeposit(poolId, amount, slippage, chainIdOverride) {
|
|
484
533
|
const chainId = chainIdOverride ?? getChainId();
|
|
485
|
-
return
|
|
534
|
+
return executeLiquidityTx({ chainId, poolId, poolType: "QUOTE", action: "deposit", amount, slippage });
|
|
486
535
|
}
|
|
487
536
|
/**
|
|
488
537
|
* Quote 池 withdraw
|
|
489
538
|
*/
|
|
490
539
|
export async function quoteWithdraw(poolId, amount, slippage, chainIdOverride) {
|
|
491
540
|
const chainId = chainIdOverride ?? getChainId();
|
|
492
|
-
return
|
|
541
|
+
return executeLiquidityTx({ chainId, poolId, poolType: "QUOTE", action: "withdraw", amount, slippage });
|
|
493
542
|
}
|
|
494
543
|
/**
|
|
495
544
|
* Base 池 deposit
|
|
496
545
|
*/
|
|
497
546
|
export async function baseDeposit(poolId, amount, slippage, chainIdOverride) {
|
|
498
547
|
const chainId = chainIdOverride ?? getChainId();
|
|
499
|
-
return
|
|
548
|
+
return executeLiquidityTx({ chainId, poolId, poolType: "BASE", action: "deposit", amount, slippage });
|
|
500
549
|
}
|
|
501
550
|
/**
|
|
502
551
|
* Base 池 withdraw
|
|
503
552
|
*/
|
|
504
553
|
export async function baseWithdraw(poolId, amount, slippage, chainIdOverride) {
|
|
505
554
|
const chainId = chainIdOverride ?? getChainId();
|
|
506
|
-
return
|
|
555
|
+
return executeLiquidityTx({ chainId, poolId, poolType: "BASE", action: "withdraw", amount, slippage });
|
|
507
556
|
}
|
|
508
557
|
/**
|
|
509
558
|
* 获取 LP 价格
|
|
@@ -4,7 +4,6 @@ import { getChainId, getQuoteToken, getQuoteDecimals } from "../auth/resolveClie
|
|
|
4
4
|
import { ensureUnits } from "../utils/units.js";
|
|
5
5
|
import { normalizeAddress } from "../utils/address.js";
|
|
6
6
|
import { normalizeSlippagePct4dp } from "../utils/slippage.js";
|
|
7
|
-
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
8
7
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
9
8
|
import { mapTimeInForce } from "../utils/mappings.js";
|
|
10
9
|
import { getFreshOraclePrice } from "./marketService.js";
|
|
@@ -96,17 +95,6 @@ function parseDecimals(value, fallback) {
|
|
|
96
95
|
function normalizeIdentifier(value) {
|
|
97
96
|
return String(value ?? "").trim().toLowerCase();
|
|
98
97
|
}
|
|
99
|
-
function toBigIntOrZero(value) {
|
|
100
|
-
try {
|
|
101
|
-
const text = String(value ?? "").trim();
|
|
102
|
-
if (!text)
|
|
103
|
-
return 0n;
|
|
104
|
-
return BigInt(text);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return 0n;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
98
|
const ORDER_VALUE_SCALE = 1000000n;
|
|
111
99
|
const DECIMAL_INPUT_RE = /^\d+(\.\d+)?$/;
|
|
112
100
|
function parseScaledDecimal(value, scale, label) {
|
|
@@ -155,37 +143,6 @@ function validateIncreaseOrderEconomics(args) {
|
|
|
155
143
|
const priceHuman = formatUnits(priceRawBig, 30);
|
|
156
144
|
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}.`);
|
|
157
145
|
}
|
|
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
|
-
}
|
|
189
146
|
async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
|
|
190
147
|
let baseDecimals = 18;
|
|
191
148
|
let quoteDecimals = getQuoteDecimals();
|
|
@@ -283,16 +240,8 @@ export async function openPosition(client, address, args) {
|
|
|
283
240
|
const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee", { allowImplicitRaw: false });
|
|
284
241
|
const resolvedMarketId = String(args.marketId ?? poolData.marketId ?? "").trim();
|
|
285
242
|
if (!resolvedMarketId) {
|
|
286
|
-
throw new Error(`marketId is required
|
|
243
|
+
throw new Error(`marketId is required for poolId=${args.poolId}.`);
|
|
287
244
|
}
|
|
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);
|
|
296
245
|
validateIncreaseOrderEconomics({
|
|
297
246
|
collateralRaw,
|
|
298
247
|
sizeRaw,
|
|
@@ -302,43 +251,6 @@ export async function openPosition(client, address, args) {
|
|
|
302
251
|
quoteDecimals,
|
|
303
252
|
});
|
|
304
253
|
const timeInForce = mapTimeInForce(args.timeInForce);
|
|
305
|
-
// --- Auto-Deposit Logic (Strict) ---
|
|
306
|
-
const allowAutoDeposit = args.autoDeposit !== false;
|
|
307
|
-
console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
|
|
308
|
-
const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
|
|
309
|
-
let marginBalanceRaw = BigInt(0);
|
|
310
|
-
let walletBalanceRaw = BigInt(0);
|
|
311
|
-
if (marginInfo?.code === 0 && marginInfo?.data) {
|
|
312
|
-
marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
|
|
313
|
-
walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
314
|
-
}
|
|
315
|
-
const requiredRaw = spend.totalSpendRaw;
|
|
316
|
-
if (marginBalanceRaw < requiredRaw) {
|
|
317
|
-
if (!allowAutoDeposit) {
|
|
318
|
-
throw new Error(`Insufficient marginBalance (${marginBalanceRaw.toString()}) for required collateral (${requiredRaw.toString()}). ` +
|
|
319
|
-
`Deposit to trading account first (account_deposit) or retry with autoDeposit=true.`);
|
|
320
|
-
}
|
|
321
|
-
const neededRaw = requiredRaw - marginBalanceRaw;
|
|
322
|
-
console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
|
|
323
|
-
if (walletBalanceRaw < neededRaw) {
|
|
324
|
-
// Also check real wallet balance just in case account info wallet field is stale.
|
|
325
|
-
const realWalletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
326
|
-
const realWalletRaw = BigInt(realWalletRes?.data || "0");
|
|
327
|
-
if (realWalletRaw < neededRaw) {
|
|
328
|
-
throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + walletBalance (${walletBalanceRaw.toString()}) + realWallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
|
|
329
|
-
}
|
|
330
|
-
walletBalanceRaw = realWalletRaw;
|
|
331
|
-
}
|
|
332
|
-
console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet...`);
|
|
333
|
-
const depositRaw = await client.account.deposit({
|
|
334
|
-
amount: neededRaw.toString(),
|
|
335
|
-
tokenAddress: poolData.quoteToken,
|
|
336
|
-
chainId
|
|
337
|
-
});
|
|
338
|
-
const depositResult = await finalizeMutationResult(depositRaw, client.signer || (client.provider ? { provider: client.provider } : null), "auto_deposit");
|
|
339
|
-
console.log(`[tradeService] Auto-deposit confirmed in tx: ${depositResult.confirmation?.txHash}`);
|
|
340
|
-
}
|
|
341
|
-
// --- End Auto-Deposit Logic ---
|
|
342
254
|
const orderParams = {
|
|
343
255
|
chainId,
|
|
344
256
|
address,
|
|
@@ -364,7 +276,7 @@ export async function openPosition(client, address, args) {
|
|
|
364
276
|
orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false });
|
|
365
277
|
if (args.slPrice)
|
|
366
278
|
orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false });
|
|
367
|
-
return client.order.createIncreaseOrder(orderParams, tradingFeeRaw,
|
|
279
|
+
return client.order.createIncreaseOrder(orderParams, tradingFeeRaw, resolvedMarketId);
|
|
368
280
|
}
|
|
369
281
|
/**
|
|
370
282
|
* 平仓 / 减仓
|
|
@@ -31,20 +31,32 @@ export const checkAccountReadyTool = {
|
|
|
31
31
|
const detail = detailRes?.data || detailRes;
|
|
32
32
|
const quoteDecimals = Number(detail?.quoteDecimals ?? 6);
|
|
33
33
|
const quoteSymbol = detail?.quoteSymbol || "USDC";
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const availableMarginRes = await client.account.getAvailableMarginBalance({ chainId, address, poolId }).catch((error) => ({
|
|
35
|
+
code: -1,
|
|
36
|
+
message: error?.message || String(error),
|
|
37
|
+
}));
|
|
38
|
+
const availableMarginBalanceRaw = availableMarginRes?.code === 0
|
|
39
|
+
? toBigIntOrZero(availableMarginRes.data)
|
|
40
|
+
: 0n;
|
|
41
|
+
const marginInfo = await client.account.getAccountInfo(chainId, address, poolId).catch(() => null);
|
|
36
42
|
let accountWalletBalanceRaw = 0n;
|
|
43
|
+
let freeMarginRaw = 0n;
|
|
44
|
+
let quoteProfitRaw = 0n;
|
|
45
|
+
let lockedMarginRaw = 0n;
|
|
37
46
|
if (marginInfo?.code === 0 && marginInfo?.data) {
|
|
38
|
-
|
|
47
|
+
freeMarginRaw = toBigIntOrZero(marginInfo.data.freeMargin);
|
|
48
|
+
quoteProfitRaw = toBigIntOrZero(marginInfo.data.quoteProfit);
|
|
49
|
+
lockedMarginRaw = toBigIntOrZero(marginInfo.data.lockedMargin);
|
|
39
50
|
accountWalletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
40
51
|
}
|
|
41
52
|
const walletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
42
53
|
const walletBalanceRaw = BigInt(walletRes?.data || "0");
|
|
43
54
|
const requiredRaw = BigInt(parseUserUnits(args.collateralAmount, quoteDecimals, "required"));
|
|
44
|
-
const isReady = (
|
|
45
|
-
const deficitRaw = requiredRaw >
|
|
55
|
+
const isReady = (availableMarginBalanceRaw >= requiredRaw) || (availableMarginBalanceRaw + walletBalanceRaw >= requiredRaw);
|
|
56
|
+
const deficitRaw = requiredRaw > availableMarginBalanceRaw ? requiredRaw - availableMarginBalanceRaw : 0n;
|
|
46
57
|
const needDepositFromWallet = deficitRaw > 0n;
|
|
47
58
|
const walletSufficient = walletBalanceRaw >= deficitRaw;
|
|
59
|
+
const degraded = availableMarginRes?.code !== 0;
|
|
48
60
|
const format = (v) => ethers.formatUnits(v, quoteDecimals);
|
|
49
61
|
return {
|
|
50
62
|
content: [{
|
|
@@ -54,14 +66,22 @@ export const checkAccountReadyTool = {
|
|
|
54
66
|
data: {
|
|
55
67
|
isReady,
|
|
56
68
|
neededTotal: format(requiredRaw),
|
|
57
|
-
|
|
69
|
+
currentAvailableMarginBalance: format(availableMarginBalanceRaw),
|
|
58
70
|
currentWalletBalance: format(walletBalanceRaw),
|
|
59
71
|
summary: {
|
|
60
|
-
hasEnoughInMargin:
|
|
72
|
+
hasEnoughInMargin: availableMarginBalanceRaw >= requiredRaw,
|
|
61
73
|
needDepositFromWallet,
|
|
62
74
|
walletSufficientForDeposit: walletSufficient,
|
|
63
75
|
accountInfoWalletBalance: format(accountWalletBalanceRaw),
|
|
76
|
+
degraded,
|
|
64
77
|
quoteSymbol
|
|
78
|
+
},
|
|
79
|
+
diagnostics: {
|
|
80
|
+
sdkAvailableMarginBalance: format(availableMarginBalanceRaw),
|
|
81
|
+
accountInfoFreeMargin: format(freeMarginRaw),
|
|
82
|
+
accountInfoQuoteProfit: format(quoteProfitRaw),
|
|
83
|
+
accountInfoLockedMargin: format(lockedMarginRaw),
|
|
84
|
+
availableMarginError: degraded ? String(availableMarginRes?.message || "Failed to get available margin balance") : null,
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
87
|
}, null, 2)
|
|
@@ -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.union([z.number(), z.string()]).describe("SDK v1.0.
|
|
44
|
+
timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 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"),
|
|
@@ -32,7 +32,7 @@ export const executeTradeTool = {
|
|
|
32
32
|
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
|
|
33
33
|
size: z.union([z.string(), z.number()]).describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
|
|
34
34
|
price: z.union([z.string(), z.number()]).describe("Execution or Limit price. e.g. '65000' or 'raw:...'"),
|
|
35
|
-
timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.
|
|
35
|
+
timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
|
|
36
36
|
postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
|
|
37
37
|
slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
|
|
38
38
|
executionFeeToken: z.string().optional().describe("Address of token to pay gas/execution fees (typically USDC). Default is pool quoteToken."),
|
|
@@ -121,9 +121,6 @@ function toSymbol(row) {
|
|
|
121
121
|
return base;
|
|
122
122
|
return normalizePoolId(row?.poolId ?? row?.pool_id);
|
|
123
123
|
}
|
|
124
|
-
function sumRaw(baseRaw, quoteRaw) {
|
|
125
|
-
return BigInt(baseRaw || "0") + BigInt(quoteRaw || "0");
|
|
126
|
-
}
|
|
127
124
|
async function readErc20Balance(provider, tokenAddress, holder) {
|
|
128
125
|
const contract = new Contract(tokenAddress, ERC20_BALANCE_ABI, provider);
|
|
129
126
|
const balance = await contract.balanceOf(holder);
|
|
@@ -131,20 +128,25 @@ async function readErc20Balance(provider, tokenAddress, holder) {
|
|
|
131
128
|
}
|
|
132
129
|
export const getMyLpHoldingsTool = {
|
|
133
130
|
name: "get_my_lp_holdings",
|
|
134
|
-
description: "[ACCOUNT] List your LP holdings across pools on the current chain by reading base/quote LP token balances. Includes standardized LP asset names: base LP `mBASE.QUOTE`, quote LP `mQUOTE.BASE`.",
|
|
131
|
+
description: "[ACCOUNT] List your LP holdings across pools on the current wallet chain by reading base/quote LP token balances. Includes standardized LP asset names: base LP `mBASE.QUOTE`, quote LP `mQUOTE.BASE`.",
|
|
135
132
|
schema: {
|
|
136
133
|
includeZero: z.coerce.boolean().optional().describe("If true, include pools with zero LP balances (default false)."),
|
|
137
134
|
poolIds: z.union([z.array(z.string()).min(1), z.string().min(1)]).optional().describe("Optional poolId filter. Supports array, JSON-array string, comma string, or single poolId."),
|
|
138
135
|
maxPools: z.coerce.number().int().positive().max(2000).optional().describe("Optional cap for scanned pools (default all)."),
|
|
136
|
+
chainId: z.coerce.number().int().positive().optional().describe("Optional chainId hint. Must match the active wallet/provider chain for LP balance reads."),
|
|
139
137
|
},
|
|
140
138
|
handler: async (args) => {
|
|
141
139
|
try {
|
|
142
140
|
const { client, address, signer } = await resolveClient();
|
|
143
|
-
const
|
|
141
|
+
const activeChainId = getChainId();
|
|
142
|
+
const chainId = args.chainId ?? activeChainId;
|
|
144
143
|
const provider = signer?.provider;
|
|
145
144
|
if (!provider) {
|
|
146
145
|
throw new Error("Provider is unavailable for LP balance reads.");
|
|
147
146
|
}
|
|
147
|
+
if (chainId !== activeChainId) {
|
|
148
|
+
throw new Error(`get_my_lp_holdings reads balances from the active wallet/provider chain only. Requested chainId=${chainId}, active chainId=${activeChainId}. Switch MCP network config first, then retry.`);
|
|
149
|
+
}
|
|
148
150
|
const includeZero = !!args.includeZero;
|
|
149
151
|
const poolIdsFilter = normalizePoolIdsInput(args.poolIds);
|
|
150
152
|
const filterSet = new Set(poolIdsFilter.map((item) => item.toLowerCase()));
|
|
@@ -227,11 +229,10 @@ export const getMyLpHoldingsTool = {
|
|
|
227
229
|
});
|
|
228
230
|
}
|
|
229
231
|
items.sort((left, right) => {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return rightSum > leftSum ? 1 : -1;
|
|
232
|
+
const symbolCompare = String(left.symbol ?? "").localeCompare(String(right.symbol ?? ""));
|
|
233
|
+
if (symbolCompare !== 0)
|
|
234
|
+
return symbolCompare;
|
|
235
|
+
return String(left.poolId ?? "").localeCompare(String(right.poolId ?? ""));
|
|
235
236
|
});
|
|
236
237
|
const payload = {
|
|
237
238
|
meta: {
|
|
@@ -4,21 +4,8 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
|
4
4
|
import { getMarketDetail, resolvePool } from "../services/marketService.js";
|
|
5
5
|
import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
|
|
6
6
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
7
|
-
import { parseUserPrice30 } from "../utils/units.js";
|
|
8
7
|
const INTEGER_RE = /^\d+$/;
|
|
9
8
|
const SIGNED_INTEGER_RE = /^-?\d+$/;
|
|
10
|
-
const DECIMAL_RE = /^\d+(\.\d+)?$/;
|
|
11
|
-
function normalizeMarketPrice30Input(value) {
|
|
12
|
-
const text = String(value ?? "").trim();
|
|
13
|
-
if (!text)
|
|
14
|
-
return "";
|
|
15
|
-
if (INTEGER_RE.test(text))
|
|
16
|
-
return text;
|
|
17
|
-
if (!DECIMAL_RE.test(text) && !/^raw:/i.test(text) && !/^human:/i.test(text)) {
|
|
18
|
-
throw new Error("marketPrice must be numeric, or prefixed with raw:/human:.");
|
|
19
|
-
}
|
|
20
|
-
return parseUserPrice30(text, "marketPrice");
|
|
21
|
-
}
|
|
22
9
|
function compactWarning(scope, err) {
|
|
23
10
|
const raw = extractErrorMessage(err);
|
|
24
11
|
const flat = raw.replace(/\s+/g, " ").trim();
|
|
@@ -211,8 +198,8 @@ export const getPoolMetadataTool = {
|
|
|
211
198
|
schema: {
|
|
212
199
|
poolId: z.string().optional().describe("Pool ID, Token Address, or Keyword"),
|
|
213
200
|
keyword: z.string().optional().describe("Market keyword (e.g. 'BTC')"),
|
|
214
|
-
includeLiquidity: z.boolean().default(false).describe("Whether to include liquidity depth (
|
|
215
|
-
marketPrice: z.union([z.string(), z.number()]).optional().describe("
|
|
201
|
+
includeLiquidity: z.boolean().default(false).describe("Whether to include liquidity depth (uses fresh oracle price automatically)"),
|
|
202
|
+
marketPrice: z.union([z.string(), z.number()]).optional().describe("Deprecated and ignored. MCP now uses fresh oracle price for liquidity depth."),
|
|
216
203
|
includeConfig: z.boolean().default(false).describe("Whether to include pool level configuration/limits"),
|
|
217
204
|
chainId: z.number().int().positive().optional().describe("Optional chainId override"),
|
|
218
205
|
},
|
|
@@ -245,10 +232,15 @@ export const getPoolMetadataTool = {
|
|
|
245
232
|
// 3. Optional: Liquidity Info
|
|
246
233
|
if (args.includeLiquidity) {
|
|
247
234
|
try {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
235
|
+
const liquidityResult = await getLiquidityInfo(client, poolId, chainId);
|
|
236
|
+
results.liquidityInfo = liquidityResult.liquidityInfo;
|
|
237
|
+
results.liquidityInfoMeta = {
|
|
238
|
+
marketPriceSource: liquidityResult.marketPriceSource,
|
|
239
|
+
marketPrice: liquidityResult.marketPrice,
|
|
240
|
+
oraclePublishTime: liquidityResult.oraclePublishTime,
|
|
241
|
+
oracleType: liquidityResult.oracleType,
|
|
242
|
+
ignoredUserMarketPrice: args.marketPrice !== undefined ? String(args.marketPrice) : null,
|
|
243
|
+
};
|
|
252
244
|
}
|
|
253
245
|
catch (err) {
|
|
254
246
|
errors.push(compactWarning("liquidityInfo", err));
|
|
@@ -113,7 +113,7 @@ export const manageLiquidityTool = {
|
|
|
113
113
|
: await baseWithdraw(poolId, amount, slippage, chainId);
|
|
114
114
|
}
|
|
115
115
|
if (!raw) {
|
|
116
|
-
throw new Error(`SDK returned an empty result for liquidity ${action}. This usually
|
|
116
|
+
throw new Error(`SDK returned an empty result for liquidity ${action}. This usually indicates a contract-level restriction or an unavailable LP execution path. Please check get_pool_metadata and retry.`);
|
|
117
117
|
}
|
|
118
118
|
if (raw && typeof raw === "object" && "code" in raw && Number(raw.code) !== 0) {
|
|
119
119
|
throw new Error(`Liquidity ${action} failed: ${extractErrorMessage(raw)}`);
|
package/dist/tools/manageTpSl.js
CHANGED
|
@@ -73,9 +73,11 @@ function hasOwnKey(input, key) {
|
|
|
73
73
|
return !!input && typeof input === "object" && Object.prototype.hasOwnProperty.call(input, key);
|
|
74
74
|
}
|
|
75
75
|
function isDeleteTpSlIntent(args) {
|
|
76
|
-
if (
|
|
77
|
-
return
|
|
78
|
-
|
|
76
|
+
if (isExplicitZeroValue(args.tpPrice) && isExplicitZeroValue(args.slPrice))
|
|
77
|
+
return true;
|
|
78
|
+
if (isExplicitZeroValue(args.price))
|
|
79
|
+
return true;
|
|
80
|
+
return false;
|
|
79
81
|
}
|
|
80
82
|
function isInvalidParameterRevert(error) {
|
|
81
83
|
const message = String(error?.message ?? error ?? "").toLowerCase();
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { OrderType } from "@myx-trade/sdk";
|
|
3
3
|
import { formatUnits } from "ethers";
|
|
4
4
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
5
|
-
import { resolvePool
|
|
5
|
+
import { resolvePool } from "../services/marketService.js";
|
|
6
6
|
import { openPosition } from "../services/tradeService.js";
|
|
7
7
|
import { isZeroAddress, normalizeAddress } from "../utils/address.js";
|
|
8
8
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
@@ -10,6 +10,7 @@ import { mapDirection, mapOrderType } from "../utils/mappings.js";
|
|
|
10
10
|
import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
11
11
|
import { parseUserPrice30, parseUserUnits } from "../utils/units.js";
|
|
12
12
|
import { verifyTradeOutcome } from "../utils/verification.js";
|
|
13
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
13
14
|
const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
|
|
14
15
|
function pow10(decimals) {
|
|
15
16
|
if (!Number.isInteger(decimals) || decimals < 0) {
|
|
@@ -100,7 +101,7 @@ export const openPositionSimpleTool = {
|
|
|
100
101
|
autoDeposit: z.coerce
|
|
101
102
|
.boolean()
|
|
102
103
|
.optional()
|
|
103
|
-
.describe("
|
|
104
|
+
.describe("Deprecated compatibility flag. SDK now handles deposit deltas during order creation."),
|
|
104
105
|
dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
|
|
105
106
|
},
|
|
106
107
|
handler: async (args) => {
|
|
@@ -150,18 +151,15 @@ export const openPositionSimpleTool = {
|
|
|
150
151
|
// 4) Determine reference price (30 decimals)
|
|
151
152
|
let price30;
|
|
152
153
|
let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const userPrice = String(args.price ?? "").trim();
|
|
160
|
-
if (!userPrice)
|
|
161
|
-
throw new Error("price is required for LIMIT/STOP.");
|
|
162
|
-
price30 = parseUserPrice30(userPrice, "price");
|
|
163
|
-
priceMeta = { source: "user", publishTime: null, oracleType: null, human: userPrice };
|
|
154
|
+
const userPrice = String(args.price ?? "").trim();
|
|
155
|
+
if (!userPrice) {
|
|
156
|
+
if (orderType === OrderType.MARKET) {
|
|
157
|
+
throw new Error("price is required for MARKET orders. MCP no longer auto-fills a fresh Oracle price for MARKET.");
|
|
158
|
+
}
|
|
159
|
+
throw new Error("price is required for LIMIT/STOP.");
|
|
164
160
|
}
|
|
161
|
+
price30 = parseUserPrice30(userPrice, "price");
|
|
162
|
+
priceMeta = { source: "user", publishTime: null, oracleType: null, human: userPrice };
|
|
165
163
|
const price30Big = asBigint(price30, "price");
|
|
166
164
|
if (price30Big <= 0n)
|
|
167
165
|
throw new Error("price must be > 0.");
|
|
@@ -352,7 +350,7 @@ export const openPositionSimpleTool = {
|
|
|
352
350
|
};
|
|
353
351
|
}
|
|
354
352
|
catch (error) {
|
|
355
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
353
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
356
354
|
}
|
|
357
355
|
},
|
|
358
356
|
};
|
|
@@ -82,6 +82,7 @@ const TOOL_DISCOVERY_META = {
|
|
|
82
82
|
category: "liquidity",
|
|
83
83
|
aliases: ["my lp", "lp balances"],
|
|
84
84
|
intents: ["list my liquidity", "portfolio lp"],
|
|
85
|
+
commonArgs: ["includeZero", "poolIds", "maxPools", "chainId"],
|
|
85
86
|
},
|
|
86
87
|
get_account_snapshot: {
|
|
87
88
|
category: "account",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractContractErrorFromText } from "./errors.js";
|
|
1
2
|
const USELESS_MESSAGE_SET = new Set([
|
|
2
3
|
"",
|
|
3
4
|
"undefined",
|
|
@@ -26,6 +27,39 @@ function safeStringify(value) {
|
|
|
26
27
|
return cleanMessage(String(value ?? ""));
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
function extractContractError(value) {
|
|
31
|
+
const visited = new Set();
|
|
32
|
+
const scan = (input, depth) => {
|
|
33
|
+
if (depth > 6 || input === null || input === undefined)
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof input === "string") {
|
|
36
|
+
return extractContractErrorFromText(input);
|
|
37
|
+
}
|
|
38
|
+
if (typeof input === "number" || typeof input === "boolean" || typeof input === "bigint") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (input instanceof Error) {
|
|
42
|
+
return scan(input.message, depth + 1) ?? scan(input.cause, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
if (!isRecord(input))
|
|
45
|
+
return null;
|
|
46
|
+
if (visited.has(input))
|
|
47
|
+
return null;
|
|
48
|
+
visited.add(input);
|
|
49
|
+
for (const key of ["data", "message", "reason", "shortMessage", "error", "cause", "info", "details"]) {
|
|
50
|
+
const decoded = scan(input[key], depth + 1);
|
|
51
|
+
if (decoded)
|
|
52
|
+
return decoded;
|
|
53
|
+
}
|
|
54
|
+
for (const nested of Object.values(input)) {
|
|
55
|
+
const decoded = scan(nested, depth + 1);
|
|
56
|
+
if (decoded)
|
|
57
|
+
return decoded;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
return scan(value, 0);
|
|
62
|
+
}
|
|
29
63
|
export function isMeaningfulErrorMessage(message) {
|
|
30
64
|
return cleanMessage(message) !== null;
|
|
31
65
|
}
|
|
@@ -65,5 +99,10 @@ export function extractErrorMessage(error, fallback = "Unknown error") {
|
|
|
65
99
|
}
|
|
66
100
|
return safeStringify(value);
|
|
67
101
|
};
|
|
68
|
-
|
|
102
|
+
const base = read(error, 0) ?? fallback;
|
|
103
|
+
const decodedContractError = extractContractError(error);
|
|
104
|
+
if (decodedContractError && !base.includes(decodedContractError)) {
|
|
105
|
+
return `${base} (Decoded: ${decodedContractError})`;
|
|
106
|
+
}
|
|
107
|
+
return base;
|
|
69
108
|
}
|
package/dist/utils/errors.js
CHANGED
|
@@ -254,3 +254,64 @@ export function decodeErrorSelector(errorData) {
|
|
|
254
254
|
const selector = hex.slice(0, 8);
|
|
255
255
|
return CONTRACT_ERRORS[selector] || null;
|
|
256
256
|
}
|
|
257
|
+
function normalizeErrorHex(errorData) {
|
|
258
|
+
if (!errorData || typeof errorData !== "string")
|
|
259
|
+
return null;
|
|
260
|
+
let hex = errorData.trim().toLowerCase();
|
|
261
|
+
if (!hex)
|
|
262
|
+
return null;
|
|
263
|
+
if (hex.startsWith("0x"))
|
|
264
|
+
hex = hex.slice(2);
|
|
265
|
+
if (!/^[0-9a-f]+$/i.test(hex) || hex.length < 8)
|
|
266
|
+
return null;
|
|
267
|
+
return hex;
|
|
268
|
+
}
|
|
269
|
+
function readUint256Arg(hex, argIndex) {
|
|
270
|
+
const start = 8 + (argIndex * 64);
|
|
271
|
+
const end = start + 64;
|
|
272
|
+
if (hex.length < end)
|
|
273
|
+
return null;
|
|
274
|
+
try {
|
|
275
|
+
return BigInt(`0x${hex.slice(start, end)}`);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export function describeContractError(errorData) {
|
|
282
|
+
const hex = normalizeErrorHex(errorData);
|
|
283
|
+
if (!hex)
|
|
284
|
+
return null;
|
|
285
|
+
const selector = hex.slice(0, 8);
|
|
286
|
+
const name = CONTRACT_ERRORS[selector];
|
|
287
|
+
if (!name)
|
|
288
|
+
return null;
|
|
289
|
+
if (selector === "ffd10028") {
|
|
290
|
+
const current = readUint256Arg(hex, 0);
|
|
291
|
+
const required = readUint256Arg(hex, 1);
|
|
292
|
+
if (current !== null && required !== null) {
|
|
293
|
+
return `${name} [current=${current.toString()}, required=${required.toString()}]`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (selector === "71c4efed") {
|
|
297
|
+
const expected = readUint256Arg(hex, 0);
|
|
298
|
+
const actual = readUint256Arg(hex, 1);
|
|
299
|
+
if (expected !== null && actual !== null) {
|
|
300
|
+
return `${name} [expected=${expected.toString()}, actual=${actual.toString()}]`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return name;
|
|
304
|
+
}
|
|
305
|
+
export function extractContractErrorFromText(text) {
|
|
306
|
+
if (!text || typeof text !== "string")
|
|
307
|
+
return null;
|
|
308
|
+
const matches = text.match(/0x[0-9a-fA-F]{8,}/g);
|
|
309
|
+
if (!matches)
|
|
310
|
+
return null;
|
|
311
|
+
for (const match of matches) {
|
|
312
|
+
const decoded = describeContractError(match);
|
|
313
|
+
if (decoded)
|
|
314
|
+
return decoded;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
package/dist/utils/mappings.js
CHANGED
|
@@ -171,7 +171,7 @@ export const mapTriggerType = (input) => {
|
|
|
171
171
|
throw new Error(`Invalid triggerType: ${input}. Use 0/NONE, 1/GTE or 2/LTE.`);
|
|
172
172
|
};
|
|
173
173
|
/**
|
|
174
|
-
* SDK v1.0.
|
|
174
|
+
* SDK v1.0.4-beta.4 currently exposes IOC only.
|
|
175
175
|
*/
|
|
176
176
|
export const mapTimeInForce = (input) => {
|
|
177
177
|
if (input === 0)
|
|
@@ -179,5 +179,5 @@ export const mapTimeInForce = (input) => {
|
|
|
179
179
|
const s = String(input ?? "").trim().toUpperCase();
|
|
180
180
|
if (s === "0" || s === "IOC")
|
|
181
181
|
return 0;
|
|
182
|
-
throw new Error(`Invalid timeInForce: ${input}. SDK v1.0.
|
|
182
|
+
throw new Error(`Invalid timeInForce: ${input}. SDK v1.0.4-beta.4 currently supports IOC only, use 0/IOC.`);
|
|
183
183
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michaleffffff/mcp-trading-server",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"myx-mcp": "dist/server.js"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
24
|
-
"@myx-trade/sdk": "^1.0.
|
|
24
|
+
"@myx-trade/sdk": "^1.0.4-beta.4",
|
|
25
25
|
"dotenv": "^17.3.1",
|
|
26
26
|
"ethers": "^6.13.1",
|
|
27
27
|
"zod": "^4.3.6"
|