@michaleffffff/mcp-trading-server 3.0.2 → 3.0.4
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 +18 -0
- package/README.md +15 -7
- package/TOOL_EXAMPLES.md +54 -18
- package/dist/prompts/tradingGuide.js +7 -1
- package/dist/server.js +2 -2
- package/dist/services/marketService.js +2 -2
- package/dist/tools/accountTransfer.js +89 -3
- package/dist/tools/closeAllPositions.js +3 -5
- package/dist/tools/closePosition.js +59 -11
- package/dist/tools/createPerpMarket.js +1 -1
- package/dist/tools/executeTrade.js +1 -1
- package/dist/tools/getMyLpHoldings.js +265 -0
- package/dist/tools/index.js +1 -2
- package/dist/tools/manageLiquidity.js +82 -9
- package/dist/tools/marketInfo.js +18 -2
- package/dist/utils/slippage.js +23 -0
- package/package.json +1 -1
- package/dist/tools/getMarketList.js +0 -20
- package/dist/tools/getMarketListRaw.js +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.4 - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `get_my_lp_holdings` tool for listing all wallet LP balances with standardized naming.
|
|
7
|
+
- Enriched `manage_liquidity` response with `lpAssetNames` (e.g., `mBTC.USDC`).
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Improved `close_all_positions` with more robust automated price fetching.
|
|
11
|
+
- `get_liquidity_info` now accepts human-readable prices and `raw:` prefixes.
|
|
12
|
+
- Updated `TOOL_EXAMPLES.md` to include LP holding and management samples.
|
|
13
|
+
|
|
14
|
+
## 3.0.3 - 2026-03-17
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Enhanced `create_perp_market` tool description to better explain `marketId` requirements.
|
|
18
|
+
- Added usage hints to `execute_trade` schema for improved AI coordination.
|
|
19
|
+
- Internal: `resolvePool` now supports `chainIdOverride` for more flexible market resolution.
|
|
20
|
+
|
|
3
21
|
## 3.0.2 - 2026-03-17
|
|
4
22
|
|
|
5
23
|
### Added
|
package/README.md
CHANGED
|
@@ -124,7 +124,8 @@ The server will run using `stdio` transport and can be connected by any MCP-comp
|
|
|
124
124
|
2. **Configure env**: Copy `.env.example` to `.env` and fill in `PRIVATE_KEY`, `RPC_URL`, etc.
|
|
125
125
|
3. **Start the server**: Run `npm run build` then `npm run start` (use `npm run dev` for development).
|
|
126
126
|
4. **Configure your MCP client**: Set `command/args/env` in your MCP client (see Claude Desktop example below).
|
|
127
|
-
5. **Common flow**: Use `search_market` (or `
|
|
127
|
+
5. **Common flow**: Use `search_market` (or `get_pool_list`) to get `poolId`, then optionally `get_market_price` / `get_oracle_price` to view prices, and finally use `open_position_simple` (recommended) to open a position.
|
|
128
|
+
Note: `open_position_simple` does not accept `tpPrice` / `tpSize` / `slPrice` / `slSize`. Use `set_tp_sl` (for positions) or `update_order_tp_sl` (for orders).
|
|
128
129
|
6. **Tool examples**: See `TOOL_EXAMPLES.md` for practical examples for every tool, common workflows, and parameter-unit guidance.
|
|
129
130
|
|
|
130
131
|
**Minimal open position example:**
|
|
@@ -160,7 +161,12 @@ The server will run using `stdio` transport and can be connected by any MCP-comp
|
|
|
160
161
|
- **Raw units (explicit)**: Prefix with `raw:` to pass raw units exactly (e.g. `raw:100000000` for 100 USDC with 6 decimals).
|
|
161
162
|
- **Price**: `"price"` is human by default and will be converted to **30-decimal** format internally. Use `raw:` to pass a 30-decimal integer directly.
|
|
162
163
|
- **Slippage**: `slippagePct` uses 4-decimal raw units: `100` = `1%`, `50` = `0.5%`, `1000` = `10%`.
|
|
163
|
-
- **
|
|
164
|
+
- **close_all_positions slippage**: Supports raw 4dp (`100`) and human percent strings (`"1.0"` or `"1%"`).
|
|
165
|
+
- **get_liquidity_info marketPrice**: Supports raw 30-dec integer, `raw:...`, and human price inputs (`"2172.5"` / `human:2172.5`).
|
|
166
|
+
- **close_position full-close helper**: `size` and `collateralAmount` accept `ALL` / `FULL` / `MAX` to auto-use exact live raw values.
|
|
167
|
+
- **manage_liquidity action**: Case-insensitive (`ADD`, `add`, `Increase` all supported).
|
|
168
|
+
- **account_deposit approval**: `account_deposit` now supports `autoApprove` and `approveMax` to auto-handle allowance when required.
|
|
169
|
+
- **Note**: `check_approval.amount` still expects **raw integer strings**.
|
|
164
170
|
- **`adjust_margin` units**: human amount is supported (e.g. `"1"` means 1 quote token). For exact atomic amount, use `raw:` (e.g. `"raw:1"`).
|
|
165
171
|
|
|
166
172
|
## P0 Reliability Contract (AI-friendly)
|
|
@@ -200,7 +206,7 @@ Common error codes:
|
|
|
200
206
|
Search behavior in P0:
|
|
201
207
|
- `search_market` now accepts empty `keyword` and returns active markets.
|
|
202
208
|
- When `search_market` returns empty/unstable data, server-side fallback uses SDK `api.getMarketList()` and `api.getPoolList()` to rebuild active-market results.
|
|
203
|
-
- For robust discovery, you can also call `
|
|
209
|
+
- For robust discovery, you can also call `get_pool_list` directly.
|
|
204
210
|
|
|
205
211
|
Example:
|
|
206
212
|
|
|
@@ -285,12 +291,12 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
285
291
|
|
|
286
292
|
# Tools
|
|
287
293
|
|
|
288
|
-
The server exposes
|
|
294
|
+
The server exposes tools categorized for AI:
|
|
289
295
|
|
|
290
296
|
For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
|
|
291
297
|
|
|
292
298
|
### Trading Operations
|
|
293
|
-
* **open_position_simple**: High-level open position helper (recommended). Computes price/size/tradingFee internally.
|
|
299
|
+
* **open_position_simple**: High-level open position helper (recommended). Computes price/size/tradingFee internally. It only handles open/increase parameters and does **not** accept TP/SL fields (`tpPrice`, `tpSize`, `slPrice`, `slSize`); unknown keys are rejected by strict schema validation.
|
|
294
300
|
* **execute_trade**: Execute a new trade or add to an existing position (includes parameter preflight, supports `human:`/`raw:` amount prefixes, auto-computes `tradingFee` when omitted).
|
|
295
301
|
* **close_position**: Close an open position.
|
|
296
302
|
* **close_all_positions**: Emergency: close ALL open positions in a pool at once.
|
|
@@ -308,12 +314,13 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
|
|
|
308
314
|
* **get_oracle_price**: Get the current EVM oracle price for a specific pool.
|
|
309
315
|
* **get_kline_latest_bar**: Get only the latest bar for an interval.
|
|
310
316
|
* **get_all_tickers**: Get all ticker snapshots in one request.
|
|
311
|
-
* **
|
|
317
|
+
* **get_kline**, **get_pool_info**... (`get_pool_info` auto-retries with oracle/ticker price on divide-by-zero reads)
|
|
312
318
|
* **get_pool_symbol_all**, **get_base_detail**, **get_pool_level_config**...
|
|
313
319
|
|
|
314
320
|
### Account & Portfolio
|
|
315
321
|
* **get_positions**: Get all open positions for the current account.
|
|
316
322
|
* **get_account**: Unified account snapshot. Clearly separates **wallet balance** and **trading-account balance** (provide `poolId` for full trading-account metrics).
|
|
323
|
+
* **get_my_lp_holdings**: Scan pools and return your base/quote LP token balances on the current chain, with standardized LP asset names (`base: mBASE.QUOTE`, `quote: mQUOTE.BASE`).
|
|
317
324
|
* **get_account_vip_info**: Query account VIP tier details.
|
|
318
325
|
* **get_position_history**, **get_open_orders**, **get_order_history**...
|
|
319
326
|
|
|
@@ -323,7 +330,8 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
|
|
|
323
330
|
- If one section fails (wallet or trading-account), the tool may return a **partial** snapshot with `meta.partial=true` and section-level `error` details.
|
|
324
331
|
|
|
325
332
|
### Liquidity Provision (LP)
|
|
326
|
-
* **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool (aliases: `add/remove/increase/decrease` are supported).
|
|
333
|
+
* **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool (aliases: `add/remove/increase/decrease` are supported). `poolId` is pre-validated on target `chainId` for clearer errors. Response includes LP naming metadata (`baseLpAssetName=mBASE.QUOTE`, `quoteLpAssetName=mQUOTE.BASE`, `operatedLpAssetName`).
|
|
334
|
+
- Example (`BTC/USDC`, `poolType=QUOTE`): `operatedLpAssetName = mUSDC.BTC`.
|
|
327
335
|
* **create_perp_market**: Create a new perpetual trading pair.
|
|
328
336
|
|
|
329
337
|
---
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -62,6 +62,10 @@ It includes:
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
|
+
Notes:
|
|
66
|
+
- `open_position_simple` only accepts open/increase parameters and does not support `tpPrice`, `tpSize`, `slPrice`, `slSize` in the same call.
|
|
67
|
+
- If those TP/SL keys are included, the server rejects them as unrecognized fields (strict schema validation).
|
|
68
|
+
- Recommended flow: open with `open_position_simple` first, then use `set_tp_sl` (position-level) or `update_order_tp_sl` (order-level).
|
|
65
69
|
|
|
66
70
|
### `execute_trade`
|
|
67
71
|
```json
|
|
@@ -296,24 +300,6 @@ Success payload includes normalized raw amount under `data.normalized.adjustAmou
|
|
|
296
300
|
```
|
|
297
301
|
Note: this tool may internally fallback to SDK `api.getMarketList()` / `api.getPoolList()` when direct search is empty.
|
|
298
302
|
|
|
299
|
-
### `get_market_list`
|
|
300
|
-
```json
|
|
301
|
-
{
|
|
302
|
-
"name": "get_market_list",
|
|
303
|
-
"arguments": {
|
|
304
|
-
"limit": 50
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### `get_market_list_raw`
|
|
310
|
-
```json
|
|
311
|
-
{
|
|
312
|
-
"name": "get_market_list_raw",
|
|
313
|
-
"arguments": {}
|
|
314
|
-
}
|
|
315
|
-
```
|
|
316
|
-
|
|
317
303
|
### `get_pool_list`
|
|
318
304
|
```json
|
|
319
305
|
{
|
|
@@ -455,6 +441,31 @@ If one section fails, check `data.meta.partial` and section-level `error` fields
|
|
|
455
441
|
}
|
|
456
442
|
```
|
|
457
443
|
|
|
444
|
+
### `get_my_lp_holdings`
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"name": "get_my_lp_holdings",
|
|
448
|
+
"arguments": {}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
LP asset naming rule in each item:
|
|
452
|
+
- `baseLpAssetName`: `mBASE.QUOTE` (e.g. `mBTC.USDC`)
|
|
453
|
+
- `quoteLpAssetName`: `mQUOTE.BASE` (e.g. `mUSDC.BTC`)
|
|
454
|
+
Include zero-balance pools and optional filtering:
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"name": "get_my_lp_holdings",
|
|
458
|
+
"arguments": {
|
|
459
|
+
"includeZero": true,
|
|
460
|
+
"poolIds": [
|
|
461
|
+
"0xPOOL_ID_1",
|
|
462
|
+
"0xPOOL_ID_2"
|
|
463
|
+
],
|
|
464
|
+
"maxPools": 50
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
458
469
|
### `get_positions`
|
|
459
470
|
```json
|
|
460
471
|
{
|
|
@@ -563,6 +574,31 @@ Alias action example (`remove` == `withdraw`):
|
|
|
563
574
|
}
|
|
564
575
|
```
|
|
565
576
|
If operation fails, check `error.message` for concrete reasons (for example `Insufficient Balance`) instead of generic `undefined`.
|
|
577
|
+
`poolId` must be valid on the target `chainId`; if uncertain, resolve with `search_market` / `get_pool_list` first.
|
|
578
|
+
Success response includes `data.lpAssetNames`:
|
|
579
|
+
- `baseLpAssetName`: `mBASE.QUOTE` (e.g. `mBTC.USDC`)
|
|
580
|
+
- `quoteLpAssetName`: `mQUOTE.BASE` (e.g. `mUSDC.BTC`)
|
|
581
|
+
- `operatedLpAssetName`: LP asset name matching current `poolType`
|
|
582
|
+
Example success payload (abridged):
|
|
583
|
+
```json
|
|
584
|
+
{
|
|
585
|
+
"status": "success",
|
|
586
|
+
"data": {
|
|
587
|
+
"confirmation": {
|
|
588
|
+
"confirmed": true,
|
|
589
|
+
"txHash": "0x..."
|
|
590
|
+
},
|
|
591
|
+
"lpAssetNames": {
|
|
592
|
+
"baseSymbol": "BTC",
|
|
593
|
+
"quoteSymbol": "USDC",
|
|
594
|
+
"baseLpAssetName": "mBTC.USDC",
|
|
595
|
+
"quoteLpAssetName": "mUSDC.BTC",
|
|
596
|
+
"operatedPoolType": "QUOTE",
|
|
597
|
+
"operatedLpAssetName": "mUSDC.BTC"
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
566
602
|
|
|
567
603
|
### `get_lp_price`
|
|
568
604
|
```json
|
|
@@ -17,7 +17,7 @@ export const tradingGuidePrompt = {
|
|
|
17
17
|
You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
|
|
18
18
|
|
|
19
19
|
## 1. The Standard Workflow
|
|
20
|
-
1. **Discovery**: Use \`search_market\` with a keyword (or empty keyword) to find active \`poolId\` values. If needed, fallback to \`
|
|
20
|
+
1. **Discovery**: Use \`search_market\` with a keyword (or empty keyword) to find active \`poolId\` values. If needed, fallback to \`get_pool_list\`.
|
|
21
21
|
2. **Context**: Use \`get_market_price\` and \`get_account\` (with \`poolId\`) to check market state, wallet balance, and trading-account margin.
|
|
22
22
|
3. **Execution**: Prefer \`open_position_simple\` for new trades. It handles unit conversions and pool resolution automatically.
|
|
23
23
|
4. **Validation**: Always check the \`verification.verified\` flag in the output. If \`false\`, read the \`cancelReason\` to explain the failure to the user.
|
|
@@ -31,9 +31,15 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
31
31
|
- **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
|
|
32
32
|
- **execute_trade**: Has built-in preflight normalization and can auto-compute \`tradingFee\` when omitted.
|
|
33
33
|
- **Balances**: Use \`get_account\` to clearly separate wallet balance vs trading-account balance (pass \`poolId\` for trading-account metrics).
|
|
34
|
+
- **LP Holdings**: Use \`get_my_lp_holdings\` to scan base/quote LP token balances across pools on the current chain; naming convention is \`baseLpAssetName=mBASE.QUOTE\`, \`quoteLpAssetName=mQUOTE.BASE\` (e.g., \`mBTC.USDC\`, \`mUSDC.BTC\`).
|
|
35
|
+
- **Liquidity Mutation Output**: \`manage_liquidity\` success output includes \`data.lpAssetNames\` with \`baseLpAssetName\`, \`quoteLpAssetName\`, and \`operatedLpAssetName\`.
|
|
34
36
|
- **Adjust Margin**: \`adjust_margin.adjustAmount\` supports human amount (e.g. "1"), and also supports exact raw amount via \`raw:\` prefix.
|
|
35
37
|
- **TP/SL Updates**: \`update_order_tp_sl\` accepts boolean-like values for \`useOrderCollateral\` and \`isTpSlOrder\`, but pending LIMIT/STOP orders can still be rejected by protocol rules; after fill, prefer \`set_tp_sl\`.
|
|
36
38
|
- **Pool Reads**: \`get_pool_info\` auto-retries with oracle/ticker price when direct on-chain read returns divide-by-zero.
|
|
39
|
+
- **Liquidity Price Input**: \`get_liquidity_info.marketPrice\` accepts raw 30-dec price and human price strings (e.g. \`2172.5\`, \`human:2172.5\`).
|
|
40
|
+
- **close_all_positions Slippage**: accepts both raw 4dp and human percent strings (e.g. \`"100"\`, \`"1.0"\`, \`"1%"\`).
|
|
41
|
+
- **close_position Full Close**: \`size\`/\`collateralAmount\` can use \`ALL\`/\`FULL\`/\`MAX\` to auto-fill exact live raw values from the current position snapshot.
|
|
42
|
+
- **Deposits & Approval**: \`account_deposit\` supports \`autoApprove\` + \`approveMax\`; without auto-approve, allowance errors should be handled via \`check_approval\`.
|
|
37
43
|
- **Examples**: Follow \`TOOL_EXAMPLES.md\` payload patterns when building tool arguments.
|
|
38
44
|
|
|
39
45
|
## 3. Self-Healing
|
package/dist/server.js
CHANGED
|
@@ -352,7 +352,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
352
352
|
};
|
|
353
353
|
}
|
|
354
354
|
// ─── MCP Server ───
|
|
355
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.
|
|
355
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.4" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
356
356
|
// List tools
|
|
357
357
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
358
358
|
return {
|
|
@@ -473,7 +473,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
473
473
|
async function main() {
|
|
474
474
|
const transport = new StdioServerTransport();
|
|
475
475
|
await server.connect(transport);
|
|
476
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
476
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.4 running (stdio, pure on-chain, prod ready)");
|
|
477
477
|
}
|
|
478
478
|
main().catch((err) => {
|
|
479
479
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -162,8 +162,8 @@ export async function getPoolLevelConfig(client, poolId, chainIdOverride) {
|
|
|
162
162
|
/**
|
|
163
163
|
* 智能解析 Pool ID (支持 ID 校验与关键词回退)
|
|
164
164
|
*/
|
|
165
|
-
export async function resolvePool(client, poolId, keyword) {
|
|
166
|
-
const chainId = getChainId();
|
|
165
|
+
export async function resolvePool(client, poolId, keyword, chainIdOverride) {
|
|
166
|
+
const chainId = chainIdOverride ?? getChainId();
|
|
167
167
|
let pid = poolId?.trim();
|
|
168
168
|
// 1. 如果提供了 poolId,先尝试验证其是否存在
|
|
169
169
|
if (pid) {
|
|
@@ -2,16 +2,53 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { normalizeAddress } from "../utils/address.js";
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
|
|
6
|
+
function asBigintOrNull(value) {
|
|
7
|
+
try {
|
|
8
|
+
const normalized = String(value ?? "").trim();
|
|
9
|
+
if (!normalized)
|
|
10
|
+
return null;
|
|
11
|
+
if (!/^-?\d+$/.test(normalized))
|
|
12
|
+
return null;
|
|
13
|
+
return BigInt(normalized);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function readReleaseTime(accountInfo) {
|
|
20
|
+
const data = accountInfo?.data;
|
|
21
|
+
if (Array.isArray(data)) {
|
|
22
|
+
return asBigintOrNull(data[6]) ?? 0n;
|
|
23
|
+
}
|
|
24
|
+
return asBigintOrNull(data?.releaseTime ?? data?.release_time) ?? 0n;
|
|
25
|
+
}
|
|
26
|
+
function readAvailableMarginRaw(raw) {
|
|
27
|
+
if (raw === null || raw === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
if (typeof raw === "object" && "data" in raw) {
|
|
30
|
+
return asBigintOrNull(raw.data);
|
|
31
|
+
}
|
|
32
|
+
return asBigintOrNull(raw);
|
|
33
|
+
}
|
|
34
|
+
function formatUnixTimestamp(timestamp) {
|
|
35
|
+
const numeric = Number(timestamp);
|
|
36
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
37
|
+
return String(timestamp);
|
|
38
|
+
return `${timestamp.toString()} (${new Date(numeric * 1000).toISOString()})`;
|
|
39
|
+
}
|
|
5
40
|
export const accountDepositTool = {
|
|
6
41
|
name: "account_deposit",
|
|
7
42
|
description: "Deposit funds from wallet into the MYX trading account.",
|
|
8
43
|
schema: {
|
|
9
44
|
amount: z.union([z.string(), z.number()]).describe("Amount to deposit (human-readable or raw units)"),
|
|
10
45
|
tokenAddress: z.string().describe("Token address"),
|
|
46
|
+
autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token allowance when needed (default false)."),
|
|
47
|
+
approveMax: z.coerce.boolean().optional().describe("If autoApprove=true, approve MaxUint256 instead of exact amount."),
|
|
11
48
|
},
|
|
12
49
|
handler: async (args) => {
|
|
13
50
|
try {
|
|
14
|
-
const { client, signer } = await resolveClient();
|
|
51
|
+
const { client, signer, address } = await resolveClient();
|
|
15
52
|
const chainId = getChainId();
|
|
16
53
|
const tokenAddress = normalizeAddress(args.tokenAddress, "tokenAddress");
|
|
17
54
|
// For deposit, we default to quote decimals (6) as it's the most common use case.
|
|
@@ -19,13 +56,32 @@ export const accountDepositTool = {
|
|
|
19
56
|
const { ensureUnits } = await import("../utils/units.js");
|
|
20
57
|
const { getQuoteDecimals } = await import("../auth/resolveClient.js");
|
|
21
58
|
const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount", { allowImplicitRaw: false });
|
|
59
|
+
let approval = null;
|
|
60
|
+
const needApproval = await client.utils.needsApproval(address, chainId, tokenAddress, amount);
|
|
61
|
+
if (needApproval) {
|
|
62
|
+
if (!args.autoApprove) {
|
|
63
|
+
throw new Error(`Insufficient token allowance for deposit amount ${amount}. ` +
|
|
64
|
+
`Run check_approval (token=${tokenAddress}) or retry with autoApprove=true.`);
|
|
65
|
+
}
|
|
66
|
+
const approveAmount = args.approveMax ? MAX_UINT256 : amount;
|
|
67
|
+
const rawApproval = await client.utils.approveAuthorization({
|
|
68
|
+
chainId,
|
|
69
|
+
quoteAddress: tokenAddress,
|
|
70
|
+
amount: approveAmount,
|
|
71
|
+
});
|
|
72
|
+
approval = await finalizeMutationResult(rawApproval, signer, "account_deposit_approval");
|
|
73
|
+
}
|
|
22
74
|
const raw = await client.account.deposit({
|
|
23
75
|
amount,
|
|
24
76
|
tokenAddress,
|
|
25
77
|
chainId,
|
|
26
78
|
});
|
|
27
79
|
const data = await finalizeMutationResult(raw, signer, "account_deposit");
|
|
28
|
-
|
|
80
|
+
const payload = {
|
|
81
|
+
...data,
|
|
82
|
+
approval: approval ? { performed: true, details: approval } : { performed: false, needApproval: !!needApproval },
|
|
83
|
+
};
|
|
84
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
29
85
|
}
|
|
30
86
|
catch (error) {
|
|
31
87
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -49,6 +105,29 @@ export const accountWithdrawTool = {
|
|
|
49
105
|
// Assuming 18 decimals for base and quoteDecimals for quote
|
|
50
106
|
const decimals = args.isQuoteToken ? getQuoteDecimals() : 18;
|
|
51
107
|
const amount = ensureUnits(args.amount, decimals, "amount", { allowImplicitRaw: false });
|
|
108
|
+
const amountRaw = asBigintOrNull(amount);
|
|
109
|
+
if (amountRaw === null || amountRaw <= 0n) {
|
|
110
|
+
throw new Error(`amount must be a positive integer raw value after normalization, got: ${amount}`);
|
|
111
|
+
}
|
|
112
|
+
// Preflight to avoid avoidable AccountInsufficientFreeAmount reverts caused by locked funds.
|
|
113
|
+
const [accountInfoRaw, availableMarginRawResult] = await Promise.all([
|
|
114
|
+
client.account.getAccountInfo(chainId, address, args.poolId).catch(() => null),
|
|
115
|
+
client.account.getAvailableMarginBalance({ poolId: args.poolId, chainId, address }).catch(() => null),
|
|
116
|
+
]);
|
|
117
|
+
const releaseTime = readReleaseTime(accountInfoRaw);
|
|
118
|
+
const availableMarginRaw = readAvailableMarginRaw(availableMarginRawResult);
|
|
119
|
+
const nowSec = BigInt(Math.floor(Date.now() / 1000));
|
|
120
|
+
if (availableMarginRaw !== null && amountRaw > availableMarginRaw) {
|
|
121
|
+
const lockHint = releaseTime > nowSec
|
|
122
|
+
? ` Funds are partially locked until releaseTime=${formatUnixTimestamp(releaseTime)}.`
|
|
123
|
+
: "";
|
|
124
|
+
throw new Error(`Requested withdraw amount ${amountRaw.toString()} exceeds current withdrawable margin ${availableMarginRaw.toString()}.` +
|
|
125
|
+
lockHint);
|
|
126
|
+
}
|
|
127
|
+
if (releaseTime > nowSec && availableMarginRaw !== null && availableMarginRaw <= 0n) {
|
|
128
|
+
throw new Error(`Account has locked funds until releaseTime=${formatUnixTimestamp(releaseTime)}. ` +
|
|
129
|
+
`Retry after unlock or reduce withdraw amount.`);
|
|
130
|
+
}
|
|
52
131
|
const raw = await client.account.withdraw({
|
|
53
132
|
chainId,
|
|
54
133
|
receiver: address,
|
|
@@ -57,7 +136,14 @@ export const accountWithdrawTool = {
|
|
|
57
136
|
isQuoteToken: args.isQuoteToken,
|
|
58
137
|
});
|
|
59
138
|
const data = await finalizeMutationResult(raw, signer, "account_withdraw");
|
|
60
|
-
|
|
139
|
+
const preflight = {
|
|
140
|
+
requestedAmountRaw: amountRaw.toString(),
|
|
141
|
+
availableMarginRaw: availableMarginRaw?.toString() ?? null,
|
|
142
|
+
releaseTime: releaseTime.toString(),
|
|
143
|
+
releaseTimeIso: Number(releaseTime) > 0 ? new Date(Number(releaseTime) * 1000).toISOString() : null,
|
|
144
|
+
locked: releaseTime > nowSec,
|
|
145
|
+
};
|
|
146
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { ...data, preflight } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
61
147
|
}
|
|
62
148
|
catch (error) {
|
|
63
149
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -3,15 +3,13 @@ import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.
|
|
|
3
3
|
import { getOraclePrice } from "../services/marketService.js";
|
|
4
4
|
import { ensureUnits } from "../utils/units.js";
|
|
5
5
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
6
|
-
import {
|
|
6
|
+
import { normalizeSlippagePct4dpFlexible, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
7
7
|
export const closeAllPositionsTool = {
|
|
8
8
|
name: "close_all_positions",
|
|
9
9
|
description: "Emergency: close ALL open positions in a pool at once. Use for risk management.",
|
|
10
10
|
schema: {
|
|
11
11
|
poolId: z.string().describe("Pool ID to close all positions in"),
|
|
12
|
-
slippagePct: z.
|
|
13
|
-
message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
|
|
14
|
-
}).optional().describe(SLIPPAGE_PCT_4DP_DESC),
|
|
12
|
+
slippagePct: z.union([z.string(), z.number()]).optional().describe(`${SLIPPAGE_PCT_4DP_DESC}. Also supports human percent format like "1.0" or "1%".`),
|
|
15
13
|
},
|
|
16
14
|
handler: async (args) => {
|
|
17
15
|
try {
|
|
@@ -28,7 +26,7 @@ export const closeAllPositionsTool = {
|
|
|
28
26
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { message: "No open positions in this pool.", closed: 0 } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
29
27
|
}
|
|
30
28
|
// 2) 为每个仓位构建平仓参数
|
|
31
|
-
const slippagePct =
|
|
29
|
+
const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "200");
|
|
32
30
|
// 3) We need actual oracle prices to avoid Revert 0x613970e0 (InvalidParameter)
|
|
33
31
|
const oraclePriceReq = await getOraclePrice(client, args.poolId).catch(() => null);
|
|
34
32
|
let fallbackPrice = "0";
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { resolveClient } from "../auth/resolveClient.js";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { closePosition as closePos } from "../services/tradeService.js";
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
5
|
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
|
+
const FULL_CLOSE_MARKERS = new Set(["ALL", "FULL", "MAX"]);
|
|
9
|
+
const INTEGER_RE = /^\d+$/;
|
|
10
|
+
function wantsFullCloseMarker(input) {
|
|
11
|
+
const value = String(input ?? "").trim().toUpperCase();
|
|
12
|
+
return FULL_CLOSE_MARKERS.has(value);
|
|
13
|
+
}
|
|
14
|
+
function readRawPositionField(position, primary, fallback) {
|
|
15
|
+
const first = String(position?.[primary] ?? "").trim();
|
|
16
|
+
if (INTEGER_RE.test(first))
|
|
17
|
+
return first;
|
|
18
|
+
if (fallback) {
|
|
19
|
+
const second = String(position?.[fallback] ?? "").trim();
|
|
20
|
+
if (INTEGER_RE.test(second))
|
|
21
|
+
return second;
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
8
25
|
export const closePositionTool = {
|
|
9
26
|
name: "close_position",
|
|
10
27
|
description: "Create a decrease order using SDK-native parameters.",
|
|
@@ -14,8 +31,8 @@ export const closePositionTool = {
|
|
|
14
31
|
orderType: z.union([z.number(), z.string()]).describe("Order type: 0/MARKET or 1/LIMIT"),
|
|
15
32
|
triggerType: z.union([z.number(), z.string()]).optional().describe("Trigger type: 0/NONE, 1/GTE, 2/LTE"),
|
|
16
33
|
direction: z.union([z.number(), z.string()]).describe("Position direction: 0/LONG or 1/SHORT"),
|
|
17
|
-
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human
|
|
18
|
-
size: z.union([z.string(), z.number()]).describe("Position size (human
|
|
34
|
+
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human/raw). Also supports ALL/FULL/MAX to use live position collateral raw."),
|
|
35
|
+
size: z.union([z.string(), z.number()]).describe("Position size (human/raw). Also supports ALL/FULL/MAX for exact full-close raw size."),
|
|
19
36
|
price: z.union([z.string(), z.number()]).describe("Price (human or 30-dec raw units)"),
|
|
20
37
|
timeInForce: z.coerce.number().int().describe("TimeInForce: 0=GTC, 1=IOC, 2=FOK"),
|
|
21
38
|
postOnly: z.coerce.boolean().describe("Post-only flag"),
|
|
@@ -26,26 +43,57 @@ export const closePositionTool = {
|
|
|
26
43
|
handler: async (args) => {
|
|
27
44
|
try {
|
|
28
45
|
const { client, address, signer } = await resolveClient();
|
|
29
|
-
const
|
|
30
|
-
const
|
|
46
|
+
const chainId = getChainId();
|
|
47
|
+
const preparedArgs = { ...args };
|
|
48
|
+
const poolId = preparedArgs.poolId;
|
|
31
49
|
// Fetch pool detail to get quoteToken for execution fee
|
|
32
50
|
const poolResponse = await client.markets.getMarketDetail({ chainId, poolId });
|
|
33
51
|
const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
|
|
34
52
|
if (!poolData)
|
|
35
53
|
throw new Error(`Could not find pool metadata for ID: ${poolId}`);
|
|
54
|
+
// Precision helper for full close: align with exact raw position values.
|
|
55
|
+
const needAutoSize = wantsFullCloseMarker(preparedArgs.size);
|
|
56
|
+
const needAutoCollateral = wantsFullCloseMarker(preparedArgs.collateralAmount);
|
|
57
|
+
if (needAutoSize || needAutoCollateral) {
|
|
58
|
+
const positionsRes = await client.position.listPositions(address);
|
|
59
|
+
const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
|
|
60
|
+
const positionId = String(preparedArgs.positionId ?? "").trim().toLowerCase();
|
|
61
|
+
const target = positions.find((position) => {
|
|
62
|
+
const pid = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
|
|
63
|
+
const pool = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
|
|
64
|
+
return pid === positionId && pool === String(poolId).toLowerCase();
|
|
65
|
+
});
|
|
66
|
+
if (!target) {
|
|
67
|
+
throw new Error(`Could not find live position snapshot for positionId=${preparedArgs.positionId} in poolId=${poolId}.`);
|
|
68
|
+
}
|
|
69
|
+
if (needAutoSize) {
|
|
70
|
+
const rawSize = readRawPositionField(target, "size", "positionSize");
|
|
71
|
+
if (!rawSize || rawSize === "0") {
|
|
72
|
+
throw new Error(`Resolved position size is empty/zero for positionId=${preparedArgs.positionId}.`);
|
|
73
|
+
}
|
|
74
|
+
preparedArgs.size = `raw:${rawSize}`;
|
|
75
|
+
}
|
|
76
|
+
if (needAutoCollateral) {
|
|
77
|
+
const rawCollateral = readRawPositionField(target, "collateral", "collateralAmount");
|
|
78
|
+
if (!rawCollateral) {
|
|
79
|
+
throw new Error(`Resolved position collateral is empty for positionId=${preparedArgs.positionId}.`);
|
|
80
|
+
}
|
|
81
|
+
preparedArgs.collateralAmount = `raw:${rawCollateral}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
36
84
|
const mappedArgs = {
|
|
37
|
-
...
|
|
38
|
-
direction: mapDirection(
|
|
39
|
-
orderType: mapOrderType(
|
|
40
|
-
triggerType:
|
|
41
|
-
executionFeeToken: poolData.quoteToken ||
|
|
85
|
+
...preparedArgs,
|
|
86
|
+
direction: mapDirection(preparedArgs.direction),
|
|
87
|
+
orderType: mapOrderType(preparedArgs.orderType),
|
|
88
|
+
triggerType: preparedArgs.triggerType !== undefined ? mapTriggerType(preparedArgs.triggerType) : undefined,
|
|
89
|
+
executionFeeToken: poolData.quoteToken || preparedArgs.executionFeeToken
|
|
42
90
|
};
|
|
43
91
|
const raw = await closePos(client, address, mappedArgs);
|
|
44
92
|
const data = await finalizeMutationResult(raw, signer, "close_position");
|
|
45
93
|
const txHash = data.confirmation?.txHash;
|
|
46
94
|
let verification = null;
|
|
47
95
|
if (txHash) {
|
|
48
|
-
verification = await verifyTradeOutcome(client, address,
|
|
96
|
+
verification = await verifyTradeOutcome(client, address, preparedArgs.poolId, txHash);
|
|
49
97
|
}
|
|
50
98
|
const payload = { ...data, verification };
|
|
51
99
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
@@ -4,7 +4,7 @@ import { resolveClient } from "../auth/resolveClient.js";
|
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
5
|
export const createPerpMarketTool = {
|
|
6
6
|
name: "create_perp_market",
|
|
7
|
-
description: "Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use
|
|
7
|
+
description: "Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use get_market_detail (after search_market/get_pool_list) to fetch an existing marketId if you don't have a specific newly allocated one.",
|
|
8
8
|
schema: {
|
|
9
9
|
baseToken: z.string().describe("Base token contract address (e.g., 0xb40aaadc43...)"),
|
|
10
10
|
marketId: z.string().describe("MUST be a valid 66-char config hash (e.g., existing USDC marketId: 0x7f6727d8026fd2c87ccc745846c83cd0b68e886c73e1e05a54a675bcadd8adb6). Do NOT generate randomly."),
|
|
@@ -35,7 +35,7 @@ export const executeTradeTool = {
|
|
|
35
35
|
tradingFee: z.union([z.string(), z.number()]).optional().describe("Trading fee in quote token units. Supports human/raw prefix. Optional: auto-computed via get_user_trading_fee_rate."),
|
|
36
36
|
assetClass: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup assetClass (default from pool config or 1)."),
|
|
37
37
|
riskTier: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup riskTier (default from pool config or 1)."),
|
|
38
|
-
marketId: z.string().describe("Specific Market Config Hash. Fetch via
|
|
38
|
+
marketId: z.string().describe("Specific Market Config Hash. Fetch via get_market_detail (resolve poolId first with search_market/get_pool_list)."),
|
|
39
39
|
},
|
|
40
40
|
handler: async (args) => {
|
|
41
41
|
try {
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { COMMON_LP_AMOUNT_DECIMALS } from "@myx-trade/sdk";
|
|
4
|
+
import { Contract, formatUnits } from "ethers";
|
|
5
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
6
|
+
const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
7
|
+
const ERC20_BALANCE_ABI = ["function balanceOf(address owner) view returns (uint256)"];
|
|
8
|
+
function collectRows(input) {
|
|
9
|
+
if (Array.isArray(input))
|
|
10
|
+
return input.flatMap(collectRows);
|
|
11
|
+
if (!input || typeof input !== "object")
|
|
12
|
+
return [];
|
|
13
|
+
if (input.poolId || input.pool_id)
|
|
14
|
+
return [input];
|
|
15
|
+
return Object.values(input).flatMap(collectRows);
|
|
16
|
+
}
|
|
17
|
+
function readAddress(value) {
|
|
18
|
+
const text = String(value ?? "").trim();
|
|
19
|
+
if (!text || !ADDRESS_RE.test(text))
|
|
20
|
+
return null;
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
function normalizePoolId(value) {
|
|
24
|
+
return String(value ?? "").trim();
|
|
25
|
+
}
|
|
26
|
+
function normalizeAssetSymbol(value) {
|
|
27
|
+
const text = String(value ?? "").trim();
|
|
28
|
+
if (!text)
|
|
29
|
+
return "";
|
|
30
|
+
return text.replace(/\s+/g, "").replace(/\//g, "").toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
function parsePairSymbols(value) {
|
|
33
|
+
const text = String(value ?? "").trim();
|
|
34
|
+
if (!text)
|
|
35
|
+
return null;
|
|
36
|
+
const parts = text
|
|
37
|
+
.split(/[\/:_-]/)
|
|
38
|
+
.map((item) => normalizeAssetSymbol(item))
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
if (parts.length >= 2) {
|
|
41
|
+
return { base: parts[0], quote: parts[1] };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function resolveBaseQuoteSymbols(row, detail) {
|
|
46
|
+
const baseCandidates = [
|
|
47
|
+
row?.baseSymbol,
|
|
48
|
+
detail?.baseSymbol,
|
|
49
|
+
row?.base_symbol,
|
|
50
|
+
detail?.base_symbol,
|
|
51
|
+
];
|
|
52
|
+
const quoteCandidates = [
|
|
53
|
+
row?.quoteSymbol,
|
|
54
|
+
detail?.quoteSymbol,
|
|
55
|
+
row?.quote_symbol,
|
|
56
|
+
detail?.quote_symbol,
|
|
57
|
+
];
|
|
58
|
+
let baseSymbol = baseCandidates.map((item) => normalizeAssetSymbol(item)).find(Boolean) || "";
|
|
59
|
+
let quoteSymbol = quoteCandidates.map((item) => normalizeAssetSymbol(item)).find(Boolean) || "";
|
|
60
|
+
if (!baseSymbol || !quoteSymbol) {
|
|
61
|
+
const pairCandidate = row?.baseQuoteSymbol ??
|
|
62
|
+
detail?.baseQuoteSymbol ??
|
|
63
|
+
row?.symbol ??
|
|
64
|
+
detail?.symbol ??
|
|
65
|
+
row?.symbolName ??
|
|
66
|
+
detail?.symbolName;
|
|
67
|
+
const parsed = parsePairSymbols(pairCandidate);
|
|
68
|
+
if (parsed) {
|
|
69
|
+
baseSymbol = baseSymbol || parsed.base;
|
|
70
|
+
quoteSymbol = quoteSymbol || parsed.quote;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
baseSymbol: baseSymbol || null,
|
|
75
|
+
quoteSymbol: quoteSymbol || null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function buildLpAssetNames(baseSymbol, quoteSymbol) {
|
|
79
|
+
if (!baseSymbol || !quoteSymbol) {
|
|
80
|
+
return { baseLpAssetName: null, quoteLpAssetName: null };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
baseLpAssetName: `m${baseSymbol}.${quoteSymbol}`,
|
|
84
|
+
quoteLpAssetName: `m${quoteSymbol}.${baseSymbol}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function normalizePoolIdsInput(input) {
|
|
88
|
+
if (Array.isArray(input)) {
|
|
89
|
+
return input.map((item) => normalizePoolId(item)).filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
if (typeof input !== "string")
|
|
92
|
+
return [];
|
|
93
|
+
const text = input.trim();
|
|
94
|
+
if (!text)
|
|
95
|
+
return [];
|
|
96
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(text);
|
|
99
|
+
if (Array.isArray(parsed)) {
|
|
100
|
+
return parsed.map((item) => normalizePoolId(item)).filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (text.includes(",")) {
|
|
107
|
+
return text.split(",").map((item) => normalizePoolId(item)).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
return [text];
|
|
110
|
+
}
|
|
111
|
+
function toSymbol(row) {
|
|
112
|
+
const baseQuote = String(row?.baseQuoteSymbol ?? row?.symbol ?? "").trim();
|
|
113
|
+
if (baseQuote)
|
|
114
|
+
return baseQuote;
|
|
115
|
+
const base = String(row?.baseSymbol ?? "").trim();
|
|
116
|
+
const quote = String(row?.quoteSymbol ?? "").trim();
|
|
117
|
+
if (base && quote)
|
|
118
|
+
return `${base}/${quote}`;
|
|
119
|
+
if (base)
|
|
120
|
+
return base;
|
|
121
|
+
return normalizePoolId(row?.poolId ?? row?.pool_id);
|
|
122
|
+
}
|
|
123
|
+
function sumRaw(baseRaw, quoteRaw) {
|
|
124
|
+
return BigInt(baseRaw || "0") + BigInt(quoteRaw || "0");
|
|
125
|
+
}
|
|
126
|
+
async function readErc20Balance(provider, tokenAddress, holder) {
|
|
127
|
+
const contract = new Contract(tokenAddress, ERC20_BALANCE_ABI, provider);
|
|
128
|
+
const balance = await contract.balanceOf(holder);
|
|
129
|
+
return BigInt(balance).toString();
|
|
130
|
+
}
|
|
131
|
+
export const getMyLpHoldingsTool = {
|
|
132
|
+
name: "get_my_lp_holdings",
|
|
133
|
+
description: "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`.",
|
|
134
|
+
schema: {
|
|
135
|
+
includeZero: z.coerce.boolean().optional().describe("If true, include pools with zero LP balances (default false)."),
|
|
136
|
+
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."),
|
|
137
|
+
maxPools: z.coerce.number().int().positive().max(2000).optional().describe("Optional cap for scanned pools (default all)."),
|
|
138
|
+
},
|
|
139
|
+
handler: async (args) => {
|
|
140
|
+
try {
|
|
141
|
+
const { client, address, signer } = await resolveClient();
|
|
142
|
+
const chainId = getChainId();
|
|
143
|
+
const provider = signer?.provider;
|
|
144
|
+
if (!provider) {
|
|
145
|
+
throw new Error("Provider is unavailable for LP balance reads.");
|
|
146
|
+
}
|
|
147
|
+
const includeZero = !!args.includeZero;
|
|
148
|
+
const poolIdsFilter = normalizePoolIdsInput(args.poolIds);
|
|
149
|
+
const filterSet = new Set(poolIdsFilter.map((item) => item.toLowerCase()));
|
|
150
|
+
const maxPools = Number.isFinite(Number(args.maxPools)) ? Math.floor(Number(args.maxPools)) : null;
|
|
151
|
+
const poolListRes = await client.api.getPoolList();
|
|
152
|
+
const rows = collectRows(poolListRes?.data ?? poolListRes);
|
|
153
|
+
const deduped = new Map();
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
const poolId = normalizePoolId(row?.poolId ?? row?.pool_id);
|
|
156
|
+
if (!poolId)
|
|
157
|
+
continue;
|
|
158
|
+
if (filterSet.size > 0 && !filterSet.has(poolId.toLowerCase()))
|
|
159
|
+
continue;
|
|
160
|
+
if (!deduped.has(poolId.toLowerCase())) {
|
|
161
|
+
deduped.set(poolId.toLowerCase(), row);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const selectedRows = Array.from(deduped.values());
|
|
165
|
+
const scannedRows = maxPools ? selectedRows.slice(0, maxPools) : selectedRows;
|
|
166
|
+
const items = [];
|
|
167
|
+
const warnings = [];
|
|
168
|
+
let totalBaseLpRaw = 0n;
|
|
169
|
+
let totalQuoteLpRaw = 0n;
|
|
170
|
+
for (const row of scannedRows) {
|
|
171
|
+
const poolId = normalizePoolId(row?.poolId ?? row?.pool_id);
|
|
172
|
+
let basePoolToken = readAddress(row?.basePoolToken ?? row?.base_pool_token);
|
|
173
|
+
let quotePoolToken = readAddress(row?.quotePoolToken ?? row?.quote_pool_token);
|
|
174
|
+
let detail = null;
|
|
175
|
+
if (!basePoolToken || !quotePoolToken) {
|
|
176
|
+
try {
|
|
177
|
+
const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
|
|
178
|
+
detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
|
|
179
|
+
basePoolToken = basePoolToken ?? readAddress(detail?.basePoolToken ?? detail?.base_pool_token);
|
|
180
|
+
quotePoolToken = quotePoolToken ?? readAddress(detail?.quotePoolToken ?? detail?.quote_pool_token);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
warnings.push(`pool ${poolId}: failed to enrich pool detail (${extractErrorMessage(error)})`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const { baseSymbol, quoteSymbol } = resolveBaseQuoteSymbols(row, detail);
|
|
187
|
+
const { baseLpAssetName, quoteLpAssetName } = buildLpAssetNames(baseSymbol, quoteSymbol);
|
|
188
|
+
let baseLpRaw = "0";
|
|
189
|
+
let quoteLpRaw = "0";
|
|
190
|
+
if (basePoolToken) {
|
|
191
|
+
try {
|
|
192
|
+
baseLpRaw = await readErc20Balance(provider, basePoolToken, address);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
warnings.push(`pool ${poolId}: failed to read base LP balance (${extractErrorMessage(error)})`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (quotePoolToken) {
|
|
199
|
+
try {
|
|
200
|
+
quoteLpRaw = await readErc20Balance(provider, quotePoolToken, address);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
warnings.push(`pool ${poolId}: failed to read quote LP balance (${extractErrorMessage(error)})`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const hasAnyLp = BigInt(baseLpRaw) > 0n || BigInt(quoteLpRaw) > 0n;
|
|
207
|
+
if (!includeZero && !hasAnyLp)
|
|
208
|
+
continue;
|
|
209
|
+
totalBaseLpRaw += BigInt(baseLpRaw);
|
|
210
|
+
totalQuoteLpRaw += BigInt(quoteLpRaw);
|
|
211
|
+
items.push({
|
|
212
|
+
poolId,
|
|
213
|
+
symbol: toSymbol(row),
|
|
214
|
+
state: row?.state ?? row?.poolState ?? null,
|
|
215
|
+
baseSymbol,
|
|
216
|
+
quoteSymbol,
|
|
217
|
+
baseLpAssetName,
|
|
218
|
+
quoteLpAssetName,
|
|
219
|
+
basePoolToken: basePoolToken ?? null,
|
|
220
|
+
quotePoolToken: quotePoolToken ?? null,
|
|
221
|
+
baseLpBalanceRaw: baseLpRaw,
|
|
222
|
+
baseLpBalance: formatUnits(baseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
223
|
+
quoteLpBalanceRaw: quoteLpRaw,
|
|
224
|
+
quoteLpBalance: formatUnits(quoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
225
|
+
hasAnyLp,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
items.sort((left, right) => {
|
|
229
|
+
const leftSum = sumRaw(left.baseLpBalanceRaw, left.quoteLpBalanceRaw);
|
|
230
|
+
const rightSum = sumRaw(right.baseLpBalanceRaw, right.quoteLpBalanceRaw);
|
|
231
|
+
if (leftSum === rightSum)
|
|
232
|
+
return 0;
|
|
233
|
+
return rightSum > leftSum ? 1 : -1;
|
|
234
|
+
});
|
|
235
|
+
const payload = {
|
|
236
|
+
meta: {
|
|
237
|
+
address,
|
|
238
|
+
chainId,
|
|
239
|
+
includeZero,
|
|
240
|
+
requestedPoolIds: poolIdsFilter,
|
|
241
|
+
scannedPools: scannedRows.length,
|
|
242
|
+
totalDiscoveredPools: selectedRows.length,
|
|
243
|
+
maxPools: maxPools ?? null,
|
|
244
|
+
},
|
|
245
|
+
summary: {
|
|
246
|
+
heldPools: items.length,
|
|
247
|
+
totalBaseLpRaw: totalBaseLpRaw.toString(),
|
|
248
|
+
totalBaseLp: formatUnits(totalBaseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
249
|
+
totalQuoteLpRaw: totalQuoteLpRaw.toString(),
|
|
250
|
+
totalQuoteLp: formatUnits(totalQuoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
|
|
251
|
+
},
|
|
252
|
+
items,
|
|
253
|
+
};
|
|
254
|
+
if (warnings.length > 0) {
|
|
255
|
+
payload.warnings = warnings.slice(0, 100);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, value) => typeof value === "bigint" ? value.toString() : value, 2) }],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
};
|
package/dist/tools/index.js
CHANGED
|
@@ -31,7 +31,6 @@ export { getPositionsTool } from "./getPositions.js";
|
|
|
31
31
|
export { getOpenOrdersTool, getOrderHistoryTool } from "./orderQueries.js";
|
|
32
32
|
export { getPositionHistoryTool } from "./positionHistory.js";
|
|
33
33
|
export { getAccountTool, getTradeFlowTool } from "./accountInfo.js";
|
|
34
|
+
export { getMyLpHoldingsTool } from "./getMyLpHoldings.js";
|
|
34
35
|
export { getAccountVipInfoTool } from "./getAccountVipInfo.js";
|
|
35
36
|
export { accountDepositTool, accountWithdrawTool } from "./accountTransfer.js";
|
|
36
|
-
export { getMarketListTool } from "./getMarketList.js";
|
|
37
|
-
export { getMarketListRawTool } from "./getMarketListRaw.js";
|
|
@@ -1,14 +1,55 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
|
|
3
|
-
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
4
4
|
import { resolvePool } from "../services/marketService.js";
|
|
5
5
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
6
6
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
7
|
+
function normalizeAssetSymbol(value) {
|
|
8
|
+
const text = String(value ?? "").trim();
|
|
9
|
+
if (!text)
|
|
10
|
+
return "";
|
|
11
|
+
return text.replace(/\s+/g, "").replace(/\//g, "").toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
function parsePairSymbols(value) {
|
|
14
|
+
const text = String(value ?? "").trim();
|
|
15
|
+
if (!text)
|
|
16
|
+
return null;
|
|
17
|
+
const parts = text
|
|
18
|
+
.split(/[\/:_-]/)
|
|
19
|
+
.map((item) => normalizeAssetSymbol(item))
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
if (parts.length >= 2) {
|
|
22
|
+
return { base: parts[0], quote: parts[1] };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function resolveLpAssetNames(detail) {
|
|
27
|
+
let baseSymbol = normalizeAssetSymbol(detail?.baseSymbol ?? detail?.base_symbol);
|
|
28
|
+
let quoteSymbol = normalizeAssetSymbol(detail?.quoteSymbol ?? detail?.quote_symbol);
|
|
29
|
+
if (!baseSymbol || !quoteSymbol) {
|
|
30
|
+
const pairCandidate = detail?.baseQuoteSymbol ?? detail?.symbol ?? detail?.symbolName;
|
|
31
|
+
const parsed = parsePairSymbols(pairCandidate);
|
|
32
|
+
if (parsed) {
|
|
33
|
+
baseSymbol = baseSymbol || parsed.base;
|
|
34
|
+
quoteSymbol = quoteSymbol || parsed.quote;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const normalizedBase = baseSymbol || null;
|
|
38
|
+
const normalizedQuote = quoteSymbol || null;
|
|
39
|
+
const baseLpAssetName = normalizedBase && normalizedQuote ? `m${normalizedBase}.${normalizedQuote}` : null;
|
|
40
|
+
const quoteLpAssetName = normalizedBase && normalizedQuote ? `m${normalizedQuote}.${normalizedBase}` : null;
|
|
41
|
+
return {
|
|
42
|
+
baseSymbol: normalizedBase,
|
|
43
|
+
quoteSymbol: normalizedQuote,
|
|
44
|
+
baseLpAssetName,
|
|
45
|
+
quoteLpAssetName,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
7
48
|
export const manageLiquidityTool = {
|
|
8
49
|
name: "manage_liquidity",
|
|
9
|
-
description: "Add or withdraw liquidity from a BASE or QUOTE pool.",
|
|
50
|
+
description: "Add or withdraw liquidity from a BASE or QUOTE pool. Success response includes LP naming metadata: base `mBASE.QUOTE`, quote `mQUOTE.BASE`, plus `operatedLpAssetName` based on poolType.",
|
|
10
51
|
schema: {
|
|
11
|
-
action: z.
|
|
52
|
+
action: z.coerce.string().describe("'deposit' or 'withdraw' (aliases: add/remove/increase/decrease; case-insensitive)"),
|
|
12
53
|
poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
|
|
13
54
|
poolId: z.string().describe("Pool ID or Base Token Address"),
|
|
14
55
|
amount: z.coerce.number().positive().describe("Amount in human-readable units"),
|
|
@@ -20,23 +61,36 @@ export const manageLiquidityTool = {
|
|
|
20
61
|
const { client, signer } = await resolveClient();
|
|
21
62
|
let { action, poolType, poolId } = args;
|
|
22
63
|
const { amount, slippage } = args;
|
|
64
|
+
const chainId = args.chainId ?? getChainId();
|
|
65
|
+
action = String(action ?? "").trim().toLowerCase();
|
|
66
|
+
const validActions = new Set(["deposit", "withdraw", "add", "remove", "increase", "decrease"]);
|
|
67
|
+
if (!validActions.has(action)) {
|
|
68
|
+
throw new Error(`Invalid action: ${args.action}. Use deposit/withdraw or aliases add/remove/increase/decrease.`);
|
|
69
|
+
}
|
|
23
70
|
// 1. Action Alias Mapping
|
|
24
71
|
if (action === "add" || action === "increase")
|
|
25
72
|
action = "deposit";
|
|
26
73
|
if (action === "remove" || action === "decrease")
|
|
27
74
|
action = "withdraw";
|
|
28
75
|
// 2. Smart Pool Resolution (Handles PoolId, Token Address, or Keywords)
|
|
29
|
-
poolId = await resolvePool(client, poolId);
|
|
76
|
+
poolId = await resolvePool(client, poolId, undefined, chainId);
|
|
77
|
+
// 3. Preflight pool validation for target chain (avoid opaque SDK "Invalid Params")
|
|
78
|
+
const detailRes = await client.markets.getMarketDetail({ chainId, poolId }).catch(() => null);
|
|
79
|
+
const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
|
|
80
|
+
if (!detail?.marketId) {
|
|
81
|
+
throw new Error(`Pool ${poolId} not found on chainId ${chainId}. ` +
|
|
82
|
+
`Please query a valid active pool via search_market/get_pool_list first.`);
|
|
83
|
+
}
|
|
30
84
|
let raw;
|
|
31
85
|
if (poolType === "QUOTE") {
|
|
32
86
|
raw = action === "deposit"
|
|
33
|
-
? await quoteDeposit(poolId, amount, slippage,
|
|
34
|
-
: await quoteWithdraw(poolId, amount, slippage,
|
|
87
|
+
? await quoteDeposit(poolId, amount, slippage, chainId)
|
|
88
|
+
: await quoteWithdraw(poolId, amount, slippage, chainId);
|
|
35
89
|
}
|
|
36
90
|
else {
|
|
37
91
|
raw = action === "deposit"
|
|
38
|
-
? await baseDeposit(poolId, amount, slippage,
|
|
39
|
-
: await baseWithdraw(poolId, amount, slippage,
|
|
92
|
+
? await baseDeposit(poolId, amount, slippage, chainId)
|
|
93
|
+
: await baseWithdraw(poolId, amount, slippage, chainId);
|
|
40
94
|
}
|
|
41
95
|
if (!raw) {
|
|
42
96
|
throw new Error(`SDK returned an empty result for liquidity ${action}. This usually occurs if the pool is not in an Active state (state: 2) or if there is a contract-level restriction. Please check pool_info.`);
|
|
@@ -45,7 +99,26 @@ export const manageLiquidityTool = {
|
|
|
45
99
|
throw new Error(`Liquidity ${action} failed: ${extractErrorMessage(raw)}`);
|
|
46
100
|
}
|
|
47
101
|
const data = await finalizeMutationResult(raw, signer, "manage_liquidity");
|
|
48
|
-
|
|
102
|
+
const lpAssetNames = resolveLpAssetNames(detail);
|
|
103
|
+
const operatedLpAssetName = poolType === "BASE" ? lpAssetNames.baseLpAssetName : lpAssetNames.quoteLpAssetName;
|
|
104
|
+
const payload = data && typeof data === "object" && !Array.isArray(data)
|
|
105
|
+
? {
|
|
106
|
+
...data,
|
|
107
|
+
lpAssetNames: {
|
|
108
|
+
...lpAssetNames,
|
|
109
|
+
operatedPoolType: poolType,
|
|
110
|
+
operatedLpAssetName,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
: {
|
|
114
|
+
result: data,
|
|
115
|
+
lpAssetNames: {
|
|
116
|
+
...lpAssetNames,
|
|
117
|
+
operatedPoolType: poolType,
|
|
118
|
+
operatedLpAssetName,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
49
122
|
}
|
|
50
123
|
catch (error) {
|
|
51
124
|
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
package/dist/tools/marketInfo.js
CHANGED
|
@@ -3,6 +3,21 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
|
3
3
|
import { getMarketDetail, resolvePool } from "../services/marketService.js";
|
|
4
4
|
import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
|
|
5
5
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
6
|
+
import { parseUserPrice30 } from "../utils/units.js";
|
|
7
|
+
const INTEGER_RE = /^\d+$/;
|
|
8
|
+
const DECIMAL_RE = /^\d+(\.\d+)?$/;
|
|
9
|
+
function normalizeMarketPrice30Input(value) {
|
|
10
|
+
const text = String(value ?? "").trim();
|
|
11
|
+
if (!text)
|
|
12
|
+
throw new Error("marketPrice is required.");
|
|
13
|
+
// Keep backward compatibility: plain integers are treated as already-raw 30-dec units.
|
|
14
|
+
if (INTEGER_RE.test(text))
|
|
15
|
+
return text;
|
|
16
|
+
if (!DECIMAL_RE.test(text) && !/^raw:/i.test(text) && !/^human:/i.test(text)) {
|
|
17
|
+
throw new Error("marketPrice must be numeric, or prefixed with raw:/human:.");
|
|
18
|
+
}
|
|
19
|
+
return parseUserPrice30(text, "marketPrice");
|
|
20
|
+
}
|
|
6
21
|
export const getMarketDetailTool = {
|
|
7
22
|
name: "get_market_detail",
|
|
8
23
|
description: "Get detailed information for a specific trading pool (fee rates, open interest, etc.). Supports PoolId, Token Address, or Keywords.",
|
|
@@ -52,7 +67,7 @@ export const getLiquidityInfoTool = {
|
|
|
52
67
|
description: "Get pool liquidity utilization and depth information. Supports PoolId, Token Address, or Keywords.",
|
|
53
68
|
schema: {
|
|
54
69
|
poolId: z.string().describe("Pool ID, Token Address, or Keyword"),
|
|
55
|
-
marketPrice: z.string().
|
|
70
|
+
marketPrice: z.union([z.string(), z.number()]).describe("Current market price. Supports raw 30-dec integer, raw:..., or human price like 2172.5 / human:2172.5."),
|
|
56
71
|
chainId: z.number().int().positive().optional().describe("Optional chainId override"),
|
|
57
72
|
},
|
|
58
73
|
handler: async (args) => {
|
|
@@ -60,7 +75,8 @@ export const getLiquidityInfoTool = {
|
|
|
60
75
|
const { client } = await resolveClient();
|
|
61
76
|
const poolId = await resolvePool(client, args.poolId);
|
|
62
77
|
const chainId = args.chainId ?? getChainId();
|
|
63
|
-
const
|
|
78
|
+
const marketPrice = normalizeMarketPrice30Input(args.marketPrice);
|
|
79
|
+
const data = await getLiquidityInfo(client, poolId, marketPrice, chainId);
|
|
64
80
|
if (!data)
|
|
65
81
|
throw new Error(`Liquidity info for ${poolId} not available.`);
|
|
66
82
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
package/dist/utils/slippage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const SLIPPAGE_PCT_4DP_RE = /^\d+$/;
|
|
2
2
|
export const SLIPPAGE_PCT_4DP_MAX = 10000n;
|
|
3
3
|
export const SLIPPAGE_PCT_4DP_DESC = "Slippage in 4-decimal precision raw units (1 = 0.01%, 10000 = 100%)";
|
|
4
|
+
const SLIPPAGE_PERCENT_HUMAN_RE = /^\d+(\.\d{1,2})?$/;
|
|
4
5
|
export function isValidSlippagePct4dp(value) {
|
|
5
6
|
if (!SLIPPAGE_PCT_4DP_RE.test(value))
|
|
6
7
|
return false;
|
|
@@ -13,3 +14,25 @@ export function normalizeSlippagePct4dp(value, label = "slippagePct") {
|
|
|
13
14
|
}
|
|
14
15
|
return raw;
|
|
15
16
|
}
|
|
17
|
+
export function normalizeSlippagePct4dpFlexible(value, label = "slippagePct") {
|
|
18
|
+
const raw = String(value ?? "").trim();
|
|
19
|
+
if (!raw) {
|
|
20
|
+
throw new Error(`${label} is required.`);
|
|
21
|
+
}
|
|
22
|
+
// Keep backward-compatible behavior: integer values remain raw 4dp units.
|
|
23
|
+
if (isValidSlippagePct4dp(raw)) {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
// Human percent helper: "1.0" / "1.25" / "1%"
|
|
27
|
+
const percentText = raw.endsWith("%") ? raw.slice(0, -1).trim() : raw;
|
|
28
|
+
if (!SLIPPAGE_PERCENT_HUMAN_RE.test(percentText)) {
|
|
29
|
+
throw new Error(`${label} must be raw 4dp integer (e.g. 100=1%) or human percent like "1.0" / "1%".`);
|
|
30
|
+
}
|
|
31
|
+
const [intPart, fracPart = ""] = percentText.split(".");
|
|
32
|
+
const frac2 = (fracPart + "00").slice(0, 2);
|
|
33
|
+
const converted = BigInt(intPart) * 100n + BigInt(frac2);
|
|
34
|
+
if (converted > SLIPPAGE_PCT_4DP_MAX) {
|
|
35
|
+
throw new Error(`${label} must be <= 100% (raw <= 10000).`);
|
|
36
|
+
}
|
|
37
|
+
return converted.toString();
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { resolveClient } from "../auth/resolveClient.js";
|
|
2
|
-
import { searchMarket } from "../services/marketService.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
export const getMarketListTool = {
|
|
5
|
-
name: "get_market_list",
|
|
6
|
-
description: "Get tradable markets/pools (state=2 Active). Supports configurable result limit; backend may still enforce its own cap.",
|
|
7
|
-
schema: {
|
|
8
|
-
limit: z.coerce.number().int().positive().optional().describe("Max results to request (default 1000)."),
|
|
9
|
-
},
|
|
10
|
-
handler: async (args) => {
|
|
11
|
-
try {
|
|
12
|
-
const { client } = await resolveClient();
|
|
13
|
-
const activeMarkets = await searchMarket(client, "", args.limit ?? 1000);
|
|
14
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: activeMarkets }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { resolveClient } from "../auth/resolveClient.js";
|
|
2
|
-
export const getMarketListRawTool = {
|
|
3
|
-
name: "get_market_list_raw",
|
|
4
|
-
description: "Get raw market list directly from MYX API without MCP-side filtering.",
|
|
5
|
-
schema: {},
|
|
6
|
-
handler: async () => {
|
|
7
|
-
try {
|
|
8
|
-
const { client } = await resolveClient();
|
|
9
|
-
const result = await client.api.getMarketList();
|
|
10
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
};
|