@michaleffffff/mcp-trading-server 3.0.15 → 3.0.20
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 +38 -0
- package/README.md +3 -1
- package/TOOL_EXAMPLES.md +7 -1
- package/dist/server.js +3 -3
- package/dist/tools/executeTrade.js +6 -0
- package/dist/tools/getPoolMetadata.js +181 -0
- package/dist/tools/manageLiquidity.js +29 -2
- package/dist/tools/openPositionSimple.js +36 -13
- package/dist/utils/address.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.20 - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Hardened `get_pool_metadata` funding-rate formatting:
|
|
7
|
+
- `fundingInfo.nextFundingRate` now preserves readable `%/秒` and `%/天` output for negative rates too.
|
|
8
|
+
- Regression coverage now asserts both numeric funding-rate views and display strings.
|
|
9
|
+
|
|
10
|
+
## 3.0.19 - 2026-03-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Refined `fundingInfo.nextFundingRate` formatting in `get_pool_metadata`:
|
|
14
|
+
- Displays percent per second as `%/秒`
|
|
15
|
+
- Displays derived percent per day as `%/天`
|
|
16
|
+
- Keeps raw integer and comma-separated views for audit/debug use
|
|
17
|
+
|
|
18
|
+
## 3.0.18 - 2026-03-19
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Further improved `fundingInfo` / `ioTracker` readability in `get_pool_metadata`:
|
|
22
|
+
- `fundingInfo.nextEpochTime` now includes UTC timestamp and seconds-until-next-epoch.
|
|
23
|
+
- `fundingInfo.nextFundingRate` and `lastFundingFeeTracker` now include comma-separated raw views.
|
|
24
|
+
- `ioTracker` now includes derived notional-at-entry views based on `poolEntryPrice`.
|
|
25
|
+
|
|
26
|
+
## 3.0.17 - 2026-03-19
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Improved precision handling for liquidity and pool read tools:
|
|
30
|
+
- `get_pool_metadata` now adds `poolInfoFormatted` for exchange rate, LP token price, LP supply, debt, collateral, reserves, and open interest.
|
|
31
|
+
- `get_lp_price` now returns both raw and human-readable formatted values.
|
|
32
|
+
- Added regression coverage to ensure formatted precision fields are present.
|
|
33
|
+
|
|
34
|
+
## 3.0.16 - 2026-03-18
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Improved MCP-side validation UX:
|
|
38
|
+
- `executionFeeToken` now fails early with a clear `INVALID_PARAM` when callers pass the zero address, and points users to the real pool `quoteToken`.
|
|
39
|
+
- `open_position_simple` no longer returns a generic numeric parse error when `collateralAmount` is omitted; it now explains that `collateralAmount` is still required and suggests an approximate value from `size`, `price`, and `leverage`.
|
|
40
|
+
|
|
3
41
|
## 3.0.15 - 2026-03-18
|
|
4
42
|
|
|
5
43
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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.
|
|
9
|
+
- **Current release: 3.0.20**
|
|
10
10
|
- **SDK baseline**: `@myx-trade/sdk@^1.0.2` compatibility completed.
|
|
11
11
|
- **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
|
|
12
12
|
- **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
|
|
@@ -20,6 +20,7 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
|
|
|
20
20
|
* **AI-First Design**: Automated Pool ID resolution and flexible unit handling (`human:` vs `raw:`).
|
|
21
21
|
* **Deep Liquidity Support**: Tools for both traders and liquidity providers.
|
|
22
22
|
* **Production Ready**: Robust error handling with actionable hints for LLMs.
|
|
23
|
+
* **Precision-Aware Reads**: Pool and LP read tools expose human-readable formatted values alongside raw on-chain integers.
|
|
23
24
|
* **Compliant**: Full Model Context Protocol (MCP) support.
|
|
24
25
|
|
|
25
26
|
---
|
|
@@ -109,6 +110,7 @@ Use these conventions when generating tool arguments:
|
|
|
109
110
|
- `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
|
|
110
111
|
- `timeInForce`: SDK `v1.0.2` currently supports `IOC` only, so use `0` or `"IOC"`
|
|
111
112
|
- `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
|
|
113
|
+
- `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
|
|
112
114
|
- Human units: `"100"` means 100 USDC or 100 token units depending on field
|
|
113
115
|
- Raw units: `"raw:1000000"` means exact on-chain integer units
|
|
114
116
|
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# MYX MCP Tool Examples Handbook (v3.0.
|
|
1
|
+
# MYX MCP Tool Examples Handbook (v3.0.20)
|
|
2
2
|
|
|
3
3
|
This guide provides practical MCP payload examples for the current unified toolset.
|
|
4
4
|
All examples use the MCP format:
|
|
@@ -74,6 +74,7 @@ Recommended high-level entry tool.
|
|
|
74
74
|
|
|
75
75
|
`marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
|
|
76
76
|
`size` is always the base-asset quantity, not the USD notional. For example, a 500 USD order at price 1200 implies `size ≈ 0.416666...`.
|
|
77
|
+
`collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
|
|
77
78
|
|
|
78
79
|
Raw-units example:
|
|
79
80
|
|
|
@@ -111,6 +112,7 @@ Low-level increase-order tool when you want full control.
|
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
`timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.2`.
|
|
115
|
+
`executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
|
|
114
116
|
|
|
115
117
|
### `close_position`
|
|
116
118
|
Close or reduce a position. Use `ALL` for a full close.
|
|
@@ -231,6 +233,8 @@ Unified pool detail, config, and liquidity info.
|
|
|
231
233
|
}
|
|
232
234
|
```
|
|
233
235
|
|
|
236
|
+
`get_pool_metadata` returns raw values in `poolInfo` and precision-safe human-readable values in `poolInfoFormatted`, including readable funding epoch timestamps, funding-rate `%/秒` and `%/天`, and IO notional-at-entry views.
|
|
237
|
+
|
|
234
238
|
### `get_kline`
|
|
235
239
|
Read chart data. Use `limit: 1` for the latest bar.
|
|
236
240
|
|
|
@@ -293,6 +297,8 @@ Read LP NAV price for BASE or QUOTE side.
|
|
|
293
297
|
}
|
|
294
298
|
```
|
|
295
299
|
|
|
300
|
+
`get_lp_price` returns both `raw` and `formatted` NAV price values.
|
|
301
|
+
|
|
296
302
|
### `get_my_lp_holdings`
|
|
297
303
|
Read current LP balances across pools.
|
|
298
304
|
|
package/dist/server.js
CHANGED
|
@@ -39,7 +39,7 @@ function safeJsonStringify(value) {
|
|
|
39
39
|
}
|
|
40
40
|
function inferToolErrorCode(message) {
|
|
41
41
|
const lower = message.toLowerCase();
|
|
42
|
-
if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized")) {
|
|
42
|
+
if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized") || lower.includes("zero address")) {
|
|
43
43
|
return "INVALID_PARAM";
|
|
44
44
|
}
|
|
45
45
|
if (lower.includes("insufficient") && (lower.includes("allowance") || lower.includes("approval"))) {
|
|
@@ -461,7 +461,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
461
461
|
};
|
|
462
462
|
}
|
|
463
463
|
// ─── MCP Server ───
|
|
464
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.
|
|
464
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.20" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
465
465
|
// List tools
|
|
466
466
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
467
467
|
return {
|
|
@@ -582,7 +582,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
582
582
|
async function main() {
|
|
583
583
|
const transport = new StdioServerTransport();
|
|
584
584
|
await server.connect(transport);
|
|
585
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
585
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.20 running (stdio, pure on-chain, prod ready)");
|
|
586
586
|
}
|
|
587
587
|
main().catch((err) => {
|
|
588
588
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -7,6 +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
11
|
const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
|
|
11
12
|
const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
|
|
12
13
|
export const executeTradeTool = {
|
|
@@ -61,6 +62,11 @@ export const executeTradeTool = {
|
|
|
61
62
|
const mappedOrderType = mapOrderType(args.orderType);
|
|
62
63
|
const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
|
|
63
64
|
const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
|
|
65
|
+
if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
|
|
66
|
+
if (isZeroAddress(args.executionFeeToken)) {
|
|
67
|
+
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${poolData.quoteToken}.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
64
70
|
const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
65
71
|
const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
|
|
66
72
|
const priceRaw = parseUserUnits(args.price, 30, "price");
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatUnits } from "ethers";
|
|
2
3
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
4
|
import { getMarketDetail, resolvePool } from "../services/marketService.js";
|
|
4
5
|
import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
|
|
5
6
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
6
7
|
import { parseUserPrice30 } from "../utils/units.js";
|
|
7
8
|
const INTEGER_RE = /^\d+$/;
|
|
9
|
+
const SIGNED_INTEGER_RE = /^-?\d+$/;
|
|
8
10
|
const DECIMAL_RE = /^\d+(\.\d+)?$/;
|
|
9
11
|
function normalizeMarketPrice30Input(value) {
|
|
10
12
|
const text = String(value ?? "").trim();
|
|
@@ -29,6 +31,180 @@ function compactWarning(scope, err) {
|
|
|
29
31
|
}
|
|
30
32
|
return `${scope}: ${flat}`;
|
|
31
33
|
}
|
|
34
|
+
function formatRawValue(value, decimals) {
|
|
35
|
+
const text = String(value ?? "").trim();
|
|
36
|
+
if (!INTEGER_RE.test(text))
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
return formatUnits(text, decimals);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function buildFormattedAmount(value, decimals, symbol) {
|
|
46
|
+
const raw = String(value ?? "");
|
|
47
|
+
const formatted = formatRawValue(value, decimals);
|
|
48
|
+
return {
|
|
49
|
+
raw,
|
|
50
|
+
decimals,
|
|
51
|
+
formatted,
|
|
52
|
+
display: formatted && symbol ? `${formatted} ${symbol}` : formatted,
|
|
53
|
+
symbol: symbol ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function buildFormattedPrice30(value, quoteSymbol) {
|
|
57
|
+
const raw = String(value ?? "");
|
|
58
|
+
const formatted = formatRawValue(value, 30);
|
|
59
|
+
return {
|
|
60
|
+
raw,
|
|
61
|
+
decimals: 30,
|
|
62
|
+
formatted,
|
|
63
|
+
display: formatted && quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
|
|
64
|
+
symbol: quoteSymbol ?? null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function buildFormattedRatio18(value) {
|
|
68
|
+
const raw = String(value ?? "");
|
|
69
|
+
const formatted = formatRawValue(value, 18);
|
|
70
|
+
return {
|
|
71
|
+
raw,
|
|
72
|
+
decimals: 18,
|
|
73
|
+
formatted,
|
|
74
|
+
display: formatted,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function buildRawScalar(value) {
|
|
78
|
+
const raw = String(value ?? "").trim();
|
|
79
|
+
if (!SIGNED_INTEGER_RE.test(raw)) {
|
|
80
|
+
return { raw, withCommas: raw || null };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return {
|
|
84
|
+
raw,
|
|
85
|
+
withCommas: BigInt(raw).toLocaleString("en-US"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { raw, withCommas: raw };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function formatScaledIntegerString(raw, decimals) {
|
|
93
|
+
if (!SIGNED_INTEGER_RE.test(raw))
|
|
94
|
+
return null;
|
|
95
|
+
const negative = raw.startsWith("-");
|
|
96
|
+
const digits = negative ? raw.slice(1) : raw;
|
|
97
|
+
const padded = digits.padStart(decimals + 1, "0");
|
|
98
|
+
const intPart = padded.slice(0, -decimals) || "0";
|
|
99
|
+
const fracPart = padded.slice(-decimals).replace(/0+$/, "");
|
|
100
|
+
const normalized = fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
101
|
+
return negative ? `-${normalized}` : normalized;
|
|
102
|
+
}
|
|
103
|
+
function buildFundingRateInfo(value) {
|
|
104
|
+
const base = buildRawScalar(value);
|
|
105
|
+
const raw = String(value ?? "").trim();
|
|
106
|
+
if (!SIGNED_INTEGER_RE.test(raw))
|
|
107
|
+
return base;
|
|
108
|
+
try {
|
|
109
|
+
const rawBig = BigInt(raw);
|
|
110
|
+
const perDayRaw = rawBig * 86400n;
|
|
111
|
+
const percentPerSecond = formatScaledIntegerString(raw, 12);
|
|
112
|
+
const percentPerDay = formatScaledIntegerString(perDayRaw.toString(), 12);
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
scale: "1e12 => percent",
|
|
116
|
+
percentPerSecond,
|
|
117
|
+
displayPerSecond: percentPerSecond ? `${percentPerSecond}%/秒` : null,
|
|
118
|
+
percentPerDay,
|
|
119
|
+
displayPerDay: percentPerDay ? `${percentPerDay}%/天` : null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return base;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function buildTimestampInfo(value) {
|
|
127
|
+
const raw = String(value ?? "").trim();
|
|
128
|
+
const base = buildRawScalar(value);
|
|
129
|
+
if (!INTEGER_RE.test(raw)) {
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
const seconds = Number(raw);
|
|
133
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
134
|
+
return base;
|
|
135
|
+
}
|
|
136
|
+
const isoUtc = new Date(seconds * 1000).toISOString();
|
|
137
|
+
const secondsUntil = seconds - Math.floor(Date.now() / 1000);
|
|
138
|
+
return {
|
|
139
|
+
...base,
|
|
140
|
+
isoUtc,
|
|
141
|
+
secondsUntil,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function computeQuoteNotionalDisplay(baseRaw, price30Raw, baseDecimals, quoteDecimals, quoteSymbol) {
|
|
145
|
+
const baseText = String(baseRaw ?? "").trim();
|
|
146
|
+
const priceText = String(price30Raw ?? "").trim();
|
|
147
|
+
if (!INTEGER_RE.test(baseText) || !INTEGER_RE.test(priceText))
|
|
148
|
+
return null;
|
|
149
|
+
try {
|
|
150
|
+
const notionalRaw = (BigInt(baseText) * BigInt(priceText) * (10n ** BigInt(quoteDecimals))) /
|
|
151
|
+
(10n ** BigInt(baseDecimals + 30));
|
|
152
|
+
return buildFormattedAmount(notionalRaw.toString(), quoteDecimals, quoteSymbol);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function formatPoolInfoSnapshot(poolInfo, marketDetail) {
|
|
159
|
+
if (!poolInfo || typeof poolInfo !== "object")
|
|
160
|
+
return null;
|
|
161
|
+
const baseSymbol = String(marketDetail?.baseSymbol ?? "BASE");
|
|
162
|
+
const quoteSymbol = String(marketDetail?.quoteSymbol ?? "QUOTE");
|
|
163
|
+
const baseDecimals = Number(marketDetail?.baseDecimals ?? 18);
|
|
164
|
+
const quoteDecimals = Number(marketDetail?.quoteDecimals ?? 6);
|
|
165
|
+
return {
|
|
166
|
+
quotePool: poolInfo.quotePool ? {
|
|
167
|
+
poolToken: poolInfo.quotePool.poolToken ?? null,
|
|
168
|
+
exchangeRate: buildFormattedRatio18(poolInfo.quotePool.exchangeRate),
|
|
169
|
+
poolTokenPrice: buildFormattedPrice30(poolInfo.quotePool.poolTokenPrice, quoteSymbol),
|
|
170
|
+
poolTokenSupply: buildFormattedAmount(poolInfo.quotePool.poolTokenSupply, 18, `m${quoteSymbol}.${baseSymbol}`),
|
|
171
|
+
totalDebt: buildFormattedAmount(poolInfo.quotePool.totalDebt, quoteDecimals, quoteSymbol),
|
|
172
|
+
baseCollateral: buildFormattedAmount(poolInfo.quotePool.baseCollateral, baseDecimals, baseSymbol),
|
|
173
|
+
} : null,
|
|
174
|
+
basePool: poolInfo.basePool ? {
|
|
175
|
+
poolToken: poolInfo.basePool.poolToken ?? null,
|
|
176
|
+
exchangeRate: buildFormattedRatio18(poolInfo.basePool.exchangeRate),
|
|
177
|
+
poolTokenPrice: buildFormattedPrice30(poolInfo.basePool.poolTokenPrice, quoteSymbol),
|
|
178
|
+
poolTokenSupply: buildFormattedAmount(poolInfo.basePool.poolTokenSupply, 18, `m${baseSymbol}.${quoteSymbol}`),
|
|
179
|
+
totalDebt: buildFormattedAmount(poolInfo.basePool.totalDebt, quoteDecimals, quoteSymbol),
|
|
180
|
+
baseCollateral: buildFormattedAmount(poolInfo.basePool.baseCollateral, baseDecimals, baseSymbol),
|
|
181
|
+
} : null,
|
|
182
|
+
reserveInfo: poolInfo.reserveInfo ? {
|
|
183
|
+
baseTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.baseTotalAmount, baseDecimals, baseSymbol),
|
|
184
|
+
baseReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.baseReservedAmount, baseDecimals, baseSymbol),
|
|
185
|
+
quoteTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteTotalAmount, quoteDecimals, quoteSymbol),
|
|
186
|
+
quoteReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteReservedAmount, quoteDecimals, quoteSymbol),
|
|
187
|
+
} : null,
|
|
188
|
+
fundingInfo: poolInfo.fundingInfo ? {
|
|
189
|
+
nextFundingRate: buildFundingRateInfo(poolInfo.fundingInfo.nextFundingRate),
|
|
190
|
+
lastFundingFeeTracker: buildRawScalar(poolInfo.fundingInfo.lastFundingFeeTracker),
|
|
191
|
+
nextEpochTime: buildTimestampInfo(poolInfo.fundingInfo.nextEpochTime),
|
|
192
|
+
} : null,
|
|
193
|
+
ioTracker: poolInfo.ioTracker ? {
|
|
194
|
+
tracker: buildFormattedAmount(poolInfo.ioTracker.tracker, baseDecimals, baseSymbol),
|
|
195
|
+
longSize: buildFormattedAmount(poolInfo.ioTracker.longSize, baseDecimals, baseSymbol),
|
|
196
|
+
shortSize: buildFormattedAmount(poolInfo.ioTracker.shortSize, baseDecimals, baseSymbol),
|
|
197
|
+
poolEntryPrice: buildFormattedPrice30(poolInfo.ioTracker.poolEntryPrice, quoteSymbol),
|
|
198
|
+
trackerNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.tracker, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
|
|
199
|
+
longNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.longSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
|
|
200
|
+
shortNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.shortSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
|
|
201
|
+
} : null,
|
|
202
|
+
liquidityInfo: poolInfo.liquidityInfo ? {
|
|
203
|
+
windowCaps: buildFormattedAmount(poolInfo.liquidityInfo.windowCaps, quoteDecimals, quoteSymbol),
|
|
204
|
+
openInterest: buildFormattedAmount(poolInfo.liquidityInfo.openInterest, 18, quoteSymbol),
|
|
205
|
+
} : null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
32
208
|
export const getPoolMetadataTool = {
|
|
33
209
|
name: "get_pool_metadata",
|
|
34
210
|
description: "[MARKET] Get comprehensive metadata for a pool (market detail, on-chain info, liquidity, and limits).",
|
|
@@ -57,6 +233,11 @@ export const getPoolMetadataTool = {
|
|
|
57
233
|
// 2. Pool Info (Reserves, Utilization)
|
|
58
234
|
try {
|
|
59
235
|
results.poolInfo = await getPoolInfo(poolId, chainId, client);
|
|
236
|
+
const rawMarketDetail = results.marketDetail?.data ?? results.marketDetail;
|
|
237
|
+
const formatted = formatPoolInfoSnapshot(results.poolInfo, rawMarketDetail);
|
|
238
|
+
if (formatted) {
|
|
239
|
+
results.poolInfoFormatted = formatted;
|
|
240
|
+
}
|
|
60
241
|
}
|
|
61
242
|
catch (err) {
|
|
62
243
|
errors.push(compactWarning("poolInfo", err));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatUnits } from "ethers";
|
|
2
3
|
import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
|
|
3
4
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
4
5
|
import { resolvePool } from "../services/marketService.js";
|
|
@@ -45,6 +46,20 @@ function resolveLpAssetNames(detail) {
|
|
|
45
46
|
quoteLpAssetName,
|
|
46
47
|
};
|
|
47
48
|
}
|
|
49
|
+
function formatLpPricePayload(value, quoteSymbol) {
|
|
50
|
+
const raw = String(value ?? "").trim();
|
|
51
|
+
if (!/^\d+$/.test(raw)) {
|
|
52
|
+
return { raw, formatted: null, decimals: 30, symbol: quoteSymbol ?? null };
|
|
53
|
+
}
|
|
54
|
+
const formatted = formatUnits(raw, 30);
|
|
55
|
+
return {
|
|
56
|
+
raw,
|
|
57
|
+
formatted,
|
|
58
|
+
decimals: 30,
|
|
59
|
+
symbol: quoteSymbol ?? null,
|
|
60
|
+
display: quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
48
63
|
export const manageLiquidityTool = {
|
|
49
64
|
name: "manage_liquidity",
|
|
50
65
|
description: "[LIQUIDITY] 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.",
|
|
@@ -135,8 +150,20 @@ export const getLpPriceTool = {
|
|
|
135
150
|
},
|
|
136
151
|
handler: async (args) => {
|
|
137
152
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
153
|
+
const { client } = await resolveClient();
|
|
154
|
+
const chainId = args.chainId ?? getChainId();
|
|
155
|
+
const poolId = await resolvePool(client, args.poolId, undefined, chainId);
|
|
156
|
+
const detailRes = await client.markets.getMarketDetail({ chainId, poolId }).catch(() => null);
|
|
157
|
+
const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
|
|
158
|
+
const quoteSymbol = String(detail?.quoteSymbol ?? "").trim() || null;
|
|
159
|
+
const rawPrice = await getLpPrice(args.poolType, poolId, chainId);
|
|
160
|
+
const payload = {
|
|
161
|
+
raw: String(rawPrice ?? ""),
|
|
162
|
+
formatted: formatLpPricePayload(rawPrice, quoteSymbol ?? undefined),
|
|
163
|
+
poolType: args.poolType,
|
|
164
|
+
poolId,
|
|
165
|
+
};
|
|
166
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
140
167
|
}
|
|
141
168
|
catch (error) {
|
|
142
169
|
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { OrderType } from "@myx-trade/sdk";
|
|
3
|
+
import { formatUnits } from "ethers";
|
|
3
4
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
4
5
|
import { resolvePool } from "../services/marketService.js";
|
|
5
6
|
import { openPosition } from "../services/tradeService.js";
|
|
6
|
-
import { normalizeAddress } from "../utils/address.js";
|
|
7
|
+
import { isZeroAddress, normalizeAddress } from "../utils/address.js";
|
|
7
8
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
8
9
|
import { mapDirection, mapOrderType } from "../utils/mappings.js";
|
|
9
10
|
import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
@@ -43,6 +44,7 @@ export const openPositionSimpleTool = {
|
|
|
43
44
|
direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
|
|
44
45
|
collateralAmount: z.coerce
|
|
45
46
|
.string()
|
|
47
|
+
.optional()
|
|
46
48
|
.describe("Collateral. e.g. '100' (quoted in USDC) or 'raw:100000000'."),
|
|
47
49
|
leverage: z.coerce.number().int().positive().describe("Leverage (integer, e.g. 5, 10)."),
|
|
48
50
|
orderType: z.union([z.string(), z.number()]).optional().describe("MARKET, LIMIT, STOP (default MARKET). Strings allowed."),
|
|
@@ -113,21 +115,14 @@ export const openPositionSimpleTool = {
|
|
|
113
115
|
const defaultAssetClass = Number(poolLevelConfig?.levelConfig?.assetClass ?? 0);
|
|
114
116
|
// 3) Parse & validate primary inputs
|
|
115
117
|
const dir = mapDirection(args.direction);
|
|
116
|
-
const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
117
|
-
const collateralRawBig = asBigint(collateralRaw, "collateralAmount");
|
|
118
|
-
if (collateralRawBig <= 0n)
|
|
119
|
-
throw new Error("collateralAmount must be > 0.");
|
|
120
|
-
const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
|
|
121
|
-
if (maxTradeAmountHuman) {
|
|
122
|
-
const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
|
|
123
|
-
const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
|
|
124
|
-
if (collateralRawBig > maxTradeRawBig) {
|
|
125
|
-
throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
118
|
const orderType = mapOrderType(args.orderType ?? 0);
|
|
129
119
|
const postOnly = Boolean(args.postOnly ?? false);
|
|
130
120
|
const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
|
|
121
|
+
if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
|
|
122
|
+
if (isZeroAddress(args.executionFeeToken)) {
|
|
123
|
+
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteToken}.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
131
126
|
const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
|
|
132
127
|
// 4) Determine reference price (30 decimals)
|
|
133
128
|
let price30;
|
|
@@ -157,6 +152,15 @@ export const openPositionSimpleTool = {
|
|
|
157
152
|
const price30Big = asBigint(price30, "price");
|
|
158
153
|
if (price30Big <= 0n)
|
|
159
154
|
throw new Error("price must be > 0.");
|
|
155
|
+
const collateralInput = String(args.collateralAmount ?? "").trim();
|
|
156
|
+
let collateralRaw = "";
|
|
157
|
+
let collateralRawBig = 0n;
|
|
158
|
+
if (collateralInput) {
|
|
159
|
+
collateralRaw = parseUserUnits(collateralInput, quoteDecimals, "collateralAmount");
|
|
160
|
+
collateralRawBig = asBigint(collateralRaw, "collateralAmount");
|
|
161
|
+
if (collateralRawBig <= 0n)
|
|
162
|
+
throw new Error("collateralAmount must be > 0.");
|
|
163
|
+
}
|
|
160
164
|
// 5) Compute or parse size (base raw units)
|
|
161
165
|
let sizeRaw = "";
|
|
162
166
|
let sizeMeta = { source: "computed" };
|
|
@@ -166,6 +170,9 @@ export const openPositionSimpleTool = {
|
|
|
166
170
|
sizeMeta = { source: "user" };
|
|
167
171
|
}
|
|
168
172
|
else {
|
|
173
|
+
if (!collateralInput) {
|
|
174
|
+
throw new Error("Either collateralAmount or size is required for open_position_simple.");
|
|
175
|
+
}
|
|
169
176
|
const notionalQuoteRaw = collateralRawBig * BigInt(args.leverage);
|
|
170
177
|
const numerator = notionalQuoteRaw * pow10(30 + baseDecimals);
|
|
171
178
|
const denominator = price30Big * pow10(quoteDecimals);
|
|
@@ -176,6 +183,22 @@ export const openPositionSimpleTool = {
|
|
|
176
183
|
sizeRaw = computed.toString();
|
|
177
184
|
sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
|
|
178
185
|
}
|
|
186
|
+
const sizeRawBig = asBigint(sizeRaw, "size");
|
|
187
|
+
if (!collateralInput) {
|
|
188
|
+
const notionalQuoteRaw = (sizeRawBig * price30Big * pow10(quoteDecimals)) / pow10(baseDecimals + 30);
|
|
189
|
+
const suggestedCollateralRaw = notionalQuoteRaw / BigInt(args.leverage);
|
|
190
|
+
const suggestedCollateralHuman = formatUnits(suggestedCollateralRaw, quoteDecimals);
|
|
191
|
+
const notionalHuman = formatUnits(notionalQuoteRaw, quoteDecimals);
|
|
192
|
+
throw new Error(`collateralAmount is required for open_position_simple. Based on size=${userSize || formatUnits(sizeRawBig, baseDecimals)}, price=${priceMeta.human ?? formatUnits(price30Big, 30)}, leverage=${args.leverage}, implied order value is ≈${notionalHuman} quote and suggested collateralAmount is ≈${suggestedCollateralHuman}.`);
|
|
193
|
+
}
|
|
194
|
+
const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
|
|
195
|
+
if (maxTradeAmountHuman) {
|
|
196
|
+
const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
|
|
197
|
+
const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
|
|
198
|
+
if (collateralRawBig > maxTradeRawBig) {
|
|
199
|
+
throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
179
202
|
// 6) Compute tradingFee (quote raw units)
|
|
180
203
|
let tradingFeeRaw = null;
|
|
181
204
|
let tradingFeeMeta = { source: "computed" };
|
package/dist/utils/address.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAddress } from "ethers";
|
|
2
|
+
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
2
3
|
export function normalizeAddress(value, label = "address") {
|
|
3
4
|
const raw = String(value || "").trim();
|
|
4
5
|
if (!raw)
|
|
@@ -15,3 +16,14 @@ export function normalizeAddress(value, label = "address") {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
export function isZeroAddress(value) {
|
|
20
|
+
const raw = String(value ?? "").trim();
|
|
21
|
+
if (!raw)
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
return getAddress(raw).toLowerCase() === ZERO_ADDRESS;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return raw.toLowerCase() === ZERO_ADDRESS;
|
|
28
|
+
}
|
|
29
|
+
}
|