@michaleffffff/mcp-trading-server 3.0.5 → 3.0.7
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 +24 -0
- package/README.md +2 -1
- package/dist/auth/resolveClient.js +19 -2
- package/dist/server.js +2 -2
- package/dist/services/poolService.js +45 -44
- package/dist/services/tradeService.js +18 -13
- package/dist/tools/accountInfo.js +4 -52
- package/dist/tools/accountTransfer.js +5 -10
- package/dist/tools/checkAccountReady.js +16 -10
- package/dist/tools/closePosition.js +17 -10
- package/dist/tools/getAccountSnapshot.js +10 -4
- package/dist/tools/getPoolMetadata.js +16 -4
- package/dist/tools/getUserTradingFeeRate.js +65 -2
- package/dist/tools/manageTpSl.js +82 -11
- package/dist/tools/openPositionSimple.js +5 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.7 - 2026-03-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Updated server runtime version banner and MCP server version to `3.0.7`.
|
|
7
|
+
- Hardened `get_pool_metadata` warning output by compacting long low-level errors into concise warnings.
|
|
8
|
+
- Optimized `get_pool_info` read path:
|
|
9
|
+
- Prefer resolving a positive oracle/ticker market price and use it directly for pool info reads.
|
|
10
|
+
- Return clearer domain error for empty-liquidity / unresolved-price scenarios.
|
|
11
|
+
- Refined `get_user_trading_fee_rate` error handling:
|
|
12
|
+
- Return structured MCP error envelope (`INVALID_PARAM` / `SDK_READ_ERROR`) instead of raw error strings.
|
|
13
|
+
- Include normalized concise error messages and request context details.
|
|
14
|
+
- Enhanced `account_deposit` usability by making `tokenAddress` optional (defaults to `QUOTE_TOKEN_ADDRESS`).
|
|
15
|
+
|
|
16
|
+
## 3.0.6 - 2026-03-18
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Upgraded SDK dependency to `@myx-trade/sdk@^1.0.2`.
|
|
20
|
+
- Updated client bootstrap to provide a viem-compatible `walletClient` shim (`json-rpc` account + `getAddresses/request/signMessage`) for SDK v1.0.2.
|
|
21
|
+
- Moved `get_trade_flow` to SDK v1.0.2 native path (`client.api.getTradeFlow`).
|
|
22
|
+
- Moved `account_withdraw` to SDK v1.0.2 native path (`account.updateAndWithdraw`).
|
|
23
|
+
- Normalized `baseToken` address before `pool.createPool` to satisfy stricter typed input.
|
|
24
|
+
- Updated account balance parsing to SDK v1.0.2 `getAccountInfo` fields (`freeMargin`, `walletBalance`).
|
|
25
|
+
- Hardened type guards for account snapshot and fee-rate parsing under SDK union return types.
|
|
26
|
+
|
|
3
27
|
## 3.0.5 - 2026-03-18
|
|
4
28
|
|
|
5
29
|
### Changed
|
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ 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.7**
|
|
10
|
+
- **SDK baseline**: `@myx-trade/sdk@^1.0.2` compatibility completed.
|
|
10
11
|
- **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
|
|
11
12
|
- **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
|
|
12
13
|
- **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
|
|
@@ -24,6 +24,8 @@ export async function resolveClient() {
|
|
|
24
24
|
const rpcUrl = process.env.RPC_URL || "https://rpc.sepolia.linea.build";
|
|
25
25
|
const privateKey = process.env.PRIVATE_KEY;
|
|
26
26
|
const chainId = Number(process.env.CHAIN_ID) || 59141;
|
|
27
|
+
const isTestnet = process.env.IS_TESTNET !== "false";
|
|
28
|
+
const isBetaMode = String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
|
|
27
29
|
const brokerAddressRaw = process.env.BROKER_ADDRESS || getDefaultBrokerByChainId(chainId);
|
|
28
30
|
const quoteTokenRaw = process.env.QUOTE_TOKEN_ADDRESS || getDefaultQuoteTokenByChainId(chainId);
|
|
29
31
|
const quoteDecimals = Number(process.env.QUOTE_TOKEN_DECIMALS) || 6;
|
|
@@ -42,12 +44,27 @@ export async function resolveClient() {
|
|
|
42
44
|
// Inject the EIP-1193 mock so SDK can sign transactions seamlessly
|
|
43
45
|
const { injectBrowserProviderMock } = await import("../utils/injectProvider.js");
|
|
44
46
|
injectBrowserProviderMock(chainId, provider, signer);
|
|
47
|
+
const ethereumProvider = globalThis.window.ethereum;
|
|
48
|
+
const walletClient = {
|
|
49
|
+
transport: ethereumProvider,
|
|
50
|
+
chain: { id: chainId },
|
|
51
|
+
account: { address: signer.address, type: "json-rpc" },
|
|
52
|
+
getAddresses: async () => [signer.address],
|
|
53
|
+
request: async (args) => ethereumProvider.request(args),
|
|
54
|
+
signMessage: async ({ message }) => {
|
|
55
|
+
const payload = typeof message === "string"
|
|
56
|
+
? message
|
|
57
|
+
: (message?.raw ?? message?.message ?? "");
|
|
58
|
+
return signer.signMessage(payload);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
45
61
|
const client = new MyxClient({
|
|
46
62
|
chainId,
|
|
47
63
|
signer: signer,
|
|
48
64
|
brokerAddress,
|
|
49
|
-
isTestnet
|
|
50
|
-
|
|
65
|
+
isTestnet,
|
|
66
|
+
isBetaMode,
|
|
67
|
+
walletClient: walletClient
|
|
51
68
|
});
|
|
52
69
|
cached = { client, address: signer.address, signer, chainId, quoteToken, quoteDecimals };
|
|
53
70
|
return cached;
|
package/dist/server.js
CHANGED
|
@@ -370,7 +370,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
372
|
// ─── MCP Server ───
|
|
373
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.
|
|
373
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.7" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
374
374
|
// List tools
|
|
375
375
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
376
376
|
return {
|
|
@@ -491,7 +491,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
491
491
|
async function main() {
|
|
492
492
|
const transport = new StdioServerTransport();
|
|
493
493
|
await server.connect(transport);
|
|
494
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
494
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.7 running (stdio, pure on-chain, prod ready)");
|
|
495
495
|
}
|
|
496
496
|
main().catch((err) => {
|
|
497
497
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -2,6 +2,7 @@ import { pool, quote, base } from "@myx-trade/sdk";
|
|
|
2
2
|
import { getChainId, resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
4
4
|
import { ensureUnits } from "../utils/units.js";
|
|
5
|
+
import { normalizeAddress } from "../utils/address.js";
|
|
5
6
|
function isDivideByZeroError(message) {
|
|
6
7
|
const lower = message.toLowerCase();
|
|
7
8
|
return (lower.includes("divide_by_zero") ||
|
|
@@ -19,74 +20,74 @@ function toPositiveBigint(input) {
|
|
|
19
20
|
return null;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
async function resolvePositiveMarketPrice30(client, poolId, chainId) {
|
|
24
|
+
if (!client)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const oracle = await client.utils?.getOraclePrice?.(poolId, chainId);
|
|
28
|
+
const byValue = toPositiveBigint(oracle?.value);
|
|
29
|
+
const byPrice = toPositiveBigint(oracle?.price);
|
|
30
|
+
if (byValue)
|
|
31
|
+
return byValue;
|
|
32
|
+
if (byPrice)
|
|
33
|
+
return byPrice;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const tickerRes = await client.markets?.getTickerList?.({ chainId, poolIds: [poolId] });
|
|
39
|
+
const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
|
|
40
|
+
if (row?.price) {
|
|
41
|
+
const tickerRaw = ensureUnits(row.price, 30, "ticker price");
|
|
42
|
+
const byTicker = toPositiveBigint(tickerRaw);
|
|
43
|
+
if (byTicker)
|
|
44
|
+
return byTicker;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
22
51
|
/**
|
|
23
52
|
* 创建合约市场池子
|
|
24
53
|
*/
|
|
25
54
|
export async function createPool(baseToken, marketId) {
|
|
26
55
|
await resolveClient();
|
|
27
56
|
const chainId = getChainId();
|
|
28
|
-
return pool.createPool({ chainId, baseToken, marketId });
|
|
57
|
+
return pool.createPool({ chainId, baseToken: normalizeAddress(baseToken, "baseToken"), marketId });
|
|
29
58
|
}
|
|
30
59
|
/**
|
|
31
60
|
* 获取池子信息
|
|
32
61
|
*/
|
|
33
62
|
export async function getPoolInfo(poolId, chainIdOverride, clientOverride) {
|
|
34
63
|
const chainId = chainIdOverride ?? getChainId();
|
|
35
|
-
|
|
64
|
+
const client = clientOverride ?? (await resolveClient()).client;
|
|
36
65
|
try {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
const marketPrice30 = await resolvePositiveMarketPrice30(client, poolId, chainId);
|
|
67
|
+
if (marketPrice30 && marketPrice30 > 0n) {
|
|
68
|
+
const withPrice = await pool.getPoolInfo(chainId, poolId, marketPrice30);
|
|
69
|
+
if (withPrice)
|
|
70
|
+
return withPrice;
|
|
71
|
+
}
|
|
41
72
|
}
|
|
42
73
|
catch (error) {
|
|
43
74
|
const message = extractErrorMessage(error);
|
|
44
75
|
if (!isDivideByZeroError(message)) {
|
|
45
76
|
throw new Error(`get_pool_info failed: ${message}`);
|
|
46
77
|
}
|
|
47
|
-
needOracleFallback = true;
|
|
48
|
-
}
|
|
49
|
-
if (!needOracleFallback)
|
|
50
|
-
return undefined;
|
|
51
|
-
const client = clientOverride ?? (await resolveClient()).client;
|
|
52
|
-
if (!client?.utils?.getOraclePrice) {
|
|
53
|
-
throw new Error("get_pool_info failed and oracle fallback is unavailable (client.utils.getOraclePrice missing).");
|
|
54
78
|
}
|
|
55
|
-
let oracleRaw = 0n;
|
|
56
79
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
const direct = await pool.getPoolInfo(chainId, poolId);
|
|
81
|
+
if (direct)
|
|
82
|
+
return direct;
|
|
83
|
+
throw new Error(`Pool info for ${poolId} returned undefined.`);
|
|
61
84
|
}
|
|
62
85
|
catch (error) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const tickerRes = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
|
|
68
|
-
const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
|
|
69
|
-
if (row?.price) {
|
|
70
|
-
const tickerRaw = ensureUnits(row.price, 30, "ticker price");
|
|
71
|
-
const byTicker = toPositiveBigint(tickerRaw);
|
|
72
|
-
oracleRaw = byTicker ?? 0n;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (oracleRaw <= 0n) {
|
|
79
|
-
throw new Error("get_pool_info fallback requires a positive oracle/ticker price, but both resolved to 0.");
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
const retried = await pool.getPoolInfo(chainId, poolId, oracleRaw);
|
|
83
|
-
if (!retried) {
|
|
84
|
-
throw new Error(`Pool info for ${poolId} returned undefined after oracle-price retry.`);
|
|
86
|
+
const message = extractErrorMessage(error);
|
|
87
|
+
if (isDivideByZeroError(message)) {
|
|
88
|
+
throw new Error("get_pool_info unavailable: pool reserves are currently empty or market price context is unresolved.");
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
throw new Error(`get_pool_info failed after oracle-price retry: ${extractErrorMessage(error)}`);
|
|
90
|
+
throw new Error(`get_pool_info failed: ${message}`);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
/**
|
|
@@ -93,6 +93,17 @@ function parseDecimals(value, fallback) {
|
|
|
93
93
|
function normalizeIdentifier(value) {
|
|
94
94
|
return String(value ?? "").trim().toLowerCase();
|
|
95
95
|
}
|
|
96
|
+
function toBigIntOrZero(value) {
|
|
97
|
+
try {
|
|
98
|
+
const text = String(value ?? "").trim();
|
|
99
|
+
if (!text)
|
|
100
|
+
return 0n;
|
|
101
|
+
return BigInt(text);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return 0n;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
96
107
|
async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
|
|
97
108
|
let baseDecimals = 18;
|
|
98
109
|
let quoteDecimals = getQuoteDecimals();
|
|
@@ -193,16 +204,10 @@ export async function openPosition(client, address, args) {
|
|
|
193
204
|
console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
|
|
194
205
|
const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
|
|
195
206
|
let marginBalanceRaw = BigInt(0);
|
|
196
|
-
let walletBalanceRaw = BigInt(0);
|
|
197
|
-
if (marginInfo?.code === 0) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
walletBalanceRaw = BigInt(marginInfo.data[1] || "0");
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
marginBalanceRaw = BigInt(marginInfo.data?.marginBalance || "0");
|
|
204
|
-
walletBalanceRaw = BigInt(marginInfo.data?.availableMargin || "0");
|
|
205
|
-
}
|
|
207
|
+
let walletBalanceRaw = BigInt(0);
|
|
208
|
+
if (marginInfo?.code === 0 && marginInfo?.data) {
|
|
209
|
+
marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
|
|
210
|
+
walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
206
211
|
}
|
|
207
212
|
const requiredRaw = BigInt(collateralRaw);
|
|
208
213
|
if (marginBalanceRaw < requiredRaw) {
|
|
@@ -213,15 +218,15 @@ export async function openPosition(client, address, args) {
|
|
|
213
218
|
const neededRaw = requiredRaw - marginBalanceRaw;
|
|
214
219
|
console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
|
|
215
220
|
if (walletBalanceRaw < neededRaw) {
|
|
216
|
-
// Also check real wallet balance just in case
|
|
221
|
+
// Also check real wallet balance just in case account info wallet field is stale.
|
|
217
222
|
const realWalletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
218
223
|
const realWalletRaw = BigInt(realWalletRes?.data || "0");
|
|
219
224
|
if (realWalletRaw < neededRaw) {
|
|
220
|
-
throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) +
|
|
225
|
+
throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + walletBalance (${walletBalanceRaw.toString()}) + realWallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
|
|
221
226
|
}
|
|
222
227
|
walletBalanceRaw = realWalletRaw;
|
|
223
228
|
}
|
|
224
|
-
console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet
|
|
229
|
+
console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet...`);
|
|
225
230
|
const depositRaw = await client.account.deposit({
|
|
226
231
|
amount: neededRaw.toString(),
|
|
227
232
|
tokenAddress: poolData.quoteToken,
|
|
@@ -1,57 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { getTradeFlowTypeDesc } from "../utils/mappings.js";
|
|
4
|
-
function readErrorMessage(error) {
|
|
5
|
-
if (error instanceof Error)
|
|
6
|
-
return error.message;
|
|
7
|
-
if (typeof error === "string")
|
|
8
|
-
return error;
|
|
9
|
-
if (error && typeof error === "object" && "message" in error) {
|
|
10
|
-
return String(error.message);
|
|
11
|
-
}
|
|
12
|
-
return String(error ?? "unknown error");
|
|
13
|
-
}
|
|
14
|
-
function assertSdkReadSuccess(result, actionName) {
|
|
15
|
-
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
16
|
-
return;
|
|
17
|
-
if (!Object.prototype.hasOwnProperty.call(result, "code"))
|
|
18
|
-
return;
|
|
19
|
-
const code = Number(result.code);
|
|
20
|
-
if (!Number.isFinite(code) || code === 0)
|
|
21
|
-
return;
|
|
22
|
-
const payload = result.data;
|
|
23
|
-
const hasPayload = payload !== null &&
|
|
24
|
-
payload !== undefined &&
|
|
25
|
-
(!Array.isArray(payload) || payload.length > 0);
|
|
26
|
-
if (code > 0 && hasPayload) {
|
|
27
|
-
// Keep aligned with server-side read normalization:
|
|
28
|
-
// positive non-zero code with usable payload is warning-like.
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const message = String(result.msg ?? result.message ?? "unknown error");
|
|
32
|
-
throw new Error(`${actionName} failed: code=${code}, msg=${message}`);
|
|
33
|
-
}
|
|
34
|
-
function normalizeAccountInfo(result) {
|
|
35
|
-
if (!(result && result.code === 0 && Array.isArray(result.data))) {
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
const values = result.data;
|
|
39
|
-
return {
|
|
40
|
-
...result,
|
|
41
|
-
data: {
|
|
42
|
-
freeMargin: values[0],
|
|
43
|
-
walletBalance: values[1],
|
|
44
|
-
freeBaseAmount: values[2],
|
|
45
|
-
baseProfit: values[3],
|
|
46
|
-
quoteProfit: values[4],
|
|
47
|
-
reservedAmount: values[5],
|
|
48
|
-
releaseTime: values[6],
|
|
49
|
-
tradeableMargin: (BigInt(values[0]) + BigInt(values[1]) + (BigInt(values[6]) === 0n ? BigInt(values[4]) : 0n)).toString(),
|
|
50
|
-
baseProfitStatus: "base token to be unlocked",
|
|
51
|
-
quoteProfitStatus: BigInt(values[6]) > 0n ? "quote token to be unlocked" : "quote token unlocked/available"
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
4
|
export const getTradeFlowTool = {
|
|
56
5
|
name: "get_trade_flow",
|
|
57
6
|
description: "[ACCOUNT] Get account trade flow / transaction history.",
|
|
@@ -64,7 +13,10 @@ export const getTradeFlowTool = {
|
|
|
64
13
|
const { client, address } = await resolveClient();
|
|
65
14
|
const chainId = getChainId();
|
|
66
15
|
const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
|
|
67
|
-
const
|
|
16
|
+
const accessToken = typeof client?.getAccessToken === "function"
|
|
17
|
+
? (await client.getAccessToken()) || ""
|
|
18
|
+
: "";
|
|
19
|
+
const result = await client.api.getTradeFlow({ ...query, address, accessToken });
|
|
68
20
|
const enhancedData = (result?.data || []).map((flow) => ({
|
|
69
21
|
...flow,
|
|
70
22
|
typeDesc: getTradeFlowTypeDesc(flow.type)
|
|
@@ -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 { normalizeAddress } from "../utils/address.js";
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
5
|
const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
|
|
@@ -42,7 +42,7 @@ export const accountDepositTool = {
|
|
|
42
42
|
description: "[ACCOUNT] Deposit funds from wallet into the MYX trading account.",
|
|
43
43
|
schema: {
|
|
44
44
|
amount: z.union([z.string(), z.number()]).describe("Amount to deposit (human-readable or raw units)"),
|
|
45
|
-
tokenAddress: z.string().describe("Token address"),
|
|
45
|
+
tokenAddress: z.string().optional().describe("Token address (optional, default: QUOTE_TOKEN_ADDRESS)"),
|
|
46
46
|
autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token allowance when needed (default false)."),
|
|
47
47
|
approveMax: z.coerce.boolean().optional().describe("If autoApprove=true, approve MaxUint256 instead of exact amount."),
|
|
48
48
|
},
|
|
@@ -50,7 +50,8 @@ export const accountDepositTool = {
|
|
|
50
50
|
try {
|
|
51
51
|
const { client, signer, address } = await resolveClient();
|
|
52
52
|
const chainId = getChainId();
|
|
53
|
-
const
|
|
53
|
+
const tokenAddressInput = String(args.tokenAddress ?? "").trim() || getQuoteToken();
|
|
54
|
+
const tokenAddress = normalizeAddress(tokenAddressInput, "tokenAddress");
|
|
54
55
|
// For deposit, we default to quote decimals (6) as it's the most common use case.
|
|
55
56
|
// ensureUnits handles 'raw:' prefix if absolute precision is needed.
|
|
56
57
|
const { ensureUnits } = await import("../utils/units.js");
|
|
@@ -128,13 +129,7 @@ export const accountWithdrawTool = {
|
|
|
128
129
|
throw new Error(`Account has locked funds until releaseTime=${formatUnixTimestamp(releaseTime)}. ` +
|
|
129
130
|
`Retry after unlock or reduce withdraw amount.`);
|
|
130
131
|
}
|
|
131
|
-
const raw = await client.account.
|
|
132
|
-
chainId,
|
|
133
|
-
receiver: address,
|
|
134
|
-
amount,
|
|
135
|
-
poolId: args.poolId,
|
|
136
|
-
isQuoteToken: args.isQuoteToken,
|
|
137
|
-
});
|
|
132
|
+
const raw = await client.account.updateAndWithdraw(address, args.poolId, Boolean(args.isQuoteToken), amount, chainId);
|
|
138
133
|
const data = await finalizeMutationResult(raw, signer, "account_withdraw");
|
|
139
134
|
const preflight = {
|
|
140
135
|
requestedAmountRaw: amountRaw.toString(),
|
|
@@ -3,6 +3,17 @@ import { resolveClient, getChainId } 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";
|
|
6
|
+
function toBigIntOrZero(value) {
|
|
7
|
+
try {
|
|
8
|
+
const text = String(value ?? "").trim();
|
|
9
|
+
if (!text)
|
|
10
|
+
return 0n;
|
|
11
|
+
return BigInt(text);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 0n;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
export const checkAccountReadyTool = {
|
|
7
18
|
name: "check_account_ready",
|
|
8
19
|
description: "[TRADE] Check if the account has sufficient funds (margin + wallet) for a planned trade.",
|
|
@@ -22,16 +33,10 @@ export const checkAccountReadyTool = {
|
|
|
22
33
|
const quoteSymbol = detail?.quoteSymbol || "USDC";
|
|
23
34
|
const marginInfo = await client.account.getAccountInfo(chainId, address, poolId);
|
|
24
35
|
let marginBalanceRaw = 0n;
|
|
25
|
-
let
|
|
26
|
-
if (marginInfo?.code === 0) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
availableMarginRaw = BigInt(marginInfo.data[1] || "0");
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
marginBalanceRaw = BigInt(marginInfo.data?.marginBalance || "0");
|
|
33
|
-
availableMarginRaw = BigInt(marginInfo.data?.availableMargin || "0");
|
|
34
|
-
}
|
|
36
|
+
let accountWalletBalanceRaw = 0n;
|
|
37
|
+
if (marginInfo?.code === 0 && marginInfo?.data) {
|
|
38
|
+
marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
|
|
39
|
+
accountWalletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
35
40
|
}
|
|
36
41
|
const walletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
37
42
|
const walletBalanceRaw = BigInt(walletRes?.data || "0");
|
|
@@ -55,6 +60,7 @@ export const checkAccountReadyTool = {
|
|
|
55
60
|
hasEnoughInMargin: marginBalanceRaw >= requiredRaw,
|
|
56
61
|
needDepositFromWallet,
|
|
57
62
|
walletSufficientForDeposit: walletSufficient,
|
|
63
|
+
accountInfoWalletBalance: format(accountWalletBalanceRaw),
|
|
58
64
|
quoteSymbol
|
|
59
65
|
}
|
|
60
66
|
}
|
|
@@ -5,23 +5,30 @@ 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
|
+
import { parseUserUnits } from "../utils/units.js";
|
|
8
9
|
const FULL_CLOSE_MARKERS = new Set(["ALL", "FULL", "MAX"]);
|
|
9
10
|
const INTEGER_RE = /^\d+$/;
|
|
10
11
|
function wantsFullCloseMarker(input) {
|
|
11
12
|
const value = String(input ?? "").trim().toUpperCase();
|
|
12
13
|
return FULL_CLOSE_MARKERS.has(value);
|
|
13
14
|
}
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const second = String(position?.[fallback] ?? "").trim();
|
|
20
|
-
if (INTEGER_RE.test(second))
|
|
21
|
-
return second;
|
|
15
|
+
function readFirstPositionField(position, fields) {
|
|
16
|
+
for (const field of fields) {
|
|
17
|
+
const value = String(position?.[field] ?? "").trim();
|
|
18
|
+
if (value)
|
|
19
|
+
return value;
|
|
22
20
|
}
|
|
23
21
|
return "";
|
|
24
22
|
}
|
|
23
|
+
function resolvePositionRaw(position, rawFields, humanFields, decimals, label) {
|
|
24
|
+
const raw = readFirstPositionField(position, rawFields);
|
|
25
|
+
if (INTEGER_RE.test(raw))
|
|
26
|
+
return raw;
|
|
27
|
+
const human = readFirstPositionField(position, humanFields);
|
|
28
|
+
if (!human)
|
|
29
|
+
return "";
|
|
30
|
+
return parseUserUnits(human, decimals, label);
|
|
31
|
+
}
|
|
25
32
|
export const closePositionTool = {
|
|
26
33
|
name: "close_position",
|
|
27
34
|
description: "[TRADE] Create a decrease order (close or reduce position) using SDK-native parameters.",
|
|
@@ -67,14 +74,14 @@ export const closePositionTool = {
|
|
|
67
74
|
throw new Error(`Could not find live position snapshot for positionId=${preparedArgs.positionId} in poolId=${poolId}.`);
|
|
68
75
|
}
|
|
69
76
|
if (needAutoSize) {
|
|
70
|
-
const rawSize =
|
|
77
|
+
const rawSize = resolvePositionRaw(target, ["sizeRaw", "positionSizeRaw"], ["size", "positionSize"], Number(poolData.baseDecimals ?? 18), "position.size");
|
|
71
78
|
if (!rawSize || rawSize === "0") {
|
|
72
79
|
throw new Error(`Resolved position size is empty/zero for positionId=${preparedArgs.positionId}.`);
|
|
73
80
|
}
|
|
74
81
|
preparedArgs.size = `raw:${rawSize}`;
|
|
75
82
|
}
|
|
76
83
|
if (needAutoCollateral) {
|
|
77
|
-
const rawCollateral =
|
|
84
|
+
const rawCollateral = resolvePositionRaw(target, ["collateralRaw", "collateralAmountRaw"], ["collateral", "collateralAmount"], Number(poolData.quoteDecimals ?? 6), "position.collateralAmount");
|
|
78
85
|
if (!rawCollateral) {
|
|
79
86
|
throw new Error(`Resolved position collateral is empty for positionId=${preparedArgs.positionId}.`);
|
|
80
87
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
function unwrapData(result) {
|
|
4
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
5
|
+
return result.data;
|
|
6
|
+
}
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
3
9
|
export const getAccountSnapshotTool = {
|
|
4
10
|
name: "get_account_snapshot",
|
|
5
11
|
description: "[ACCOUNT] Get unified account snapshot (balances, trading metrics, and VIP tier).",
|
|
@@ -23,14 +29,14 @@ export const getAccountSnapshotTool = {
|
|
|
23
29
|
getMarginBalance(client, address, args.poolId, chainId).catch(() => null)
|
|
24
30
|
]);
|
|
25
31
|
tradingAccount = {
|
|
26
|
-
info: info
|
|
27
|
-
margin: margin
|
|
32
|
+
info: unwrapData(info),
|
|
33
|
+
margin: unwrapData(margin)
|
|
28
34
|
};
|
|
29
35
|
}
|
|
30
36
|
const results = {
|
|
31
|
-
wallet: walletRes
|
|
37
|
+
wallet: unwrapData(walletRes),
|
|
32
38
|
tradingAccount,
|
|
33
|
-
vip: vipRes
|
|
39
|
+
vip: unwrapData(vipRes),
|
|
34
40
|
};
|
|
35
41
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: results }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
36
42
|
}
|
|
@@ -17,6 +17,18 @@ function normalizeMarketPrice30Input(value) {
|
|
|
17
17
|
}
|
|
18
18
|
return parseUserPrice30(text, "marketPrice");
|
|
19
19
|
}
|
|
20
|
+
function compactWarning(scope, err) {
|
|
21
|
+
const raw = extractErrorMessage(err);
|
|
22
|
+
const flat = raw.replace(/\s+/g, " ").trim();
|
|
23
|
+
const lower = flat.toLowerCase();
|
|
24
|
+
if (lower.includes("division or modulo by zero") || lower.includes("panic code 0x12")) {
|
|
25
|
+
return `${scope}: unavailable for this pool at current liquidity/price context.`;
|
|
26
|
+
}
|
|
27
|
+
if (flat.length > 220) {
|
|
28
|
+
return `${scope}: ${flat.slice(0, 220)}...`;
|
|
29
|
+
}
|
|
30
|
+
return `${scope}: ${flat}`;
|
|
31
|
+
}
|
|
20
32
|
export const getPoolMetadataTool = {
|
|
21
33
|
name: "get_pool_metadata",
|
|
22
34
|
description: "[MARKET] Get comprehensive metadata for a pool (market detail, on-chain info, liquidity, and limits).",
|
|
@@ -40,14 +52,14 @@ export const getPoolMetadataTool = {
|
|
|
40
52
|
results.marketDetail = await getMarketDetail(client, poolId, chainId);
|
|
41
53
|
}
|
|
42
54
|
catch (err) {
|
|
43
|
-
errors.push(
|
|
55
|
+
errors.push(compactWarning("marketDetail", err));
|
|
44
56
|
}
|
|
45
57
|
// 2. Pool Info (Reserves, Utilization)
|
|
46
58
|
try {
|
|
47
59
|
results.poolInfo = await getPoolInfo(poolId, chainId, client);
|
|
48
60
|
}
|
|
49
61
|
catch (err) {
|
|
50
|
-
errors.push(
|
|
62
|
+
errors.push(compactWarning("poolInfo", err));
|
|
51
63
|
}
|
|
52
64
|
// 3. Optional: Liquidity Info
|
|
53
65
|
if (args.includeLiquidity) {
|
|
@@ -58,7 +70,7 @@ export const getPoolMetadataTool = {
|
|
|
58
70
|
results.liquidityInfo = await getLiquidityInfo(client, poolId, price, chainId);
|
|
59
71
|
}
|
|
60
72
|
catch (err) {
|
|
61
|
-
errors.push(
|
|
73
|
+
errors.push(compactWarning("liquidityInfo", err));
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
// 4. Optional: Config / Limits
|
|
@@ -68,7 +80,7 @@ export const getPoolMetadataTool = {
|
|
|
68
80
|
results.levelConfig = await getPoolLevelConfig(client, poolId, chainId);
|
|
69
81
|
}
|
|
70
82
|
catch (err) {
|
|
71
|
-
errors.push(
|
|
83
|
+
errors.push(compactWarning("levelConfig", err));
|
|
72
84
|
}
|
|
73
85
|
}
|
|
74
86
|
if (errors.length > 0) {
|
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
4
|
+
function compactMessage(message) {
|
|
5
|
+
const flat = String(message ?? "").replace(/\s+/g, " ").trim();
|
|
6
|
+
if (!flat)
|
|
7
|
+
return "Unknown fee-rate read error.";
|
|
8
|
+
if (flat.length <= 240)
|
|
9
|
+
return flat;
|
|
10
|
+
return `${flat.slice(0, 240)}...`;
|
|
11
|
+
}
|
|
12
|
+
async function withMutedSdkFeeRateLogs(runner) {
|
|
13
|
+
const original = console.error;
|
|
14
|
+
console.error = (...args) => {
|
|
15
|
+
const first = args?.[0];
|
|
16
|
+
const firstText = typeof first === "string"
|
|
17
|
+
? first
|
|
18
|
+
: first instanceof Error
|
|
19
|
+
? first.message
|
|
20
|
+
: String(first ?? "");
|
|
21
|
+
const lower = firstText.toLowerCase();
|
|
22
|
+
if (lower.includes("myx-sdk-error") ||
|
|
23
|
+
(lower.includes("getuserfeerate") && (lower.includes("revert") || lower.includes("contractfunctionexecutionerror")))) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
original(...args);
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
return await runner();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
console.error = original;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function buildErrorPayload(args, messageLike) {
|
|
36
|
+
const message = compactMessage(extractErrorMessage(messageLike));
|
|
37
|
+
const lower = message.toLowerCase();
|
|
38
|
+
const code = lower.includes("invalidparameter") || lower.includes("invalid parameter")
|
|
39
|
+
? "INVALID_PARAM"
|
|
40
|
+
: "SDK_READ_ERROR";
|
|
41
|
+
const hint = code === "INVALID_PARAM"
|
|
42
|
+
? "Check assetClass/riskTier for this pool and retry."
|
|
43
|
+
: "Fee tier may be unavailable for current account/market context. Retry with valid assetClass/riskTier or provide tradingFee manually.";
|
|
44
|
+
return {
|
|
45
|
+
status: "error",
|
|
46
|
+
error: {
|
|
47
|
+
tool: "get_user_trading_fee_rate",
|
|
48
|
+
code,
|
|
49
|
+
message,
|
|
50
|
+
hint,
|
|
51
|
+
action: "Adjust params/context and retry.",
|
|
52
|
+
details: {
|
|
53
|
+
assetClass: args.assetClass,
|
|
54
|
+
riskTier: args.riskTier,
|
|
55
|
+
chainId: args.chainId ?? getChainId(),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
3
60
|
export const getUserTradingFeeRateTool = {
|
|
4
61
|
name: "get_user_trading_fee_rate",
|
|
5
62
|
description: "[TRADE] Get maker/taker fee rates for a given assetClass and riskTier.",
|
|
@@ -12,11 +69,17 @@ export const getUserTradingFeeRateTool = {
|
|
|
12
69
|
try {
|
|
13
70
|
const { client } = await resolveClient();
|
|
14
71
|
const chainId = args.chainId ?? getChainId();
|
|
15
|
-
const result = await client.utils.getUserTradingFeeRate(args.assetClass, args.riskTier, chainId);
|
|
72
|
+
const result = await withMutedSdkFeeRateLogs(() => client.utils.getUserTradingFeeRate(args.assetClass, args.riskTier, chainId));
|
|
73
|
+
const maybeCode = Number(result?.code);
|
|
74
|
+
if (Number.isFinite(maybeCode) && maybeCode !== 0) {
|
|
75
|
+
const body = buildErrorPayload(args, result?.msg ?? result?.message ?? result);
|
|
76
|
+
return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
|
|
77
|
+
}
|
|
16
78
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
|
|
17
79
|
}
|
|
18
80
|
catch (error) {
|
|
19
|
-
|
|
81
|
+
const body = buildErrorPayload(args, error);
|
|
82
|
+
return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
|
|
20
83
|
}
|
|
21
84
|
},
|
|
22
85
|
};
|
package/dist/tools/manageTpSl.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
4
|
+
import { parseUserUnits } from "../utils/units.js";
|
|
5
|
+
const INTEGER_RE = /^\d+$/;
|
|
4
6
|
function normalizeDirectionInput(value) {
|
|
5
7
|
if (value === undefined || value === null || value === "")
|
|
6
8
|
return undefined;
|
|
@@ -60,6 +62,44 @@ async function findOrderSnapshot(client, address, chainId, orderId, poolId) {
|
|
|
60
62
|
return null;
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
async function findPositionSnapshot(client, address, poolId, positionId) {
|
|
66
|
+
const targetPos = String(positionId ?? "").trim().toLowerCase();
|
|
67
|
+
const targetPool = String(poolId ?? "").trim().toLowerCase();
|
|
68
|
+
if (!targetPos || !targetPool)
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const positionsRes = await client.position.listPositions(address);
|
|
72
|
+
const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
|
|
73
|
+
return positions.find((position) => {
|
|
74
|
+
const pid = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
|
|
75
|
+
const pool = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
|
|
76
|
+
return pid === targetPos && pool === targetPool;
|
|
77
|
+
}) ?? null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function resolvePositionSizeRaw(positionSnapshot, baseDecimals) {
|
|
84
|
+
const rawCandidates = [positionSnapshot?.sizeRaw, positionSnapshot?.positionSizeRaw];
|
|
85
|
+
for (const value of rawCandidates) {
|
|
86
|
+
const text = String(value ?? "").trim();
|
|
87
|
+
if (INTEGER_RE.test(text))
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
const humanCandidates = [positionSnapshot?.size, positionSnapshot?.positionSize];
|
|
91
|
+
for (const value of humanCandidates) {
|
|
92
|
+
const text = String(value ?? "").trim();
|
|
93
|
+
if (!text)
|
|
94
|
+
continue;
|
|
95
|
+
try {
|
|
96
|
+
return parseUserUnits(text, baseDecimals, "position.size");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
63
103
|
export const manageTpSlTool = {
|
|
64
104
|
name: "manage_tp_sl",
|
|
65
105
|
description: "[TRADE] Set or update Take Profit (TP) and Stop Loss (SL) for a position.",
|
|
@@ -87,12 +127,16 @@ export const manageTpSlTool = {
|
|
|
87
127
|
const direction = normalizeDirectionInput(args.direction);
|
|
88
128
|
const slippagePct = args.slippagePct ?? "100";
|
|
89
129
|
const { setPositionTpSl, updateOrderTpSl } = await import("../services/tradeService.js");
|
|
130
|
+
const { getMarketDetail } = await import("../services/marketService.js");
|
|
131
|
+
const marketRes = await getMarketDetail(client, args.poolId, chainId);
|
|
132
|
+
const market = marketRes?.data ?? marketRes;
|
|
133
|
+
if (!market?.marketId) {
|
|
134
|
+
throw new Error(`Could not resolve market metadata for poolId=${args.poolId}.`);
|
|
135
|
+
}
|
|
136
|
+
const baseDecimals = Number(market.baseDecimals ?? 18);
|
|
90
137
|
let raw;
|
|
91
138
|
if (args.orderId) {
|
|
92
139
|
// Update existing
|
|
93
|
-
const { getMarketDetail } = await import("../services/marketService.js");
|
|
94
|
-
const marketRes = await getMarketDetail(client, args.poolId, chainId);
|
|
95
|
-
const market = marketRes.data ?? marketRes;
|
|
96
140
|
const orderSnapshot = await findOrderSnapshot(client, address, chainId, args.orderId, args.poolId);
|
|
97
141
|
const snapshotSize = readSnapshotText(orderSnapshot, ["size", "orderSize", "positionSize"]);
|
|
98
142
|
const snapshotPrice = readSnapshotText(orderSnapshot, ["price", "orderPrice", "triggerPrice"]);
|
|
@@ -134,23 +178,50 @@ export const manageTpSlTool = {
|
|
|
134
178
|
}
|
|
135
179
|
else {
|
|
136
180
|
// Create new
|
|
137
|
-
if (
|
|
181
|
+
if (!args.positionId)
|
|
182
|
+
throw new Error("positionId is required when creating new TP/SL.");
|
|
183
|
+
const positionSnapshot = await findPositionSnapshot(client, address, args.poolId, args.positionId);
|
|
184
|
+
let resolvedDirection = direction;
|
|
185
|
+
if (resolvedDirection === undefined && positionSnapshot?.direction !== undefined) {
|
|
186
|
+
resolvedDirection = normalizeDirectionInput(positionSnapshot.direction);
|
|
187
|
+
}
|
|
188
|
+
let resolvedLeverage = args.leverage;
|
|
189
|
+
if ((!resolvedLeverage || Number(resolvedLeverage) <= 0) && positionSnapshot) {
|
|
190
|
+
const leverageCandidate = Number(positionSnapshot?.userLeverage ??
|
|
191
|
+
positionSnapshot?.leverage ??
|
|
192
|
+
positionSnapshot?.positionLeverage);
|
|
193
|
+
if (Number.isFinite(leverageCandidate) && leverageCandidate > 0) {
|
|
194
|
+
resolvedLeverage = leverageCandidate;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (resolvedDirection === undefined || !resolvedLeverage || Number(resolvedLeverage) <= 0) {
|
|
138
198
|
throw new Error("direction and leverage are required when creating new TP/SL.");
|
|
139
199
|
}
|
|
140
|
-
|
|
141
|
-
|
|
200
|
+
let tpSizeInput = isNonEmpty(args.tpSize) ? String(args.tpSize) : "";
|
|
201
|
+
let slSizeInput = isNonEmpty(args.slSize) ? String(args.slSize) : "";
|
|
202
|
+
const needsTpSize = isNonEmpty(args.tpPrice) && !tpSizeInput;
|
|
203
|
+
const needsSlSize = isNonEmpty(args.slPrice) && !slSizeInput;
|
|
204
|
+
if (needsTpSize || needsSlSize) {
|
|
205
|
+
const positionSizeRaw = resolvePositionSizeRaw(positionSnapshot, baseDecimals);
|
|
206
|
+
if (!positionSizeRaw) {
|
|
207
|
+
throw new Error("tpSize/slSize missing and unable to infer position size from live snapshot. Please provide tpSize/slSize explicitly.");
|
|
208
|
+
}
|
|
209
|
+
if (needsTpSize)
|
|
210
|
+
tpSizeInput = `raw:${positionSizeRaw}`;
|
|
211
|
+
if (needsSlSize)
|
|
212
|
+
slSizeInput = `raw:${positionSizeRaw}`;
|
|
142
213
|
}
|
|
143
214
|
raw = await setPositionTpSl(client, address, {
|
|
144
215
|
poolId: args.poolId,
|
|
145
216
|
positionId: args.positionId,
|
|
146
|
-
direction,
|
|
147
|
-
leverage:
|
|
148
|
-
executionFeeToken: args.executionFeeToken,
|
|
217
|
+
direction: resolvedDirection,
|
|
218
|
+
leverage: Number(resolvedLeverage),
|
|
219
|
+
executionFeeToken: args.executionFeeToken || market.quoteToken,
|
|
149
220
|
slippagePct,
|
|
150
221
|
tpPrice: args.tpPrice,
|
|
151
|
-
tpSize:
|
|
222
|
+
tpSize: tpSizeInput || undefined,
|
|
152
223
|
slPrice: args.slPrice,
|
|
153
|
-
slSize:
|
|
224
|
+
slSize: slSizeInput || undefined
|
|
154
225
|
}, chainId);
|
|
155
226
|
}
|
|
156
227
|
const data = await finalizeMutationResult(raw, signer, "manage_tp_sl");
|
|
@@ -185,15 +185,17 @@ export const openPositionSimpleTool = {
|
|
|
185
185
|
tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: null, error: null };
|
|
186
186
|
try {
|
|
187
187
|
const feeRes = await client.utils.getUserTradingFeeRate(assetClass, riskTier, chainId);
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
const hasData = feeRes && typeof feeRes === "object" && "data" in feeRes && feeRes.data;
|
|
189
|
+
if (feeRes && Number(feeRes.code) === 0 && hasData) {
|
|
190
|
+
const feeData = feeRes.data;
|
|
191
|
+
const rateRaw = postOnly ? feeData.makerFeeRate : feeData.takerFeeRate;
|
|
190
192
|
tradingFeeMeta.feeRate = rateRaw;
|
|
191
193
|
const rate = asBigint(String(rateRaw), "feeRate");
|
|
192
194
|
const fee = (collateralRawBig * rate) / 1000000n;
|
|
193
195
|
tradingFeeRaw = fee.toString();
|
|
194
196
|
}
|
|
195
197
|
else {
|
|
196
|
-
tradingFeeMeta.error = feeRes
|
|
198
|
+
tradingFeeMeta.error = String((feeRes && (feeRes.message ?? feeRes.msg)) || "fee_rate_unavailable");
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michaleffffff/mcp-trading-server",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"myx-mcp": "dist/server.js"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
23
|
-
"@myx-trade/sdk": "^0.
|
|
23
|
+
"@myx-trade/sdk": "^1.0.2",
|
|
24
24
|
"dotenv": "^17.3.1",
|
|
25
25
|
"ethers": "^6.13.1",
|
|
26
26
|
"zod": "^4.3.6"
|