@michaleffffff/mcp-trading-server 3.0.31 → 3.1.1
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 +17 -0
- package/README.md +5 -6
- package/TOOL_EXAMPLES.md +2 -4
- package/dist/auth/resolveClient.js +1 -14
- package/dist/prompts/tradingGuide.js +2 -3
- package/dist/server.js +2 -2
- package/dist/services/balanceService.js +2 -2
- package/dist/services/marketService.js +38 -10
- package/dist/services/poolService.js +3 -36
- package/dist/tools/accountInfo.js +1 -4
- package/dist/tools/accountTransfer.js +13 -1
- package/dist/tools/cancelOrders.js +5 -1
- package/dist/tools/checkAccountReady.js +6 -2
- package/dist/tools/closePosition.js +23 -6
- package/dist/tools/executeTrade.js +28 -12
- package/dist/tools/getAllTickers.js +1 -1
- package/dist/tools/getBaseDetail.js +28 -5
- package/dist/tools/getMyLpHoldings.js +244 -2
- package/dist/tools/listPools.js +2 -1
- package/dist/tools/manageTpSl.js +4 -2
- package/dist/tools/openPositionSimple.js +72 -15
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.1.1 - 2026-03-23
|
|
4
|
+
### Changed
|
|
5
|
+
- Upgraded the active SDK baseline to `@myx-trade/sdk@^1.0.4`.
|
|
6
|
+
- Synced operator-facing docs to the current baseline:
|
|
7
|
+
- `README.md` now references `@myx-trade/sdk@^1.0.4`.
|
|
8
|
+
- `mcp_config_guide.md` no longer refers to a separate Beta environment for LP / create-market flows.
|
|
9
|
+
- Current testnet live regression notes now align with the post-upgrade trade/account behavior.
|
|
10
|
+
- Deduplicated SDK compatibility helpers used by market lookup / `get_base_detail` without changing runtime behavior.
|
|
11
|
+
|
|
12
|
+
## 3.1.0 - 2026-03-20
|
|
13
|
+
### Changed
|
|
14
|
+
- 发布 `3.1.0`,统一版本元数据到新的主次版本号。
|
|
15
|
+
- 运行环境现在只区分测试环境与正式环境,不再走 beta host / beta 地址分支。
|
|
16
|
+
- 修复测试环境下的市场发现链路,`list_pools` 恢复返回当前链池列表,`get_all_tickers` 在主接口不可用时继续走 fallback。
|
|
17
|
+
- `open_position_simple` 在用户提供 `size + price + leverage` 但未提供 `collateralAmount` 时,改为自动反推保证金;若用户传入的 `collateralAmount` 明显不足,则提前返回清晰错误。
|
|
18
|
+
- `execute_trade` / `close_position` 不再对外暴露 `timeInForce`;MCP 内部固定使用 `IOC (0)`,避免把一个不可配置字段误导成可配置参数。
|
|
19
|
+
|
|
3
20
|
## 3.0.31 - 2026-03-20
|
|
4
21
|
### Fixed
|
|
5
22
|
- 修复 `normalizeSlippageRatio` 在处理 1% 到 100% 之间数值时的歧义问题。
|
package/README.md
CHANGED
|
@@ -6,11 +6,12 @@ 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.4
|
|
9
|
+
- **Current release: 3.1.0**
|
|
10
|
+
- **SDK baseline**: `@myx-trade/sdk@^1.0.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
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
|
+
- **Regression status**: Current trade/account compatibility was verified against the `1.0.4` baseline, including testnet live checks for `get_trade_flow`, `get_base_detail`, `account_deposit`, `account_withdraw`, `cancel_orders`, `manage_tp_sl`, and `close_position`.
|
|
14
15
|
- **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
|
|
15
16
|
|
|
16
17
|
---
|
|
@@ -37,7 +38,6 @@ CHAIN_ID=...
|
|
|
37
38
|
BROKER_ADDRESS=0x...
|
|
38
39
|
QUOTE_TOKEN_ADDRESS=0x...
|
|
39
40
|
QUOTE_TOKEN_DECIMALS=...
|
|
40
|
-
IS_BETA_MODE=true
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## Testnet `MYXBroker` Reference
|
|
@@ -46,7 +46,6 @@ IS_BETA_MODE=true
|
|
|
46
46
|
- Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
47
47
|
|
|
48
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.
|
|
50
49
|
|
|
51
50
|
---
|
|
52
51
|
|
|
@@ -91,7 +90,6 @@ If `IS_BETA_MODE` is omitted, MCP now auto-detects beta mode for the two testnet
|
|
|
91
90
|
|
|
92
91
|
- **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
92
|
- **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.
|
|
95
93
|
- **Exact approvals by default**: local fallback flows now prefer exact approvals instead of implicit unlimited approval.
|
|
96
94
|
- **Size semantics**: `size` always means base-asset quantity, not USD notional.
|
|
97
95
|
- **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`.
|
|
@@ -137,8 +135,9 @@ Use these conventions when generating tool arguments:
|
|
|
137
135
|
- `status`: `OPEN` / `HISTORY` / `ALL` are canonical; lowercase is tolerated
|
|
138
136
|
- `poolType`: `BASE` / `QUOTE` are canonical; lowercase is tolerated
|
|
139
137
|
- `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
|
|
140
|
-
-
|
|
138
|
+
- Trading MCP tools now fix `timeInForce` internally to `IOC (0)`; do not pass this field
|
|
141
139
|
- `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
|
|
140
|
+
- `open_position_simple`: if `size + price + leverage` are provided, MCP can auto-compute `collateralAmount`; if `collateralAmount` is also provided, MCP validates it against the requested notional
|
|
142
141
|
- `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
|
|
143
142
|
- `slippagePct`: trading tools use 4-decimal raw units where `100 = 1.00%` and `50 = 0.50%`
|
|
144
143
|
- Human units: `"100"` means 100 USDC or 100 token units depending on field
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -81,7 +81,7 @@ Recommended high-level entry tool.
|
|
|
81
81
|
|
|
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
|
+
If `size + price + leverage` are provided on `open_position_simple`, MCP can now auto-compute `collateralAmount`. If you also provide `collateralAmount`, MCP validates that it is sufficient for the requested position.
|
|
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
87
|
`autoDeposit` is now a deprecated compatibility flag. MCP delegates increase-order funding deltas to the SDK during `createIncreaseOrder`.
|
|
@@ -116,13 +116,12 @@ Low-level increase-order tool when you want full control.
|
|
|
116
116
|
"price": "2500",
|
|
117
117
|
"size": "0.2",
|
|
118
118
|
"collateralAmount": "100",
|
|
119
|
-
"timeInForce": 0,
|
|
120
119
|
"leverage": 5
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
```
|
|
124
123
|
|
|
125
|
-
`timeInForce`
|
|
124
|
+
`timeInForce` is fixed internally to `IOC (0)` in MCP trading tools; do not pass this field.
|
|
126
125
|
`executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
|
|
127
126
|
If `positionId` is supplied on increase flows, `direction` must remain consistent with the live position.
|
|
128
127
|
|
|
@@ -140,7 +139,6 @@ Close or reduce a position. Use `ALL` for a full close.
|
|
|
140
139
|
"collateralAmount": "ALL",
|
|
141
140
|
"size": "ALL",
|
|
142
141
|
"price": "2200",
|
|
143
|
-
"timeInForce": 0,
|
|
144
142
|
"postOnly": false,
|
|
145
143
|
"slippagePct": "50",
|
|
146
144
|
"executionFeeToken": "0x...",
|
|
@@ -2,10 +2,6 @@ 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
|
-
};
|
|
9
5
|
function getDefaultBrokerByChainId(chainId) {
|
|
10
6
|
// Testnet mappings
|
|
11
7
|
if (chainId === 421614)
|
|
@@ -22,14 +18,6 @@ function getDefaultQuoteTokenByChainId(chainId) {
|
|
|
22
18
|
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b"; // Linea Sepolia
|
|
23
19
|
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b";
|
|
24
20
|
}
|
|
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
|
-
}
|
|
33
21
|
export async function resolveClient() {
|
|
34
22
|
if (cached)
|
|
35
23
|
return cached;
|
|
@@ -50,7 +38,6 @@ export async function resolveClient() {
|
|
|
50
38
|
throw new Error("QUOTE_TOKEN_ADDRESS env var is required.");
|
|
51
39
|
const brokerAddress = normalizeAddress(brokerAddressRaw, "BROKER_ADDRESS");
|
|
52
40
|
const quoteToken = normalizeAddress(quoteTokenRaw, "QUOTE_TOKEN_ADDRESS");
|
|
53
|
-
const isBetaMode = resolveIsBetaMode(chainId, brokerAddress);
|
|
54
41
|
const provider = new JsonRpcProvider(rpcUrl);
|
|
55
42
|
const signer = new Wallet(privateKey, provider);
|
|
56
43
|
// Inject the EIP-1193 mock so SDK can sign transactions seamlessly
|
|
@@ -75,7 +62,7 @@ export async function resolveClient() {
|
|
|
75
62
|
signer: signer,
|
|
76
63
|
brokerAddress,
|
|
77
64
|
isTestnet,
|
|
78
|
-
isBetaMode,
|
|
65
|
+
isBetaMode: false,
|
|
79
66
|
walletClient: walletClient
|
|
80
67
|
});
|
|
81
68
|
cached = { client, address: signer.address, signer, chainId, quoteToken, quoteDecimals };
|
|
@@ -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
|
|
15
|
+
# MYX Trading MCP Best Practices (v3.1.0)
|
|
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
|
|
|
@@ -34,9 +34,9 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
34
34
|
- **LP Strategy**: Use \`get_my_lp_holdings\` to monitor liquidity positions. Naming follows \`mBASE.QUOTE\` (e.g., \`mBTC.USDC\`).
|
|
35
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.
|
|
36
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
|
+
- **Collateral Auto-Compute**: If you provide \`size + price + leverage\` to \`open_position_simple\`, MCP can infer \`collateralAmount\` automatically. If you provide \`collateralAmount\` too, MCP validates that it is sufficient for the intended notional.
|
|
37
38
|
- **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
39
|
- **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.
|
|
40
40
|
- **Approval Safety**: Local fallback flows prefer exact approval sizing. Do not assume unlimited approvals are necessary.
|
|
41
41
|
- **Position Semantics**: \`size\` is BASE quantity, not USD notional. If a \`positionId\` is supplied, \`direction\` must match the live position.
|
|
42
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.
|
|
@@ -49,7 +49,6 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
49
49
|
- Arbitrum test: \`0x895C4ae2A22bB26851011d733A9355f663a1F939\`
|
|
50
50
|
- Linea test: \`0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2\`
|
|
51
51
|
- Always keep \`RPC_URL\`, \`CHAIN_ID\`, and \`BROKER_ADDRESS\` on the same network.
|
|
52
|
-
- If \`IS_BETA_MODE\` is unset, MCP auto-detects beta mode for those two testnet brokers.
|
|
53
52
|
|
|
54
53
|
Current Session:
|
|
55
54
|
- 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.1.0" }, { 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.1.0 running (stdio, pure on-chain, prod ready)");
|
|
586
586
|
}
|
|
587
587
|
main().catch((err) => {
|
|
588
588
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getChainId } from "../auth/resolveClient.js";
|
|
1
|
+
import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
2
2
|
export async function getBalances(client, address, chainIdOverride) {
|
|
3
3
|
const chainId = chainIdOverride ?? getChainId();
|
|
4
|
-
return client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
4
|
+
return client.account.getWalletQuoteTokenBalance({ chainId, address, tokenAddress: getQuoteToken() });
|
|
5
5
|
}
|
|
6
6
|
export async function getMarginBalance(client, address, poolId, chainIdOverride) {
|
|
7
7
|
const chainId = chainIdOverride ?? getChainId();
|
|
@@ -46,6 +46,12 @@ function normalizePoolId(row) {
|
|
|
46
46
|
const poolId = row?.poolId ?? row?.pool_id ?? "";
|
|
47
47
|
return String(poolId);
|
|
48
48
|
}
|
|
49
|
+
function matchesChainId(row, chainId) {
|
|
50
|
+
const raw = row?.chainId ?? row?.chain_id;
|
|
51
|
+
if (raw === undefined || raw === null || raw === "")
|
|
52
|
+
return true;
|
|
53
|
+
return Number(raw) === chainId;
|
|
54
|
+
}
|
|
49
55
|
function matchesKeyword(row, keywordUpper) {
|
|
50
56
|
const poolId = normalizePoolId(row);
|
|
51
57
|
const haystack = [
|
|
@@ -63,11 +69,28 @@ function matchesKeyword(row, keywordUpper) {
|
|
|
63
69
|
.join("|");
|
|
64
70
|
return haystack.includes(keywordUpper);
|
|
65
71
|
}
|
|
72
|
+
export async function searchMarketCompat(client, params) {
|
|
73
|
+
const normalizedKeyword = String(params.keyword ?? "").trim();
|
|
74
|
+
const attempts = [
|
|
75
|
+
{ chainId: params.chainId, keyword: normalizedKeyword, limit: params.limit },
|
|
76
|
+
{ chainId: params.chainId, searchKey: normalizedKeyword },
|
|
77
|
+
];
|
|
78
|
+
let lastError = null;
|
|
79
|
+
for (const attempt of attempts) {
|
|
80
|
+
try {
|
|
81
|
+
return await client.markets.searchMarket(attempt);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
lastError = error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw lastError ?? new Error("searchMarket failed");
|
|
88
|
+
}
|
|
66
89
|
async function fetchApiMarketRows(client, chainId) {
|
|
67
90
|
// Preferred path: documented markets.searchMarket
|
|
68
91
|
try {
|
|
69
|
-
const searchRes = await client
|
|
70
|
-
const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
|
|
92
|
+
const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
|
|
93
|
+
const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
71
94
|
if (searchRows.length > 0)
|
|
72
95
|
return searchRows;
|
|
73
96
|
}
|
|
@@ -76,7 +99,7 @@ async function fetchApiMarketRows(client, chainId) {
|
|
|
76
99
|
// Secondary path: documented markets.getPoolSymbolAll
|
|
77
100
|
try {
|
|
78
101
|
const symbolsRes = await client.markets.getPoolSymbolAll();
|
|
79
|
-
const symbolRows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
|
|
102
|
+
const symbolRows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
80
103
|
if (symbolRows.length > 0)
|
|
81
104
|
return symbolRows;
|
|
82
105
|
}
|
|
@@ -84,12 +107,12 @@ async function fetchApiMarketRows(client, chainId) {
|
|
|
84
107
|
}
|
|
85
108
|
// Legacy fallback: internal api namespace (for backward compatibility)
|
|
86
109
|
const marketListRes = await client.api?.getMarketList?.().catch(() => null);
|
|
87
|
-
const marketRows = extractMarketRows(marketListRes, chainId);
|
|
110
|
+
const marketRows = extractMarketRows(marketListRes, chainId).filter((row) => matchesChainId(row, chainId));
|
|
88
111
|
const marketRowsWithPoolId = marketRows.filter((row) => normalizePoolId(row));
|
|
89
112
|
if (marketRowsWithPoolId.length > 0)
|
|
90
113
|
return marketRowsWithPoolId;
|
|
91
114
|
const poolListRes = await client.api?.getPoolList?.().catch(() => null);
|
|
92
|
-
return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row));
|
|
115
|
+
return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
93
116
|
}
|
|
94
117
|
export async function getMarketPrice(client, poolId, chainIdOverride) {
|
|
95
118
|
const chainId = chainIdOverride ?? getChainId();
|
|
@@ -150,7 +173,11 @@ export async function searchMarket(client, keyword, limit = 1000, chainIdOverrid
|
|
|
150
173
|
const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
|
|
151
174
|
let dataList = [];
|
|
152
175
|
try {
|
|
153
|
-
const searchRes = await client
|
|
176
|
+
const searchRes = await searchMarketCompat(client, {
|
|
177
|
+
chainId,
|
|
178
|
+
keyword: normalizedKeyword,
|
|
179
|
+
limit: requestedLimit,
|
|
180
|
+
});
|
|
154
181
|
dataList = extractMarketRows(searchRes, chainId);
|
|
155
182
|
}
|
|
156
183
|
catch {
|
|
@@ -229,8 +256,8 @@ export async function getMarketDetail(client, poolId, chainIdOverride) {
|
|
|
229
256
|
export async function getPoolList(client, chainIdOverride) {
|
|
230
257
|
const chainId = chainIdOverride ?? getChainId();
|
|
231
258
|
try {
|
|
232
|
-
const searchRes = await client
|
|
233
|
-
const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
|
|
259
|
+
const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
|
|
260
|
+
const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
234
261
|
if (rows.length > 0)
|
|
235
262
|
return rows;
|
|
236
263
|
}
|
|
@@ -238,13 +265,14 @@ export async function getPoolList(client, chainIdOverride) {
|
|
|
238
265
|
}
|
|
239
266
|
try {
|
|
240
267
|
const symbolsRes = await client.markets.getPoolSymbolAll();
|
|
241
|
-
const rows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
|
|
268
|
+
const rows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
242
269
|
if (rows.length > 0)
|
|
243
270
|
return rows;
|
|
244
271
|
}
|
|
245
272
|
catch {
|
|
246
273
|
}
|
|
247
|
-
|
|
274
|
+
const poolListRes = await client.api?.getPoolList?.();
|
|
275
|
+
return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
248
276
|
}
|
|
249
277
|
/**
|
|
250
278
|
* 获取池子分级配置
|
|
@@ -13,12 +13,6 @@ const POOL_MANAGER_BY_CHAIN = {
|
|
|
13
13
|
97: "0x4F917ef137b573D9790b87e3cF6dfb698cF00c9c",
|
|
14
14
|
56: "0x13F2130c2F3bfd612BBCBF35FB9E467dd32bAF3A",
|
|
15
15
|
};
|
|
16
|
-
const POOL_MANAGER_BY_CHAIN_BETA = {
|
|
17
|
-
421614: "0x05314a21Fc97B74f168730153b2B63A870D25dE5",
|
|
18
|
-
59141: "0xcf51a6895864c6D8E507fC31EF16b9011287c5f4",
|
|
19
|
-
97: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
20
|
-
56: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
21
|
-
};
|
|
22
16
|
const POOL_MANAGER_ABI = [
|
|
23
17
|
"function deployPool((bytes32 marketId,address baseToken))",
|
|
24
18
|
"function getMarketPool(bytes32 marketId,address asset) view returns ((bytes32 marketId,bytes32 poolId,address baseToken,address quoteToken,uint8 riskTier,uint8 state,address basePoolToken,address quotePoolToken,uint16 maxPriceDeviation,bool compoundEnabled,uint64 windowCapUsd,address poolVault,address tradingVault))",
|
|
@@ -45,28 +39,6 @@ const LP_ROUTER_BY_CHAIN = {
|
|
|
45
39
|
quotePool: "0xB1E6df749A602892FafB27bb39Fd4F044527121E",
|
|
46
40
|
},
|
|
47
41
|
};
|
|
48
|
-
const LP_ROUTER_BY_CHAIN_BETA = {
|
|
49
|
-
421614: {
|
|
50
|
-
router: "0xfb790ECE13Cd9e296b4a06ABF38D10431360c236",
|
|
51
|
-
basePool: "0x1F767BEa83EDe5E0C18904f439C579f52c2c0F1b",
|
|
52
|
-
quotePool: "0x50ad7da312c58c6689bEF028954a366cb5b8ee13",
|
|
53
|
-
},
|
|
54
|
-
59141: {
|
|
55
|
-
router: "0x0AE31989318565620c03d35889a0B2536b8fba5C",
|
|
56
|
-
basePool: "0xc9e3826c42183207B418116A16827C1605132c51",
|
|
57
|
-
quotePool: "0x9A9934D3b22103dE822d94A22A3177d728FE0e5a",
|
|
58
|
-
},
|
|
59
|
-
97: {
|
|
60
|
-
router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
|
|
61
|
-
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
62
|
-
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
63
|
-
},
|
|
64
|
-
56: {
|
|
65
|
-
router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
|
|
66
|
-
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
67
|
-
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
42
|
const PREVIEW_POOL_ABI = [
|
|
71
43
|
"function previewLpAmountOut(bytes32,uint256,uint256) view returns (uint256)",
|
|
72
44
|
"function previewQuoteAmountOut(bytes32,uint256,uint256) view returns (uint256)",
|
|
@@ -165,24 +137,19 @@ async function withMutedSdkAbiMismatchLogs(runner) {
|
|
|
165
137
|
console.error = original;
|
|
166
138
|
}
|
|
167
139
|
}
|
|
168
|
-
function isBetaModeEnabled() {
|
|
169
|
-
return String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
|
|
170
|
-
}
|
|
171
140
|
function getPoolManagerAddress(chainId) {
|
|
172
141
|
const envAddress = String(process.env.POOL_MANAGER_ADDRESS ?? "").trim();
|
|
173
142
|
if (envAddress) {
|
|
174
143
|
return normalizeAddress(envAddress, "POOL_MANAGER_ADDRESS");
|
|
175
144
|
}
|
|
176
|
-
const
|
|
177
|
-
const mapped = source[chainId];
|
|
145
|
+
const mapped = POOL_MANAGER_BY_CHAIN[chainId];
|
|
178
146
|
if (!mapped) {
|
|
179
|
-
throw new Error(`Pool manager address is not configured for chainId=${chainId}
|
|
147
|
+
throw new Error(`Pool manager address is not configured for chainId=${chainId}. Set POOL_MANAGER_ADDRESS env var.`);
|
|
180
148
|
}
|
|
181
149
|
return mapped;
|
|
182
150
|
}
|
|
183
151
|
function getLpAddresses(chainId) {
|
|
184
|
-
const
|
|
185
|
-
const matched = source[chainId];
|
|
152
|
+
const matched = LP_ROUTER_BY_CHAIN[chainId];
|
|
186
153
|
if (!matched) {
|
|
187
154
|
throw new Error(`Liquidity router config not found for chainId=${chainId}.`);
|
|
188
155
|
}
|
|
@@ -13,10 +13,7 @@ export const getTradeFlowTool = {
|
|
|
13
13
|
const { client, address } = await resolveClient();
|
|
14
14
|
const chainId = getChainId();
|
|
15
15
|
const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
|
|
16
|
-
const
|
|
17
|
-
? (await client.getAccessToken()) || ""
|
|
18
|
-
: "";
|
|
19
|
-
const result = await client.api.getTradeFlow({ ...query, address, accessToken });
|
|
16
|
+
const result = await client.account.getTradeFlow(query, address);
|
|
20
17
|
const enhancedData = (result?.data || []).map((flow) => ({
|
|
21
18
|
...flow,
|
|
22
19
|
typeDesc: getTradeFlowTypeDesc(flow.type)
|
|
@@ -38,6 +38,12 @@ function formatUnixTimestamp(timestamp) {
|
|
|
38
38
|
return String(timestamp);
|
|
39
39
|
return `${timestamp.toString()} (${new Date(numeric * 1000).toISOString()})`;
|
|
40
40
|
}
|
|
41
|
+
async function withdrawCompat(client, params) {
|
|
42
|
+
if (typeof client?.account?.withdraw === "function") {
|
|
43
|
+
return client.account.withdraw(params);
|
|
44
|
+
}
|
|
45
|
+
return client.account.updateAndWithdraw(params.receiver, params.poolId, params.isQuoteToken, params.amount, params.chainId);
|
|
46
|
+
}
|
|
41
47
|
export const accountDepositTool = {
|
|
42
48
|
name: "account_deposit",
|
|
43
49
|
description: "[ACCOUNT] Deposit funds from wallet into the MYX trading account.",
|
|
@@ -136,7 +142,13 @@ export const accountWithdrawTool = {
|
|
|
136
142
|
throw new Error(`Account has locked funds until releaseTime=${formatUnixTimestamp(releaseTime)}. ` +
|
|
137
143
|
`Retry after unlock or reduce withdraw amount.`);
|
|
138
144
|
}
|
|
139
|
-
const raw = await client
|
|
145
|
+
const raw = await withdrawCompat(client, {
|
|
146
|
+
receiver: address,
|
|
147
|
+
poolId: args.poolId,
|
|
148
|
+
isQuoteToken: Boolean(args.isQuoteToken),
|
|
149
|
+
amount,
|
|
150
|
+
chainId,
|
|
151
|
+
});
|
|
140
152
|
const data = await finalizeMutationResult(raw, signer, "account_withdraw");
|
|
141
153
|
const preflight = {
|
|
142
154
|
requestedAmountRaw: amountRaw.toString(),
|
|
@@ -52,7 +52,11 @@ export const cancelOrdersTool = {
|
|
|
52
52
|
if (targetOrderIds.length === 0) {
|
|
53
53
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", message: "No matching open orders found to cancel." }) }] };
|
|
54
54
|
}
|
|
55
|
-
const raw =
|
|
55
|
+
const raw = args.cancelAll
|
|
56
|
+
? await client.order.cancelAllOrders(targetOrderIds, chainId)
|
|
57
|
+
: targetOrderIds.length === 1
|
|
58
|
+
? await client.order.cancelOrder(targetOrderIds[0], chainId)
|
|
59
|
+
: await client.order.cancelOrders(targetOrderIds, chainId);
|
|
56
60
|
const result = await finalizeMutationResult(raw, signer, "cancel_orders");
|
|
57
61
|
return {
|
|
58
62
|
content: [{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
2
|
+
import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
3
3
|
import { resolvePool } from "../services/marketService.js";
|
|
4
4
|
import { parseUserUnits } from "../utils/units.js";
|
|
5
5
|
import { ethers } from "ethers";
|
|
@@ -49,7 +49,11 @@ export const checkAccountReadyTool = {
|
|
|
49
49
|
lockedMarginRaw = toBigIntOrZero(marginInfo.data.lockedMargin);
|
|
50
50
|
accountWalletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
51
51
|
}
|
|
52
|
-
const walletRes = await client.account.getWalletQuoteTokenBalance(
|
|
52
|
+
const walletRes = await client.account.getWalletQuoteTokenBalance({
|
|
53
|
+
chainId,
|
|
54
|
+
address,
|
|
55
|
+
tokenAddress: getQuoteToken(),
|
|
56
|
+
});
|
|
53
57
|
const walletBalanceRaw = BigInt(walletRes?.data || "0");
|
|
54
58
|
const requiredRaw = BigInt(parseUserUnits(args.collateralAmount, quoteDecimals, "required"));
|
|
55
59
|
const isReady = (availableMarginBalanceRaw >= requiredRaw) || (availableMarginBalanceRaw + walletBalanceRaw >= requiredRaw);
|
|
@@ -6,6 +6,7 @@ import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
|
6
6
|
import { verifyTradeOutcome } from "../utils/verification.js";
|
|
7
7
|
import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
|
|
8
8
|
import { parseUserUnits } from "../utils/units.js";
|
|
9
|
+
import { isZeroAddress, normalizeAddress } from "../utils/address.js";
|
|
9
10
|
const FULL_CLOSE_MARKERS = new Set(["ALL", "FULL", "MAX"]);
|
|
10
11
|
const INTEGER_RE = /^\d+$/;
|
|
11
12
|
function wantsFullCloseMarker(input) {
|
|
@@ -29,6 +30,20 @@ function resolvePositionRaw(position, rawFields, humanFields, decimals, label) {
|
|
|
29
30
|
return "";
|
|
30
31
|
return parseUserUnits(human, decimals, label);
|
|
31
32
|
}
|
|
33
|
+
function resolveQuoteExecutionFeeToken(input, quoteToken) {
|
|
34
|
+
const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
|
|
35
|
+
const raw = String(input ?? "").trim();
|
|
36
|
+
if (!raw)
|
|
37
|
+
return quoteTokenNormalized;
|
|
38
|
+
if (isZeroAddress(raw)) {
|
|
39
|
+
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
|
|
40
|
+
}
|
|
41
|
+
const normalized = normalizeAddress(raw, "executionFeeToken");
|
|
42
|
+
if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
|
|
43
|
+
throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
|
|
44
|
+
}
|
|
45
|
+
return quoteTokenNormalized;
|
|
46
|
+
}
|
|
32
47
|
export const closePositionTool = {
|
|
33
48
|
name: "close_position",
|
|
34
49
|
description: "[TRADE] Create a decrease order (close or reduce position) using SDK-native parameters.",
|
|
@@ -39,13 +54,13 @@ export const closePositionTool = {
|
|
|
39
54
|
triggerType: z.union([z.number(), z.string()]).optional().describe("Trigger type: 0/NONE, 1/GTE, 2/LTE"),
|
|
40
55
|
direction: z.union([z.number(), z.string()]).describe("Position direction: 0/LONG or 1/SHORT"),
|
|
41
56
|
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human/raw). Also supports ALL/FULL/MAX to use live position collateral raw."),
|
|
42
|
-
size: z.union([z.string(), z.number()]).describe("Position size (human/raw). Also supports ALL/FULL/MAX for exact full-close raw size."),
|
|
57
|
+
size: z.union([z.string(), z.number()]).describe("Position size as base asset quantity (human/raw), NOT USD notional. Also supports ALL/FULL/MAX for exact full-close raw size."),
|
|
43
58
|
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.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
|
|
45
59
|
postOnly: z.coerce.boolean().describe("Post-only flag"),
|
|
46
60
|
slippagePct: z.coerce.string().default("50").describe(SLIPPAGE_PCT_4DP_DESC),
|
|
47
|
-
executionFeeToken: z.string().describe("
|
|
61
|
+
executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
|
|
48
62
|
leverage: z.coerce.number().describe("Leverage"),
|
|
63
|
+
verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
|
|
49
64
|
},
|
|
50
65
|
handler: async (args) => {
|
|
51
66
|
try {
|
|
@@ -93,7 +108,8 @@ export const closePositionTool = {
|
|
|
93
108
|
direction: mapDirection(preparedArgs.direction),
|
|
94
109
|
orderType: mapOrderType(preparedArgs.orderType),
|
|
95
110
|
triggerType: preparedArgs.triggerType !== undefined ? mapTriggerType(preparedArgs.triggerType) : undefined,
|
|
96
|
-
executionFeeToken: poolData.quoteToken
|
|
111
|
+
executionFeeToken: resolveQuoteExecutionFeeToken(preparedArgs.executionFeeToken, String(poolData.quoteToken ?? "")),
|
|
112
|
+
timeInForce: 0,
|
|
97
113
|
};
|
|
98
114
|
const positionsRes = await client.position.listPositions(address);
|
|
99
115
|
const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
|
|
@@ -113,10 +129,11 @@ export const closePositionTool = {
|
|
|
113
129
|
const data = await finalizeMutationResult(raw, signer, "close_position");
|
|
114
130
|
const txHash = data.confirmation?.txHash;
|
|
115
131
|
let verification = null;
|
|
116
|
-
|
|
132
|
+
const shouldVerify = Boolean(preparedArgs.verify ?? false);
|
|
133
|
+
if (txHash && shouldVerify) {
|
|
117
134
|
verification = await verifyTradeOutcome(client, address, preparedArgs.poolId, txHash);
|
|
118
135
|
}
|
|
119
|
-
const payload = { ...data, verification };
|
|
136
|
+
const payload = { ...data, verification, verificationSkipped: !!txHash && !shouldVerify };
|
|
120
137
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
121
138
|
}
|
|
122
139
|
catch (error) {
|
|
@@ -7,7 +7,7 @@ import { verifyTradeOutcome } from "../utils/verification.js";
|
|
|
7
7
|
import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
|
|
8
8
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
9
9
|
import { parseUserUnits } from "../utils/units.js";
|
|
10
|
-
import { isZeroAddress } from "../utils/address.js";
|
|
10
|
+
import { isZeroAddress, normalizeAddress } from "../utils/address.js";
|
|
11
11
|
const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
|
|
12
12
|
const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
|
|
13
13
|
function pow10(decimals) {
|
|
@@ -18,6 +18,20 @@ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimal
|
|
|
18
18
|
const denominator = pow10(baseDecimals + 30);
|
|
19
19
|
return numerator / denominator;
|
|
20
20
|
}
|
|
21
|
+
function resolveQuoteExecutionFeeToken(input, quoteToken) {
|
|
22
|
+
const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
|
|
23
|
+
const raw = String(input ?? "").trim();
|
|
24
|
+
if (!raw)
|
|
25
|
+
return quoteTokenNormalized;
|
|
26
|
+
if (isZeroAddress(raw)) {
|
|
27
|
+
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
|
|
28
|
+
}
|
|
29
|
+
const normalized = normalizeAddress(raw, "executionFeeToken");
|
|
30
|
+
if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
|
|
31
|
+
throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
|
|
32
|
+
}
|
|
33
|
+
return quoteTokenNormalized;
|
|
34
|
+
}
|
|
21
35
|
export const executeTradeTool = {
|
|
22
36
|
name: "execute_trade",
|
|
23
37
|
description: "[TRADE] Create an increase order (open or add to position) using SDK-native parameters.",
|
|
@@ -30,12 +44,11 @@ export const executeTradeTool = {
|
|
|
30
44
|
triggerType: z.union([z.number(), z.string()]).optional().describe("0=None (Market), 1=GTE, 2=LTE. e.g. 'GTE'."),
|
|
31
45
|
direction: z.union([z.number(), z.string()]).describe("0/LONG/BUY or 1/SHORT/SELL."),
|
|
32
46
|
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
|
|
33
|
-
size: z.union([z.string(), z.number()]).describe("
|
|
47
|
+
size: z.union([z.string(), z.number()]).describe("Base asset quantity, NOT USD notional. e.g. '0.5' BTC or 'raw:50000000'."),
|
|
34
48
|
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.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
|
|
36
49
|
postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
|
|
37
50
|
slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
|
|
38
|
-
executionFeeToken: z.string().optional().describe("
|
|
51
|
+
executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
|
|
39
52
|
leverage: z.coerce.number().positive().describe("Leverage multiplier, e.g., 10 for 10x."),
|
|
40
53
|
tpSize: z.union([z.string(), z.number()]).optional().describe("Take Profit size. Use '0' to disable."),
|
|
41
54
|
tpPrice: z.union([z.string(), z.number()]).optional().describe("Take Profit trigger price."),
|
|
@@ -45,6 +58,7 @@ export const executeTradeTool = {
|
|
|
45
58
|
assetClass: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup assetClass (default from pool config or 1)."),
|
|
46
59
|
riskTier: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup riskTier (default from pool config or 1)."),
|
|
47
60
|
marketId: z.string().describe("Specific Market Config Hash. Fetch via get_pool_metadata (resolve poolId first with find_pool/list_pools)."),
|
|
61
|
+
verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
|
|
48
62
|
},
|
|
49
63
|
handler: async (args) => {
|
|
50
64
|
try {
|
|
@@ -70,11 +84,7 @@ export const executeTradeTool = {
|
|
|
70
84
|
const mappedOrderType = mapOrderType(args.orderType);
|
|
71
85
|
const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
|
|
72
86
|
const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
|
|
73
|
-
|
|
74
|
-
if (isZeroAddress(args.executionFeeToken)) {
|
|
75
|
-
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${poolData.quoteToken}.`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
87
|
+
const executionFeeToken = resolveQuoteExecutionFeeToken(args.executionFeeToken, String(poolData.quoteToken ?? ""));
|
|
78
88
|
const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
79
89
|
const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
|
|
80
90
|
const priceRaw = parseUserUnits(args.price, 30, "price");
|
|
@@ -142,13 +152,13 @@ export const executeTradeTool = {
|
|
|
142
152
|
triggerType: mappedTriggerType,
|
|
143
153
|
// Normalize positionId
|
|
144
154
|
positionId: normalizedPositionId,
|
|
145
|
-
|
|
146
|
-
executionFeeToken: poolData.quoteToken || args.executionFeeToken,
|
|
155
|
+
executionFeeToken,
|
|
147
156
|
collateralAmount: `raw:${collateralRaw}`,
|
|
148
157
|
size: `raw:${sizeRaw}`,
|
|
149
158
|
price: `raw:${priceRaw}`,
|
|
150
159
|
tradingFee: `raw:${tradingFeeRaw}`,
|
|
151
160
|
slippagePct: slippagePctNormalized,
|
|
161
|
+
timeInForce: 0,
|
|
152
162
|
};
|
|
153
163
|
if (args.tpSize !== undefined) {
|
|
154
164
|
mappedArgs.tpSize = `raw:${parseUserUnits(args.tpSize, baseDecimals, "tpSize")}`;
|
|
@@ -166,17 +176,23 @@ export const executeTradeTool = {
|
|
|
166
176
|
const data = await finalizeMutationResult(raw, signer, "execute_trade");
|
|
167
177
|
const txHash = data.confirmation?.txHash;
|
|
168
178
|
let verification = null;
|
|
169
|
-
|
|
179
|
+
const shouldVerify = Boolean(args.verify ?? false);
|
|
180
|
+
if (txHash && shouldVerify) {
|
|
170
181
|
verification = await verifyTradeOutcome(client, address, args.poolId, txHash);
|
|
171
182
|
}
|
|
172
183
|
const payload = {
|
|
173
184
|
...data,
|
|
174
185
|
verification,
|
|
186
|
+
verificationSkipped: !!txHash && !shouldVerify,
|
|
175
187
|
preflight: {
|
|
176
188
|
normalized: {
|
|
177
189
|
collateralAmountRaw: collateralRaw,
|
|
178
190
|
sizeRaw,
|
|
179
191
|
priceRaw30: priceRaw,
|
|
192
|
+
sizeSemantics: "base_quantity",
|
|
193
|
+
impliedNotionalQuoteRaw: computeQuoteNotionalRaw(BigInt(sizeRaw), BigInt(priceRaw), baseDecimals, quoteDecimals).toString(),
|
|
194
|
+
executionFeeToken,
|
|
195
|
+
timeInForce: mappedArgs.timeInForce,
|
|
180
196
|
tradingFeeRaw,
|
|
181
197
|
tpSizeRaw: mappedArgs.tpSize?.replace(/^raw:/i, "") ?? null,
|
|
182
198
|
tpPriceRaw30: mappedArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
|
|
@@ -15,7 +15,7 @@ export const getAllTickersTool = {
|
|
|
15
15
|
// Fallback for networks/environments where getAllTickers endpoint is unavailable.
|
|
16
16
|
const chainId = getChainId();
|
|
17
17
|
const poolList = await getPoolList(client, chainId);
|
|
18
|
-
const pools = Array.isArray(poolList
|
|
18
|
+
const pools = Array.isArray(poolList) ? poolList : (Array.isArray(poolList?.data) ? poolList.data : []);
|
|
19
19
|
const poolIds = pools.map((p) => p?.poolId ?? p?.pool_id).filter((id) => !!id);
|
|
20
20
|
if (poolIds.length === 0) {
|
|
21
21
|
throw new Error("Failed to fetch all tickers and no pools were available for fallback query.");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { searchMarketCompat } from "../services/marketService.js";
|
|
3
4
|
import { normalizeAddress } from "../utils/address.js";
|
|
4
5
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
5
6
|
function collectRows(input) {
|
|
@@ -14,10 +15,10 @@ function collectRows(input) {
|
|
|
14
15
|
function isNonEmptyObject(value) {
|
|
15
16
|
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
16
17
|
}
|
|
17
|
-
async function
|
|
18
|
+
async function loadMarketRows(client, chainId) {
|
|
18
19
|
let rows = [];
|
|
19
20
|
try {
|
|
20
|
-
const searchRes = await client
|
|
21
|
+
const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
|
|
21
22
|
rows = collectRows(searchRes?.data ?? searchRes);
|
|
22
23
|
}
|
|
23
24
|
catch {
|
|
@@ -32,8 +33,15 @@ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
|
|
|
32
33
|
rows = [];
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
function findMarketRowByBaseAddress(rows, baseAddress) {
|
|
39
|
+
const normalizedBaseAddress = baseAddress.toLowerCase();
|
|
40
|
+
return rows.find((row) => String(row?.baseToken ?? row?.baseAddress ?? "").trim().toLowerCase() === normalizedBaseAddress);
|
|
41
|
+
}
|
|
42
|
+
async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
|
|
43
|
+
const rows = await loadMarketRows(client, chainId);
|
|
44
|
+
const matched = findMarketRowByBaseAddress(rows, baseAddress);
|
|
37
45
|
if (!matched)
|
|
38
46
|
return null;
|
|
39
47
|
const poolId = String(matched?.poolId ?? matched?.pool_id ?? "").trim();
|
|
@@ -83,6 +91,12 @@ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
|
|
|
83
91
|
source: "market_search_fallback",
|
|
84
92
|
};
|
|
85
93
|
}
|
|
94
|
+
async function resolvePoolIdByBaseAddress(client, chainId, baseAddress) {
|
|
95
|
+
const rows = await loadMarketRows(client, chainId);
|
|
96
|
+
const matched = findMarketRowByBaseAddress(rows, baseAddress);
|
|
97
|
+
const poolId = String(matched?.poolId ?? matched?.pool_id ?? "").trim();
|
|
98
|
+
return poolId || null;
|
|
99
|
+
}
|
|
86
100
|
function buildReadErrorPayload(args, messageLike, code = "SDK_READ_ERROR") {
|
|
87
101
|
const chainId = args.chainId ?? getChainId();
|
|
88
102
|
const message = extractErrorMessage(messageLike, "Failed to read base token detail.");
|
|
@@ -113,7 +127,16 @@ export const getBaseDetailTool = {
|
|
|
113
127
|
const { client } = await resolveClient();
|
|
114
128
|
const chainId = args.chainId ?? getChainId();
|
|
115
129
|
const baseAddress = normalizeAddress(args.baseAddress, "baseAddress");
|
|
116
|
-
const
|
|
130
|
+
const poolId = await resolvePoolIdByBaseAddress(client, chainId, baseAddress);
|
|
131
|
+
if (!poolId) {
|
|
132
|
+
const fallback = await findBaseDetailFromMarkets(client, chainId, baseAddress);
|
|
133
|
+
if (fallback) {
|
|
134
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: fallback }, null, 2) }] };
|
|
135
|
+
}
|
|
136
|
+
const body = buildReadErrorPayload({ ...args, baseAddress }, "Could not resolve poolId from baseAddress.", "NOT_FOUND");
|
|
137
|
+
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
|
|
138
|
+
}
|
|
139
|
+
const result = await client.markets.getBaseDetail({ chainId, poolId });
|
|
117
140
|
const hasCode = !!result && typeof result === "object" && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, "code");
|
|
118
141
|
const code = hasCode ? Number(result.code) : 0;
|
|
119
142
|
const payload = hasCode ? result.data : result;
|
|
@@ -3,9 +3,10 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
|
3
3
|
import { COMMON_LP_AMOUNT_DECIMALS } from "@myx-trade/sdk";
|
|
4
4
|
import { Contract, formatUnits } from "ethers";
|
|
5
5
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
6
|
-
import { getPoolList } from "../services/marketService.js";
|
|
7
|
-
import { getPoolInfo } from "../services/poolService.js";
|
|
6
|
+
import { getFreshOraclePrice, getPoolList } from "../services/marketService.js";
|
|
7
|
+
import { getLpPrice, getPoolInfo } from "../services/poolService.js";
|
|
8
8
|
const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
9
|
+
const INTEGER_RE = /^\d+$/;
|
|
9
10
|
const ERC20_BALANCE_ABI = ["function balanceOf(address owner) view returns (uint256)"];
|
|
10
11
|
function collectRows(input) {
|
|
11
12
|
if (Array.isArray(input))
|
|
@@ -127,6 +128,71 @@ async function readErc20Balance(provider, tokenAddress, holder) {
|
|
|
127
128
|
const balance = await contract.balanceOf(holder);
|
|
128
129
|
return BigInt(balance).toString();
|
|
129
130
|
}
|
|
131
|
+
function pow10(decimals) {
|
|
132
|
+
if (!Number.isInteger(decimals) || decimals < 0) {
|
|
133
|
+
throw new Error(`Invalid decimals: ${decimals}`);
|
|
134
|
+
}
|
|
135
|
+
return 10n ** BigInt(decimals);
|
|
136
|
+
}
|
|
137
|
+
function parseIntegerRaw(value) {
|
|
138
|
+
const text = String(value ?? "").trim();
|
|
139
|
+
if (!INTEGER_RE.test(text))
|
|
140
|
+
return null;
|
|
141
|
+
try {
|
|
142
|
+
return BigInt(text);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function formatRawValue(value, decimals) {
|
|
149
|
+
if (!value || !INTEGER_RE.test(value))
|
|
150
|
+
return null;
|
|
151
|
+
try {
|
|
152
|
+
return formatUnits(value, decimals);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function computeLpValueQuoteRaw(lpBalanceRaw, lpPriceQuoteRaw30, quoteDecimals) {
|
|
159
|
+
const balance = parseIntegerRaw(lpBalanceRaw);
|
|
160
|
+
const price = parseIntegerRaw(lpPriceQuoteRaw30);
|
|
161
|
+
if (balance === null || price === null)
|
|
162
|
+
return null;
|
|
163
|
+
const valueRaw = (balance * price * pow10(quoteDecimals)) /
|
|
164
|
+
pow10(COMMON_LP_AMOUNT_DECIMALS + 30);
|
|
165
|
+
return valueRaw.toString();
|
|
166
|
+
}
|
|
167
|
+
function computeUnderlyingTokenRawFromExchangeRate(lpBalanceRaw, exchangeRateRaw18, tokenDecimals) {
|
|
168
|
+
const balance = parseIntegerRaw(lpBalanceRaw);
|
|
169
|
+
const exchangeRate = parseIntegerRaw(exchangeRateRaw18);
|
|
170
|
+
if (balance === null || exchangeRate === null)
|
|
171
|
+
return null;
|
|
172
|
+
const underlyingRaw = (balance * exchangeRate * pow10(tokenDecimals)) /
|
|
173
|
+
pow10(COMMON_LP_AMOUNT_DECIMALS + 18);
|
|
174
|
+
return underlyingRaw.toString();
|
|
175
|
+
}
|
|
176
|
+
function computeQuoteValueFromBaseAmountRaw(baseAmountRaw, basePriceRaw30, baseDecimals, quoteDecimals) {
|
|
177
|
+
const baseAmount = parseIntegerRaw(baseAmountRaw);
|
|
178
|
+
const basePrice = parseIntegerRaw(basePriceRaw30);
|
|
179
|
+
if (baseAmount === null || basePrice === null)
|
|
180
|
+
return null;
|
|
181
|
+
const quoteValueRaw = (baseAmount * basePrice * pow10(quoteDecimals)) /
|
|
182
|
+
pow10(baseDecimals + 30);
|
|
183
|
+
return quoteValueRaw.toString();
|
|
184
|
+
}
|
|
185
|
+
function sumRawValues(...values) {
|
|
186
|
+
let total = 0n;
|
|
187
|
+
let found = false;
|
|
188
|
+
for (const value of values) {
|
|
189
|
+
if (!value || !INTEGER_RE.test(value))
|
|
190
|
+
continue;
|
|
191
|
+
total += BigInt(value);
|
|
192
|
+
found = true;
|
|
193
|
+
}
|
|
194
|
+
return found ? total.toString() : null;
|
|
195
|
+
}
|
|
130
196
|
function collectAddressCandidates(...values) {
|
|
131
197
|
const unique = new Set();
|
|
132
198
|
for (const value of values) {
|
|
@@ -205,6 +271,9 @@ export const getMyLpHoldingsTool = {
|
|
|
205
271
|
const warnings = [];
|
|
206
272
|
let totalBaseLpRaw = 0n;
|
|
207
273
|
let totalQuoteLpRaw = 0n;
|
|
274
|
+
let totalBaseEstimatedValueQuoteRaw = 0n;
|
|
275
|
+
let totalQuoteEstimatedValueQuoteRaw = 0n;
|
|
276
|
+
const valuationBuckets = new Map();
|
|
208
277
|
for (const row of scannedRows) {
|
|
209
278
|
const poolId = normalizePoolId(row?.poolId ?? row?.pool_id);
|
|
210
279
|
let basePoolToken = readAddress(row?.basePoolToken ?? row?.base_pool_token);
|
|
@@ -259,6 +328,140 @@ export const getMyLpHoldingsTool = {
|
|
|
259
328
|
continue;
|
|
260
329
|
totalBaseLpRaw += BigInt(baseLpRaw);
|
|
261
330
|
totalQuoteLpRaw += BigInt(quoteLpRaw);
|
|
331
|
+
const baseDecimals = Number(detail?.baseDecimals ?? row?.baseDecimals ?? row?.base_decimals ?? 18);
|
|
332
|
+
const quoteDecimals = Number(detail?.quoteDecimals ?? row?.quoteDecimals ?? row?.quote_decimals ?? 6);
|
|
333
|
+
async function ensurePoolInfoLoaded() {
|
|
334
|
+
if (poolInfo)
|
|
335
|
+
return poolInfo;
|
|
336
|
+
poolInfo = await getPoolInfo(poolId, chainId, client);
|
|
337
|
+
return poolInfo;
|
|
338
|
+
}
|
|
339
|
+
let baseLpPriceQuoteRaw = null;
|
|
340
|
+
let quoteLpPriceQuoteRaw = null;
|
|
341
|
+
let baseEstimatedUnderlyingRaw = null;
|
|
342
|
+
let quoteEstimatedUnderlyingRaw = null;
|
|
343
|
+
let baseEstimatedValueQuoteRaw = null;
|
|
344
|
+
let quoteEstimatedValueQuoteRaw = null;
|
|
345
|
+
let baseValueSource = null;
|
|
346
|
+
let quoteValueSource = null;
|
|
347
|
+
let baseOraclePriceRaw30 = null;
|
|
348
|
+
const baseValuationNotes = [];
|
|
349
|
+
const quoteValuationNotes = [];
|
|
350
|
+
if (BigInt(baseLpRaw) > 0n) {
|
|
351
|
+
try {
|
|
352
|
+
const raw = String(await getLpPrice("BASE", poolId, chainId) ?? "").trim();
|
|
353
|
+
if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
|
|
354
|
+
baseLpPriceQuoteRaw = raw;
|
|
355
|
+
baseEstimatedValueQuoteRaw = computeLpValueQuoteRaw(baseLpRaw, raw, quoteDecimals);
|
|
356
|
+
baseValueSource = "sdk.getLpPrice(BASE)";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
baseValuationNotes.push(`failed to fetch BASE LP price (${extractErrorMessage(error)})`);
|
|
361
|
+
}
|
|
362
|
+
if (!baseEstimatedValueQuoteRaw) {
|
|
363
|
+
try {
|
|
364
|
+
const info = await ensurePoolInfoLoaded();
|
|
365
|
+
const raw = String(info?.basePool?.poolTokenPrice ?? "").trim();
|
|
366
|
+
if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
|
|
367
|
+
baseLpPriceQuoteRaw = raw;
|
|
368
|
+
baseEstimatedValueQuoteRaw = computeLpValueQuoteRaw(baseLpRaw, raw, quoteDecimals);
|
|
369
|
+
baseValueSource = "poolInfo.basePool.poolTokenPrice";
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
baseValuationNotes.push(`failed to load pool info for BASE valuation (${extractErrorMessage(error)})`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!baseEstimatedValueQuoteRaw) {
|
|
377
|
+
try {
|
|
378
|
+
const info = await ensurePoolInfoLoaded();
|
|
379
|
+
const exchangeRateRaw = String(info?.basePool?.exchangeRate ?? "").trim();
|
|
380
|
+
baseEstimatedUnderlyingRaw = computeUnderlyingTokenRawFromExchangeRate(baseLpRaw, exchangeRateRaw, baseDecimals);
|
|
381
|
+
if (baseEstimatedUnderlyingRaw && BigInt(baseEstimatedUnderlyingRaw) > 0n) {
|
|
382
|
+
const oracle = await getFreshOraclePrice(client, poolId, chainId);
|
|
383
|
+
baseOraclePriceRaw30 = String(oracle?.price ?? "").trim();
|
|
384
|
+
baseEstimatedValueQuoteRaw = computeQuoteValueFromBaseAmountRaw(baseEstimatedUnderlyingRaw, baseOraclePriceRaw30, baseDecimals, quoteDecimals);
|
|
385
|
+
if (baseEstimatedValueQuoteRaw) {
|
|
386
|
+
baseValueSource = "poolInfo.basePool.exchangeRate * oraclePrice";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
baseValuationNotes.push(`failed to estimate BASE LP value from exchangeRate/oracle (${extractErrorMessage(error)})`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!baseEstimatedValueQuoteRaw) {
|
|
395
|
+
warnings.push(`pool ${poolId}: BASE LP value unavailable (${baseValuationNotes.join("; ") || "no valuation source"})`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (BigInt(quoteLpRaw) > 0n) {
|
|
399
|
+
try {
|
|
400
|
+
const raw = String(await getLpPrice("QUOTE", poolId, chainId) ?? "").trim();
|
|
401
|
+
if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
|
|
402
|
+
quoteLpPriceQuoteRaw = raw;
|
|
403
|
+
quoteEstimatedValueQuoteRaw = computeLpValueQuoteRaw(quoteLpRaw, raw, quoteDecimals);
|
|
404
|
+
quoteValueSource = "sdk.getLpPrice(QUOTE)";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
quoteValuationNotes.push(`failed to fetch QUOTE LP price (${extractErrorMessage(error)})`);
|
|
409
|
+
}
|
|
410
|
+
if (!quoteEstimatedValueQuoteRaw) {
|
|
411
|
+
try {
|
|
412
|
+
const info = await ensurePoolInfoLoaded();
|
|
413
|
+
const raw = String(info?.quotePool?.poolTokenPrice ?? "").trim();
|
|
414
|
+
if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
|
|
415
|
+
quoteLpPriceQuoteRaw = raw;
|
|
416
|
+
quoteEstimatedValueQuoteRaw = computeLpValueQuoteRaw(quoteLpRaw, raw, quoteDecimals);
|
|
417
|
+
quoteValueSource = "poolInfo.quotePool.poolTokenPrice";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
quoteValuationNotes.push(`failed to load pool info for QUOTE valuation (${extractErrorMessage(error)})`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!quoteEstimatedValueQuoteRaw) {
|
|
425
|
+
try {
|
|
426
|
+
const info = await ensurePoolInfoLoaded();
|
|
427
|
+
const exchangeRateRaw = String(info?.quotePool?.exchangeRate ?? "").trim();
|
|
428
|
+
quoteEstimatedUnderlyingRaw = computeUnderlyingTokenRawFromExchangeRate(quoteLpRaw, exchangeRateRaw, quoteDecimals);
|
|
429
|
+
if (quoteEstimatedUnderlyingRaw && BigInt(quoteEstimatedUnderlyingRaw) > 0n) {
|
|
430
|
+
quoteEstimatedValueQuoteRaw = quoteEstimatedUnderlyingRaw;
|
|
431
|
+
quoteValueSource = "poolInfo.quotePool.exchangeRate";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
quoteValuationNotes.push(`failed to estimate QUOTE LP value from exchangeRate (${extractErrorMessage(error)})`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!quoteEstimatedValueQuoteRaw) {
|
|
439
|
+
warnings.push(`pool ${poolId}: QUOTE LP value unavailable (${quoteValuationNotes.join("; ") || "no valuation source"})`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const estimatedValueQuoteRaw = sumRawValues(baseEstimatedValueQuoteRaw, quoteEstimatedValueQuoteRaw);
|
|
443
|
+
if (baseEstimatedValueQuoteRaw) {
|
|
444
|
+
totalBaseEstimatedValueQuoteRaw += BigInt(baseEstimatedValueQuoteRaw);
|
|
445
|
+
}
|
|
446
|
+
if (quoteEstimatedValueQuoteRaw) {
|
|
447
|
+
totalQuoteEstimatedValueQuoteRaw += BigInt(quoteEstimatedValueQuoteRaw);
|
|
448
|
+
}
|
|
449
|
+
if (baseEstimatedValueQuoteRaw || quoteEstimatedValueQuoteRaw) {
|
|
450
|
+
const bucketKey = `${quoteSymbol ?? "QUOTE"}:${quoteDecimals}`;
|
|
451
|
+
const bucket = valuationBuckets.get(bucketKey) ?? {
|
|
452
|
+
quoteSymbol,
|
|
453
|
+
quoteDecimals,
|
|
454
|
+
totalBaseEstimatedValueQuoteRaw: 0n,
|
|
455
|
+
totalQuoteEstimatedValueQuoteRaw: 0n,
|
|
456
|
+
};
|
|
457
|
+
if (baseEstimatedValueQuoteRaw) {
|
|
458
|
+
bucket.totalBaseEstimatedValueQuoteRaw += BigInt(baseEstimatedValueQuoteRaw);
|
|
459
|
+
}
|
|
460
|
+
if (quoteEstimatedValueQuoteRaw) {
|
|
461
|
+
bucket.totalQuoteEstimatedValueQuoteRaw += BigInt(quoteEstimatedValueQuoteRaw);
|
|
462
|
+
}
|
|
463
|
+
valuationBuckets.set(bucketKey, bucket);
|
|
464
|
+
}
|
|
262
465
|
items.push({
|
|
263
466
|
poolId,
|
|
264
467
|
symbol: toSymbol(row),
|
|
@@ -271,8 +474,26 @@ export const getMyLpHoldingsTool = {
|
|
|
271
474
|
quotePoolToken: quotePoolToken ?? null,
|
|
272
475
|
baseLpBalanceRaw: baseLpRaw,
|
|
273
476
|
baseLpBalance: formatUnits(baseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
477
|
+
baseLpPriceQuoteRaw,
|
|
478
|
+
baseLpPriceQuote: formatRawValue(baseLpPriceQuoteRaw, 30),
|
|
479
|
+
baseEstimatedUnderlyingRaw,
|
|
480
|
+
baseEstimatedUnderlying: formatRawValue(baseEstimatedUnderlyingRaw, baseDecimals),
|
|
481
|
+
baseOraclePriceRaw30,
|
|
482
|
+
baseOraclePrice: formatRawValue(baseOraclePriceRaw30, 30),
|
|
483
|
+
baseEstimatedValueQuoteRaw,
|
|
484
|
+
baseEstimatedValueQuote: formatRawValue(baseEstimatedValueQuoteRaw, quoteDecimals),
|
|
485
|
+
baseValueSource,
|
|
274
486
|
quoteLpBalanceRaw: quoteLpRaw,
|
|
275
487
|
quoteLpBalance: formatUnits(quoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
488
|
+
quoteLpPriceQuoteRaw,
|
|
489
|
+
quoteLpPriceQuote: formatRawValue(quoteLpPriceQuoteRaw, 30),
|
|
490
|
+
quoteEstimatedUnderlyingRaw,
|
|
491
|
+
quoteEstimatedUnderlying: formatRawValue(quoteEstimatedUnderlyingRaw, quoteDecimals),
|
|
492
|
+
quoteEstimatedValueQuoteRaw,
|
|
493
|
+
quoteEstimatedValueQuote: formatRawValue(quoteEstimatedValueQuoteRaw, quoteDecimals),
|
|
494
|
+
quoteValueSource,
|
|
495
|
+
estimatedValueQuoteRaw,
|
|
496
|
+
estimatedValueQuote: formatRawValue(estimatedValueQuoteRaw, quoteDecimals),
|
|
276
497
|
hasAnyLp,
|
|
277
498
|
});
|
|
278
499
|
}
|
|
@@ -282,6 +503,20 @@ export const getMyLpHoldingsTool = {
|
|
|
282
503
|
return symbolCompare;
|
|
283
504
|
return String(left.poolId ?? "").localeCompare(String(right.poolId ?? ""));
|
|
284
505
|
});
|
|
506
|
+
const valuationSummaryByQuote = Array.from(valuationBuckets.values()).map((bucket) => {
|
|
507
|
+
const totalEstimatedValueQuoteRaw = bucket.totalBaseEstimatedValueQuoteRaw + bucket.totalQuoteEstimatedValueQuoteRaw;
|
|
508
|
+
return {
|
|
509
|
+
quoteSymbol: bucket.quoteSymbol,
|
|
510
|
+
quoteDecimals: bucket.quoteDecimals,
|
|
511
|
+
totalBaseEstimatedValueQuoteRaw: bucket.totalBaseEstimatedValueQuoteRaw.toString(),
|
|
512
|
+
totalBaseEstimatedValueQuote: formatUnits(bucket.totalBaseEstimatedValueQuoteRaw, bucket.quoteDecimals),
|
|
513
|
+
totalQuoteEstimatedValueQuoteRaw: bucket.totalQuoteEstimatedValueQuoteRaw.toString(),
|
|
514
|
+
totalQuoteEstimatedValueQuote: formatUnits(bucket.totalQuoteEstimatedValueQuoteRaw, bucket.quoteDecimals),
|
|
515
|
+
totalEstimatedValueQuoteRaw: totalEstimatedValueQuoteRaw.toString(),
|
|
516
|
+
totalEstimatedValueQuote: formatUnits(totalEstimatedValueQuoteRaw, bucket.quoteDecimals),
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
const singleQuoteSummary = valuationSummaryByQuote.length === 1 ? valuationSummaryByQuote[0] : null;
|
|
285
520
|
const payload = {
|
|
286
521
|
meta: {
|
|
287
522
|
address,
|
|
@@ -298,6 +533,13 @@ export const getMyLpHoldingsTool = {
|
|
|
298
533
|
totalBaseLp: formatUnits(totalBaseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
299
534
|
totalQuoteLpRaw: totalQuoteLpRaw.toString(),
|
|
300
535
|
totalQuoteLp: formatUnits(totalQuoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
536
|
+
totalBaseEstimatedValueQuoteRaw: totalBaseEstimatedValueQuoteRaw.toString(),
|
|
537
|
+
totalBaseEstimatedValueQuote: singleQuoteSummary?.totalBaseEstimatedValueQuote ?? null,
|
|
538
|
+
totalQuoteEstimatedValueQuoteRaw: totalQuoteEstimatedValueQuoteRaw.toString(),
|
|
539
|
+
totalQuoteEstimatedValueQuote: singleQuoteSummary?.totalQuoteEstimatedValueQuote ?? null,
|
|
540
|
+
totalEstimatedValueQuoteRaw: (totalBaseEstimatedValueQuoteRaw + totalQuoteEstimatedValueQuoteRaw).toString(),
|
|
541
|
+
totalEstimatedValueQuote: singleQuoteSummary?.totalEstimatedValueQuote ?? null,
|
|
542
|
+
valuationSummaryByQuote,
|
|
301
543
|
},
|
|
302
544
|
items,
|
|
303
545
|
};
|
package/dist/tools/listPools.js
CHANGED
|
@@ -21,7 +21,8 @@ export const listPoolsTool = {
|
|
|
21
21
|
getPoolList(client),
|
|
22
22
|
client.markets.getPoolSymbolAll().catch(() => ({ data: [] }))
|
|
23
23
|
]);
|
|
24
|
-
const
|
|
24
|
+
const poolsSource = Array.isArray(poolListRes) ? poolListRes : poolListRes?.data ?? poolListRes;
|
|
25
|
+
const poolsRaw = collectRows(poolsSource);
|
|
25
26
|
const symbolsRaw = collectRows(symbolsRes?.data ?? symbolsRes);
|
|
26
27
|
const symbolMap = new Map(symbolsRaw
|
|
27
28
|
.filter((row) => row?.poolId || row?.pool_id)
|
package/dist/tools/manageTpSl.js
CHANGED
|
@@ -227,7 +227,7 @@ function validateTpSlPriceSemantics(direction, entryPriceInput, tpPriceInput, sl
|
|
|
227
227
|
}
|
|
228
228
|
async function cancelTpSlByIntent(client, address, signer, chainId, args) {
|
|
229
229
|
if (args.orderId) {
|
|
230
|
-
const raw = await client.order.
|
|
230
|
+
const raw = await client.order.cancelOrder(String(args.orderId), chainId);
|
|
231
231
|
const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
|
|
232
232
|
return {
|
|
233
233
|
content: [{
|
|
@@ -264,7 +264,9 @@ async function cancelTpSlByIntent(client, address, signer, chainId, args) {
|
|
|
264
264
|
}]
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
|
-
const raw =
|
|
267
|
+
const raw = orderIds.length === 1
|
|
268
|
+
? await client.order.cancelOrder(orderIds[0], chainId)
|
|
269
|
+
: await client.order.cancelOrders(orderIds, chainId);
|
|
268
270
|
const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
|
|
269
271
|
return {
|
|
270
272
|
content: [{
|
|
@@ -31,6 +31,14 @@ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimal
|
|
|
31
31
|
const denominator = pow10(baseDecimals + 30);
|
|
32
32
|
return numerator / denominator;
|
|
33
33
|
}
|
|
34
|
+
function divRoundUp(numerator, denominator) {
|
|
35
|
+
if (denominator <= 0n) {
|
|
36
|
+
throw new Error("denominator must be > 0.");
|
|
37
|
+
}
|
|
38
|
+
if (numerator <= 0n)
|
|
39
|
+
return 0n;
|
|
40
|
+
return (numerator + denominator - 1n) / denominator;
|
|
41
|
+
}
|
|
34
42
|
async function getRequiredApprovalSpendRaw(client, marketId, args, chainId) {
|
|
35
43
|
const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
|
|
36
44
|
if (!/^\d+$/.test(networkFeeText)) {
|
|
@@ -58,6 +66,20 @@ function pickMarketDetail(res) {
|
|
|
58
66
|
return res;
|
|
59
67
|
return null;
|
|
60
68
|
}
|
|
69
|
+
function resolveQuoteExecutionFeeToken(input, quoteToken) {
|
|
70
|
+
const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
|
|
71
|
+
const raw = String(input ?? "").trim();
|
|
72
|
+
if (!raw)
|
|
73
|
+
return quoteTokenNormalized;
|
|
74
|
+
if (isZeroAddress(raw)) {
|
|
75
|
+
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
|
|
76
|
+
}
|
|
77
|
+
const normalized = normalizeAddress(raw, "executionFeeToken");
|
|
78
|
+
if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
|
|
79
|
+
throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
|
|
80
|
+
}
|
|
81
|
+
return quoteTokenNormalized;
|
|
82
|
+
}
|
|
61
83
|
export const openPositionSimpleTool = {
|
|
62
84
|
name: "open_position_simple",
|
|
63
85
|
description: "[TRADE] High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
|
|
@@ -79,13 +101,13 @@ export const openPositionSimpleTool = {
|
|
|
79
101
|
size: z.coerce
|
|
80
102
|
.string()
|
|
81
103
|
.optional()
|
|
82
|
-
.describe("Position size. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
|
|
104
|
+
.describe("Position size as base asset quantity, NOT USD notional. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
|
|
83
105
|
slippagePct: z.coerce
|
|
84
106
|
.string()
|
|
85
107
|
.optional()
|
|
86
108
|
.describe(`${SLIPPAGE_PCT_4DP_DESC} Default 50 (=0.5%).`),
|
|
87
109
|
postOnly: z.coerce.boolean().optional().describe("Post-only (default false)."),
|
|
88
|
-
executionFeeToken: z.string().optional().describe("
|
|
110
|
+
executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
|
|
89
111
|
assetClass: z.coerce.number().int().nonnegative().optional().describe("Fee query assetClass (default 1)."),
|
|
90
112
|
riskTier: z.coerce.number().int().nonnegative().optional().describe("Fee query riskTier (default 1)."),
|
|
91
113
|
tradingFee: z.coerce
|
|
@@ -103,6 +125,7 @@ export const openPositionSimpleTool = {
|
|
|
103
125
|
.optional()
|
|
104
126
|
.describe("Deprecated compatibility flag. SDK now handles deposit deltas during order creation."),
|
|
105
127
|
dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
|
|
128
|
+
verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
|
|
106
129
|
},
|
|
107
130
|
handler: async (args) => {
|
|
108
131
|
try {
|
|
@@ -142,12 +165,7 @@ export const openPositionSimpleTool = {
|
|
|
142
165
|
const orderType = mapOrderType(args.orderType ?? 0);
|
|
143
166
|
const postOnly = Boolean(args.postOnly ?? false);
|
|
144
167
|
const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
|
|
145
|
-
|
|
146
|
-
if (isZeroAddress(args.executionFeeToken)) {
|
|
147
|
-
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteToken}.`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
|
|
168
|
+
const executionFeeToken = resolveQuoteExecutionFeeToken(args.executionFeeToken, quoteToken);
|
|
151
169
|
// 4) Determine reference price (30 decimals)
|
|
152
170
|
let price30;
|
|
153
171
|
let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
|
|
@@ -166,6 +184,7 @@ export const openPositionSimpleTool = {
|
|
|
166
184
|
const collateralInput = String(args.collateralAmount ?? "").trim();
|
|
167
185
|
let collateralRaw = "";
|
|
168
186
|
let collateralRawBig = 0n;
|
|
187
|
+
let collateralMeta = { source: collateralInput ? "user" : "computed" };
|
|
169
188
|
if (collateralInput) {
|
|
170
189
|
collateralRaw = parseUserUnits(collateralInput, quoteDecimals, "collateralAmount");
|
|
171
190
|
collateralRawBig = asBigint(collateralRaw, "collateralAmount");
|
|
@@ -195,12 +214,25 @@ export const openPositionSimpleTool = {
|
|
|
195
214
|
sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
|
|
196
215
|
}
|
|
197
216
|
const sizeRawBig = asBigint(sizeRaw, "size");
|
|
217
|
+
const leverageBig = BigInt(args.leverage);
|
|
218
|
+
const impliedNotionalQuoteRaw = computeQuoteNotionalRaw(sizeRawBig, price30Big, baseDecimals, quoteDecimals);
|
|
198
219
|
if (!collateralInput) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
220
|
+
collateralRawBig = divRoundUp(impliedNotionalQuoteRaw, leverageBig);
|
|
221
|
+
if (collateralRawBig <= 0n) {
|
|
222
|
+
throw new Error("Computed collateralAmount is 0. Reduce size, reduce leverage, or check price precision.");
|
|
223
|
+
}
|
|
224
|
+
collateralRaw = collateralRawBig.toString();
|
|
225
|
+
collateralMeta = {
|
|
226
|
+
source: "computed",
|
|
227
|
+
impliedNotionalQuoteRaw: impliedNotionalQuoteRaw.toString(),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const maxNotionalFromCollateralRaw = collateralRawBig * leverageBig;
|
|
232
|
+
if (maxNotionalFromCollateralRaw < impliedNotionalQuoteRaw) {
|
|
233
|
+
const requiredCollateralRaw = divRoundUp(impliedNotionalQuoteRaw, leverageBig);
|
|
234
|
+
throw new Error(`collateralAmount is insufficient for open_position_simple. Based on size=${userSize || formatUnits(sizeRawBig, baseDecimals)}, price=${priceMeta.human ?? formatUnits(price30Big, 30)}, leverage=${args.leverage}, implied order value is ≈${formatUnits(impliedNotionalQuoteRaw, quoteDecimals)} quote, provided collateralAmount is ${formatUnits(collateralRawBig, quoteDecimals)}, and required collateralAmount is at least ≈${formatUnits(requiredCollateralRaw, quoteDecimals)}.`);
|
|
235
|
+
}
|
|
204
236
|
}
|
|
205
237
|
const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
|
|
206
238
|
if (maxTradeAmountHuman) {
|
|
@@ -267,6 +299,7 @@ export const openPositionSimpleTool = {
|
|
|
267
299
|
tradingFeeMeta,
|
|
268
300
|
autoDeposit: Boolean(args.autoDeposit ?? false),
|
|
269
301
|
priceMeta,
|
|
302
|
+
collateralMeta,
|
|
270
303
|
sizeMeta,
|
|
271
304
|
tpPrice: args.tpPrice ? parseUserPrice30(args.tpPrice, "tpPrice") : null,
|
|
272
305
|
tpSize: args.tpSize ? parseUserUnits(args.tpSize, baseDecimals, "tpSize") : (args.tpPrice ? sizeRaw : null),
|
|
@@ -338,8 +371,32 @@ export const openPositionSimpleTool = {
|
|
|
338
371
|
const raw = await openPosition(client, address, openArgs);
|
|
339
372
|
const data = await finalizeMutationResult(raw, signer, "open_position_simple");
|
|
340
373
|
const txHash = data.confirmation?.txHash;
|
|
341
|
-
const
|
|
342
|
-
const
|
|
374
|
+
const shouldVerify = Boolean(args.verify ?? false);
|
|
375
|
+
const verification = txHash && shouldVerify ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
|
|
376
|
+
const payload = {
|
|
377
|
+
prepared: prep,
|
|
378
|
+
approval,
|
|
379
|
+
...data,
|
|
380
|
+
verification,
|
|
381
|
+
verificationSkipped: !!txHash && !shouldVerify,
|
|
382
|
+
preflight: {
|
|
383
|
+
normalized: {
|
|
384
|
+
collateralAmountRaw: collateralRaw,
|
|
385
|
+
sizeRaw,
|
|
386
|
+
priceRaw30: price30,
|
|
387
|
+
sizeSemantics: "base_quantity",
|
|
388
|
+
impliedNotionalQuoteRaw: impliedNotionalQuoteRaw.toString(),
|
|
389
|
+
executionFeeToken,
|
|
390
|
+
timeInForce: openArgs.timeInForce,
|
|
391
|
+
tradingFeeRaw: String(tradingFeeRaw),
|
|
392
|
+
tpSizeRaw: openArgs.tpSize?.replace(/^raw:/i, "") ?? null,
|
|
393
|
+
tpPriceRaw30: openArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
|
|
394
|
+
slSizeRaw: openArgs.slSize?.replace(/^raw:/i, "") ?? null,
|
|
395
|
+
slPriceRaw30: openArgs.slPrice?.replace(/^raw:/i, "") ?? null,
|
|
396
|
+
},
|
|
397
|
+
tradingFeeMeta,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
343
400
|
return {
|
|
344
401
|
content: [
|
|
345
402
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michaleffffff/mcp-trading-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
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.4
|
|
24
|
+
"@myx-trade/sdk": "^1.0.4",
|
|
25
25
|
"dotenv": "^17.3.1",
|
|
26
26
|
"ethers": "^6.13.1",
|
|
27
27
|
"zod": "^4.3.6"
|