@michaleffffff/mcp-trading-server 2.4.2 → 2.5.0
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/README.md +5 -0
- package/dist/auth/resolveClient.js +20 -3
- package/dist/services/tradeService.js +4 -3
- package/dist/tools/accountTransfer.js +9 -6
- package/dist/tools/adjustMargin.js +7 -5
- package/dist/tools/cancelAllOrders.js +4 -2
- package/dist/tools/cancelOrder.js +5 -3
- package/dist/tools/checkApproval.js +5 -3
- package/dist/tools/closeAllPositions.js +9 -4
- package/dist/tools/closePosition.js +15 -10
- package/dist/tools/createPerpMarket.js +6 -2
- package/dist/tools/executeTrade.js +103 -19
- package/dist/tools/manageLiquidity.js +8 -4
- package/dist/tools/setTpSl.js +17 -12
- package/dist/tools/updateOrderTpSl.js +5 -3
- package/dist/utils/mutationResult.js +68 -0
- package/dist/utils/slippage.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,11 @@ IS_TESTNET=false
|
|
|
82
82
|
MAX_TRADE_AMOUNT=5000 # Optional safety constraint limit for AI
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
Testnet quick mapping:
|
|
86
|
+
|
|
87
|
+
- `Arbitrum Sepolia (421614)`: `BROKER_ADDRESS=0x8f3153C18f698166f5D6124d8ba5B567F5f120f9`, `QUOTE_TOKEN_ADDRESS=0x7E248Ec1721639413A280d9E82e2862Cae2E6E28`
|
|
88
|
+
- `Linea Sepolia (59141)`: `BROKER_ADDRESS=0x0FB08D3A1Ea6bE515fe78D3e0CaEb6990b468Cf3`, `QUOTE_TOKEN_ADDRESS=0xD984fd34f91F92DA0586e1bE82E262fF27DC431b`
|
|
89
|
+
|
|
85
90
|
---
|
|
86
91
|
|
|
87
92
|
# Running the Server
|
|
@@ -2,14 +2,30 @@ import { MyxClient } from "@myx-trade/sdk";
|
|
|
2
2
|
import { JsonRpcProvider, Wallet } from "ethers";
|
|
3
3
|
import { normalizeAddress } from "../utils/address.js";
|
|
4
4
|
let cached = null;
|
|
5
|
+
function getDefaultBrokerByChainId(chainId) {
|
|
6
|
+
// Testnet mappings
|
|
7
|
+
if (chainId === 421614)
|
|
8
|
+
return "0x8f3153C18f698166f5D6124d8ba5B567F5f120f9"; // Arbitrum Sepolia
|
|
9
|
+
if (chainId === 59141)
|
|
10
|
+
return "0x0FB08D3A1Ea6bE515fe78D3e0CaEb6990b468Cf3"; // Linea Sepolia
|
|
11
|
+
return "0x8f3153C18f698166f5D6124d8ba5B567F5f120f9";
|
|
12
|
+
}
|
|
13
|
+
function getDefaultQuoteTokenByChainId(chainId) {
|
|
14
|
+
// Testnet mappings
|
|
15
|
+
if (chainId === 421614)
|
|
16
|
+
return "0x7E248Ec1721639413A280d9E82e2862Cae2E6E28"; // Arbitrum Sepolia
|
|
17
|
+
if (chainId === 59141)
|
|
18
|
+
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b"; // Linea Sepolia
|
|
19
|
+
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b";
|
|
20
|
+
}
|
|
5
21
|
export async function resolveClient() {
|
|
6
22
|
if (cached)
|
|
7
23
|
return cached;
|
|
8
24
|
const rpcUrl = process.env.RPC_URL || "https://rpc.sepolia.linea.build";
|
|
9
25
|
const privateKey = process.env.PRIVATE_KEY;
|
|
10
|
-
const brokerAddressRaw = process.env.BROKER_ADDRESS || "0xAc6C93eaBDc3DBE4e1B0176914dc2a16f8Fd1800";
|
|
11
26
|
const chainId = Number(process.env.CHAIN_ID) || 59141;
|
|
12
|
-
const
|
|
27
|
+
const brokerAddressRaw = process.env.BROKER_ADDRESS || getDefaultBrokerByChainId(chainId);
|
|
28
|
+
const quoteTokenRaw = process.env.QUOTE_TOKEN_ADDRESS || getDefaultQuoteTokenByChainId(chainId);
|
|
13
29
|
const quoteDecimals = Number(process.env.QUOTE_TOKEN_DECIMALS) || 6;
|
|
14
30
|
if (!rpcUrl)
|
|
15
31
|
throw new Error("RPC_URL env var is required.");
|
|
@@ -44,7 +60,8 @@ export function getChainId() {
|
|
|
44
60
|
export function getQuoteToken() {
|
|
45
61
|
if (cached)
|
|
46
62
|
return cached.quoteToken;
|
|
47
|
-
const
|
|
63
|
+
const chainId = Number(process.env.CHAIN_ID) || 59141;
|
|
64
|
+
const tokenRaw = process.env.QUOTE_TOKEN_ADDRESS || getDefaultQuoteTokenByChainId(chainId);
|
|
48
65
|
if (!tokenRaw)
|
|
49
66
|
throw new Error("QUOTE_TOKEN_ADDRESS env var is required.");
|
|
50
67
|
return normalizeAddress(tokenRaw, "QUOTE_TOKEN_ADDRESS");
|
|
@@ -3,6 +3,7 @@ import { parseUnits } from "ethers";
|
|
|
3
3
|
import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
4
4
|
import { ensureUnits } from "../utils/units.js";
|
|
5
5
|
import { normalizeAddress } from "../utils/address.js";
|
|
6
|
+
import { normalizeSlippagePct4dp } from "../utils/slippage.js";
|
|
6
7
|
/**
|
|
7
8
|
* 将人类可读数值转为指定精度的整数字串
|
|
8
9
|
* e.g. toUnits("100", 6) => "100000000"
|
|
@@ -45,7 +46,7 @@ export async function openPosition(client, address, args) {
|
|
|
45
46
|
price: ensureUnits(args.price, 30, "price"),
|
|
46
47
|
timeInForce: args.timeInForce,
|
|
47
48
|
postOnly: args.postOnly,
|
|
48
|
-
slippagePct:
|
|
49
|
+
slippagePct: normalizeSlippagePct4dp(args.slippagePct),
|
|
49
50
|
executionFeeToken,
|
|
50
51
|
leverage: args.leverage,
|
|
51
52
|
};
|
|
@@ -91,7 +92,7 @@ export async function closePosition(client, address, args) {
|
|
|
91
92
|
price: ensureUnits(args.price, 30, "price"),
|
|
92
93
|
timeInForce: args.timeInForce,
|
|
93
94
|
postOnly: args.postOnly,
|
|
94
|
-
slippagePct:
|
|
95
|
+
slippagePct: normalizeSlippagePct4dp(args.slippagePct),
|
|
95
96
|
executionFeeToken,
|
|
96
97
|
leverage: args.leverage,
|
|
97
98
|
});
|
|
@@ -116,7 +117,7 @@ export async function setPositionTpSl(client, address, args) {
|
|
|
116
117
|
executionFeeToken,
|
|
117
118
|
tpTriggerType: args.tpTriggerType,
|
|
118
119
|
slTriggerType: args.slTriggerType,
|
|
119
|
-
slippagePct: args.slippagePct,
|
|
120
|
+
slippagePct: normalizeSlippagePct4dp(args.slippagePct),
|
|
120
121
|
};
|
|
121
122
|
if (args.tpPrice)
|
|
122
123
|
params.tpPrice = args.tpPrice;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { normalizeAddress } from "../utils/address.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
4
5
|
export const accountDepositTool = {
|
|
5
6
|
name: "account_deposit",
|
|
6
7
|
description: "Deposit funds from wallet into the MYX trading account.",
|
|
@@ -10,15 +11,16 @@ export const accountDepositTool = {
|
|
|
10
11
|
},
|
|
11
12
|
handler: async (args) => {
|
|
12
13
|
try {
|
|
13
|
-
const { client } = await resolveClient();
|
|
14
|
+
const { client, signer } = await resolveClient();
|
|
14
15
|
const chainId = getChainId();
|
|
15
16
|
const tokenAddress = normalizeAddress(args.tokenAddress, "tokenAddress");
|
|
16
|
-
const
|
|
17
|
+
const raw = await client.account.deposit({
|
|
17
18
|
amount: args.amount,
|
|
18
19
|
tokenAddress,
|
|
19
20
|
chainId,
|
|
20
21
|
});
|
|
21
|
-
|
|
22
|
+
const data = await finalizeMutationResult(raw, signer, "account_deposit");
|
|
23
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
22
24
|
}
|
|
23
25
|
catch (error) {
|
|
24
26
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -35,16 +37,17 @@ export const accountWithdrawTool = {
|
|
|
35
37
|
},
|
|
36
38
|
handler: async (args) => {
|
|
37
39
|
try {
|
|
38
|
-
const { client, address } = await resolveClient();
|
|
40
|
+
const { client, address, signer } = await resolveClient();
|
|
39
41
|
const chainId = getChainId();
|
|
40
|
-
const
|
|
42
|
+
const raw = await client.account.withdraw({
|
|
41
43
|
chainId,
|
|
42
44
|
receiver: address,
|
|
43
45
|
amount: args.amount,
|
|
44
46
|
poolId: args.poolId,
|
|
45
47
|
isQuoteToken: args.isQuoteToken,
|
|
46
48
|
});
|
|
47
|
-
|
|
49
|
+
const data = await finalizeMutationResult(raw, signer, "account_withdraw");
|
|
50
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
48
51
|
}
|
|
49
52
|
catch (error) {
|
|
50
53
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { adjustMargin as adjustMarginSvc } from "../services/tradeService.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
4
5
|
export const adjustMarginTool = {
|
|
5
6
|
name: "adjust_margin",
|
|
6
7
|
description: "Adjust the margin (collateral) of an open position.",
|
|
7
8
|
schema: {
|
|
8
9
|
poolId: z.string().describe("Pool ID"),
|
|
9
10
|
positionId: z.string().describe("Position ID"),
|
|
10
|
-
adjustAmount: z.string().regex(/^-?\d+$/).describe("Quote token raw units. Positive = add, negative = remove."),
|
|
11
|
+
adjustAmount: z.coerce.string().regex(/^-?\d+$/).describe("Quote token raw units. Positive = add, negative = remove."),
|
|
11
12
|
quoteToken: z.string().optional().describe("Quote token address"),
|
|
12
|
-
poolOracleType: z.number().optional().describe("Oracle type: 1 for Chainlink, 2 for Pyth"),
|
|
13
|
+
poolOracleType: z.coerce.number().optional().describe("Oracle type: 1 for Chainlink, 2 for Pyth"),
|
|
13
14
|
},
|
|
14
15
|
handler: async (args) => {
|
|
15
16
|
try {
|
|
16
|
-
const { client, address } = await resolveClient();
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const { client, address, signer } = await resolveClient();
|
|
18
|
+
const raw = await adjustMarginSvc(client, address, args);
|
|
19
|
+
const data = await finalizeMutationResult(raw, signer, "adjust_margin");
|
|
20
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
19
21
|
}
|
|
20
22
|
catch (error) {
|
|
21
23
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
3
4
|
export const cancelAllOrdersTool = {
|
|
4
5
|
name: "cancel_all_orders",
|
|
5
6
|
description: "Cancel multiple open orders by orderIds.",
|
|
@@ -8,10 +9,11 @@ export const cancelAllOrdersTool = {
|
|
|
8
9
|
},
|
|
9
10
|
handler: async (args) => {
|
|
10
11
|
try {
|
|
11
|
-
const { client } = await resolveClient();
|
|
12
|
+
const { client, signer } = await resolveClient();
|
|
12
13
|
const chainId = getChainId();
|
|
13
14
|
const orderIds = args.orderIds;
|
|
14
|
-
const
|
|
15
|
+
const raw = await client.order.cancelAllOrders(orderIds, chainId);
|
|
16
|
+
const result = await finalizeMutationResult(raw, signer, "cancel_all_orders");
|
|
15
17
|
return {
|
|
16
18
|
content: [{ type: "text", text: JSON.stringify({ status: "success", data: { cancelled: orderIds.length, orderIds, result } }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }],
|
|
17
19
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
3
4
|
export const cancelOrderTool = {
|
|
4
5
|
name: "cancel_order",
|
|
5
6
|
description: "Cancel an open order by its order ID.",
|
|
@@ -8,10 +9,11 @@ export const cancelOrderTool = {
|
|
|
8
9
|
},
|
|
9
10
|
handler: async (args) => {
|
|
10
11
|
try {
|
|
11
|
-
const { client } = await resolveClient();
|
|
12
|
+
const { client, signer } = await resolveClient();
|
|
12
13
|
const chainId = getChainId();
|
|
13
|
-
const
|
|
14
|
-
|
|
14
|
+
const raw = await client.order.cancelOrder(args.orderId, chainId);
|
|
15
|
+
const data = await finalizeMutationResult(raw, signer, "cancel_order");
|
|
16
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
15
17
|
}
|
|
16
18
|
catch (error) {
|
|
17
19
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
3
3
|
import { normalizeAddress } from "../utils/address.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
4
5
|
const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
|
|
5
6
|
export const checkApprovalTool = {
|
|
6
7
|
name: "check_approval",
|
|
@@ -13,18 +14,19 @@ export const checkApprovalTool = {
|
|
|
13
14
|
},
|
|
14
15
|
handler: async (args) => {
|
|
15
16
|
try {
|
|
16
|
-
const { client, address } = await resolveClient();
|
|
17
|
+
const { client, address, signer } = await resolveClient();
|
|
17
18
|
const chainId = getChainId();
|
|
18
19
|
const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
|
|
19
20
|
const needApproval = await client.utils.needsApproval(address, chainId, quoteToken, args.amount);
|
|
20
21
|
if (needApproval && args.autoApprove) {
|
|
21
22
|
const approveAmount = args.approveMax ? MAX_UINT256 : args.amount;
|
|
22
|
-
await client.utils.approveAuthorization({
|
|
23
|
+
const raw = await client.utils.approveAuthorization({
|
|
23
24
|
chainId,
|
|
24
25
|
quoteAddress: quoteToken,
|
|
25
26
|
amount: approveAmount,
|
|
26
27
|
});
|
|
27
|
-
|
|
28
|
+
const approval = await finalizeMutationResult(raw, signer, "approve_authorization");
|
|
29
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { needApproval: true, approved: true, quoteToken, approvedAmount: approveAmount, approveMax: !!args.approveMax, approval } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
28
30
|
}
|
|
29
31
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { needApproval, approved: false, quoteToken } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
30
32
|
}
|
|
@@ -2,16 +2,20 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
3
3
|
import { getOraclePrice } from "../services/marketService.js";
|
|
4
4
|
import { ensureUnits } from "../utils/units.js";
|
|
5
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
6
|
+
import { normalizeSlippagePct4dp, isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
5
7
|
export const closeAllPositionsTool = {
|
|
6
8
|
name: "close_all_positions",
|
|
7
9
|
description: "Emergency: close ALL open positions in a pool at once. Use for risk management.",
|
|
8
10
|
schema: {
|
|
9
11
|
poolId: z.string().describe("Pool ID to close all positions in"),
|
|
10
|
-
slippagePct: z.string().
|
|
12
|
+
slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
|
|
13
|
+
message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
|
|
14
|
+
}).optional().describe(SLIPPAGE_PCT_4DP_DESC),
|
|
11
15
|
},
|
|
12
16
|
handler: async (args) => {
|
|
13
17
|
try {
|
|
14
|
-
const { client, address } = await resolveClient();
|
|
18
|
+
const { client, address, signer } = await resolveClient();
|
|
15
19
|
const chainId = getChainId();
|
|
16
20
|
// 1) 先获取该池的所有持仓
|
|
17
21
|
const posResult = await client.position.listPositions(address);
|
|
@@ -24,7 +28,7 @@ export const closeAllPositionsTool = {
|
|
|
24
28
|
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) }] };
|
|
25
29
|
}
|
|
26
30
|
// 2) 为每个仓位构建平仓参数
|
|
27
|
-
const slippagePct = args.slippagePct ?? "200";
|
|
31
|
+
const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "200");
|
|
28
32
|
// 3) We need actual oracle prices to avoid Revert 0x613970e0 (InvalidParameter)
|
|
29
33
|
const oraclePriceReq = await getOraclePrice(client, args.poolId).catch(() => null);
|
|
30
34
|
let fallbackPrice = "0";
|
|
@@ -63,7 +67,8 @@ export const closeAllPositionsTool = {
|
|
|
63
67
|
leverage: pos.userLeverage ?? pos.leverage ?? 1,
|
|
64
68
|
};
|
|
65
69
|
});
|
|
66
|
-
const
|
|
70
|
+
const raw = await client.order.closeAllPositions(chainId, closeParams);
|
|
71
|
+
const result = await finalizeMutationResult(raw, signer, "close_all_positions");
|
|
67
72
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { closed: poolPositions.length, result } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
68
73
|
}
|
|
69
74
|
catch (error) {
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { closePosition as closePos } from "../services/tradeService.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
4
6
|
export const closePositionTool = {
|
|
5
7
|
name: "close_position",
|
|
6
8
|
description: "Create a decrease order using SDK-native parameters.",
|
|
7
9
|
schema: {
|
|
8
10
|
poolId: z.string().describe("Pool ID"),
|
|
9
11
|
positionId: z.string().describe("Position ID to close"),
|
|
10
|
-
orderType: z.number().int().min(0).max(3).describe("OrderType enum value"),
|
|
11
|
-
triggerType: z.number().int().min(0).max(2).describe("TriggerType enum value"),
|
|
12
|
-
direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT"),
|
|
12
|
+
orderType: z.coerce.number().int().min(0).max(3).describe("OrderType enum value"),
|
|
13
|
+
triggerType: z.coerce.number().int().min(0).max(2).describe("TriggerType enum value"),
|
|
14
|
+
direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
|
|
13
15
|
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human-readable or raw units)"),
|
|
14
16
|
size: z.union([z.string(), z.number()]).describe("Position size (human-readable or raw units)"),
|
|
15
17
|
price: z.union([z.string(), z.number()]).describe("Price (human-readable or 30-dec raw units)"),
|
|
16
|
-
timeInForce: z.number().int().describe("TimeInForce enum value"),
|
|
17
|
-
postOnly: z.boolean().describe("Post-only flag"),
|
|
18
|
-
slippagePct: z.
|
|
18
|
+
timeInForce: z.coerce.number().int().describe("TimeInForce enum value"),
|
|
19
|
+
postOnly: z.coerce.boolean().describe("Post-only flag"),
|
|
20
|
+
slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
|
|
21
|
+
message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
|
|
22
|
+
}).describe(SLIPPAGE_PCT_4DP_DESC),
|
|
19
23
|
executionFeeToken: z.string().describe("Execution fee token address"),
|
|
20
|
-
leverage: z.number().describe("Leverage"),
|
|
24
|
+
leverage: z.coerce.number().describe("Leverage"),
|
|
21
25
|
},
|
|
22
26
|
handler: async (args) => {
|
|
23
27
|
try {
|
|
24
|
-
const { client, address } = await resolveClient();
|
|
25
|
-
const
|
|
26
|
-
|
|
28
|
+
const { client, address, signer } = await resolveClient();
|
|
29
|
+
const raw = await closePos(client, address, args);
|
|
30
|
+
const data = await finalizeMutationResult(raw, signer, "close_position");
|
|
31
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
27
32
|
}
|
|
28
33
|
catch (error) {
|
|
29
34
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createPool } from "../services/poolService.js";
|
|
3
|
+
import { resolveClient } from "../auth/resolveClient.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
3
5
|
export const createPerpMarketTool = {
|
|
4
6
|
name: "create_perp_market",
|
|
5
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_list to fetch an existing marketId if you don't have a specific newly allocated one.",
|
|
@@ -11,8 +13,10 @@ export const createPerpMarketTool = {
|
|
|
11
13
|
try {
|
|
12
14
|
if (!args.baseToken || !args.marketId)
|
|
13
15
|
throw new Error("baseToken and marketId are required.");
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
+
const { signer } = await resolveClient();
|
|
17
|
+
const raw = await createPool(args.baseToken, args.marketId);
|
|
18
|
+
const data = await finalizeMutationResult(raw, signer, "create_perp_market");
|
|
19
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
16
20
|
}
|
|
17
21
|
catch (error) {
|
|
18
22
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -1,35 +1,119 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { openPosition } from "../services/tradeService.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
6
|
+
const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
|
|
7
|
+
function toList(input) {
|
|
8
|
+
if (Array.isArray(input))
|
|
9
|
+
return input;
|
|
10
|
+
if (Array.isArray(input?.data))
|
|
11
|
+
return input.data;
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
function buildPositionFingerprint(rows, poolId) {
|
|
15
|
+
const map = new Map();
|
|
16
|
+
for (const row of rows) {
|
|
17
|
+
if (String(row?.poolId ?? row?.pool_id ?? "") !== poolId)
|
|
18
|
+
continue;
|
|
19
|
+
const id = String(row?.positionId ?? row?.position_id ?? "");
|
|
20
|
+
if (!id)
|
|
21
|
+
continue;
|
|
22
|
+
const fingerprint = JSON.stringify({
|
|
23
|
+
size: String(row?.size ?? ""),
|
|
24
|
+
collateralAmount: String(row?.collateralAmount ?? row?.collateral_amount ?? ""),
|
|
25
|
+
txTime: Number(row?.txTime ?? row?.tx_time ?? 0),
|
|
26
|
+
});
|
|
27
|
+
map.set(id, fingerprint);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
}
|
|
31
|
+
function buildOpenOrderSet(rows, poolId) {
|
|
32
|
+
const set = new Set();
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
if (String(row?.poolId ?? row?.pool_id ?? "") !== poolId)
|
|
35
|
+
continue;
|
|
36
|
+
const id = row?.orderId ?? row?.id ?? row?.order_id;
|
|
37
|
+
if (id !== undefined && id !== null)
|
|
38
|
+
set.add(String(id));
|
|
39
|
+
}
|
|
40
|
+
return set;
|
|
41
|
+
}
|
|
42
|
+
async function snapshotTradeState(client, address, poolId) {
|
|
43
|
+
const [ordersRes, positionsRes] = await Promise.all([
|
|
44
|
+
client.order.getOrders(address).catch(() => null),
|
|
45
|
+
client.position.listPositions(address).catch(() => null),
|
|
46
|
+
]);
|
|
47
|
+
return {
|
|
48
|
+
openOrders: buildOpenOrderSet(toList(ordersRes), poolId),
|
|
49
|
+
positions: buildPositionFingerprint(toList(positionsRes), poolId),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function hasTradeEffect(before, after) {
|
|
53
|
+
for (const id of after.openOrders) {
|
|
54
|
+
if (!before.openOrders.has(id))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
for (const [id, fp] of after.positions.entries()) {
|
|
58
|
+
if (!before.positions.has(id))
|
|
59
|
+
return true;
|
|
60
|
+
if (before.positions.get(id) !== fp)
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
async function waitForTradeEffect(client, address, poolId, before) {
|
|
66
|
+
const maxAttempts = 8;
|
|
67
|
+
const intervalMs = 1500;
|
|
68
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
69
|
+
const after = await snapshotTradeState(client, address, poolId);
|
|
70
|
+
if (hasTradeEffect(before, after)) {
|
|
71
|
+
return { detected: true, attempts: i + 1 };
|
|
72
|
+
}
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
74
|
+
}
|
|
75
|
+
return { detected: false, attempts: maxAttempts };
|
|
76
|
+
}
|
|
4
77
|
export const executeTradeTool = {
|
|
5
78
|
name: "execute_trade",
|
|
6
79
|
description: "Create an increase order using SDK-native parameters.",
|
|
7
80
|
schema: {
|
|
8
81
|
poolId: z.string().describe("Pool ID"),
|
|
9
|
-
positionId: z.string().
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
82
|
+
positionId: z.string().refine((value) => POSITION_ID_RE.test(value), {
|
|
83
|
+
message: "positionId must be empty string for new position, or a bytes32 hex string.",
|
|
84
|
+
}).describe("Position ID: empty string for new position"),
|
|
85
|
+
orderType: z.coerce.number().int().min(0).max(3).describe("OrderType enum value"),
|
|
86
|
+
triggerType: z.coerce.number().int().min(0).max(2).describe("TriggerType enum value"),
|
|
87
|
+
direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
|
|
88
|
+
collateralAmount: z.coerce.string().regex(/^\d+$/).describe("Collateral amount (raw units)"),
|
|
89
|
+
size: z.coerce.string().regex(/^\d+$/).describe("Position size (raw units)"),
|
|
90
|
+
price: z.coerce.string().regex(/^\d+$/).describe("Price (30-decimal raw units)"),
|
|
91
|
+
timeInForce: z.coerce.number().int().describe("TimeInForce enum value"),
|
|
92
|
+
postOnly: z.coerce.boolean().describe("Post-only flag"),
|
|
93
|
+
slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
|
|
94
|
+
message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
|
|
95
|
+
}).describe(SLIPPAGE_PCT_4DP_DESC),
|
|
19
96
|
executionFeeToken: z.string().describe("Execution fee token address"),
|
|
20
|
-
leverage: z.number().describe("Leverage"),
|
|
21
|
-
tpSize: z.
|
|
22
|
-
tpPrice: z.
|
|
23
|
-
slSize: z.
|
|
24
|
-
slPrice: z.
|
|
25
|
-
tradingFee: z.string().regex(/^\d+$/).describe("Trading fee raw units"),
|
|
97
|
+
leverage: z.coerce.number().describe("Leverage"),
|
|
98
|
+
tpSize: z.coerce.string().regex(/^\d+$/).optional().describe("TP size raw units"),
|
|
99
|
+
tpPrice: z.coerce.string().regex(/^\d+$/).optional().describe("TP price raw units (30 decimals)"),
|
|
100
|
+
slSize: z.coerce.string().regex(/^\d+$/).optional().describe("SL size raw units"),
|
|
101
|
+
slPrice: z.coerce.string().regex(/^\d+$/).optional().describe("SL price raw units (30 decimals)"),
|
|
102
|
+
tradingFee: z.coerce.string().regex(/^\d+$/).describe("Trading fee raw units"),
|
|
26
103
|
marketId: z.string().describe("Market ID"),
|
|
27
104
|
},
|
|
28
105
|
handler: async (args) => {
|
|
29
106
|
try {
|
|
30
|
-
const { client, address } = await resolveClient();
|
|
31
|
-
const
|
|
32
|
-
|
|
107
|
+
const { client, address, signer } = await resolveClient();
|
|
108
|
+
const before = await snapshotTradeState(client, address, args.poolId);
|
|
109
|
+
const raw = await openPosition(client, address, args);
|
|
110
|
+
const data = await finalizeMutationResult(raw, signer, "execute_trade");
|
|
111
|
+
const effect = await waitForTradeEffect(client, address, args.poolId, before);
|
|
112
|
+
if (!effect.detected) {
|
|
113
|
+
throw new Error(`execute_trade tx confirmed but no order/position change detected in ${effect.attempts} checks.`);
|
|
114
|
+
}
|
|
115
|
+
const payload = { ...data, effect };
|
|
116
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v) }] };
|
|
33
117
|
}
|
|
34
118
|
catch (error) {
|
|
35
119
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -1,5 +1,7 @@
|
|
|
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";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
3
5
|
export const manageLiquidityTool = {
|
|
4
6
|
name: "manage_liquidity",
|
|
5
7
|
description: "Add or withdraw liquidity from a BASE or QUOTE pool.",
|
|
@@ -13,20 +15,22 @@ export const manageLiquidityTool = {
|
|
|
13
15
|
},
|
|
14
16
|
handler: async (args) => {
|
|
15
17
|
try {
|
|
18
|
+
const { signer } = await resolveClient();
|
|
16
19
|
const { action, poolType, poolId } = args;
|
|
17
20
|
const { amount, slippage } = args;
|
|
18
|
-
let
|
|
21
|
+
let raw;
|
|
19
22
|
if (poolType === "QUOTE") {
|
|
20
|
-
|
|
23
|
+
raw = action === "deposit"
|
|
21
24
|
? await quoteDeposit(poolId, amount, slippage, args.chainId)
|
|
22
25
|
: await quoteWithdraw(poolId, amount, slippage, args.chainId);
|
|
23
26
|
}
|
|
24
27
|
else {
|
|
25
|
-
|
|
28
|
+
raw = action === "deposit"
|
|
26
29
|
? await baseDeposit(poolId, amount, slippage, args.chainId)
|
|
27
30
|
: await baseWithdraw(poolId, amount, slippage, args.chainId);
|
|
28
31
|
}
|
|
29
|
-
|
|
32
|
+
const data = await finalizeMutationResult(raw, signer, "manage_liquidity");
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
30
34
|
}
|
|
31
35
|
catch (error) {
|
|
32
36
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
package/dist/tools/setTpSl.js
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { setPositionTpSl } from "../services/tradeService.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
4
6
|
export const setTpSlTool = {
|
|
5
7
|
name: "set_tp_sl",
|
|
6
8
|
description: "Create TP/SL order using SDK-native parameters.",
|
|
7
9
|
schema: {
|
|
8
10
|
poolId: z.string().describe("Pool ID"),
|
|
9
11
|
positionId: z.string().describe("Position ID"),
|
|
10
|
-
direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT"),
|
|
11
|
-
leverage: z.number().describe("Leverage"),
|
|
12
|
+
direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
|
|
13
|
+
leverage: z.coerce.number().describe("Leverage"),
|
|
12
14
|
executionFeeToken: z.string().describe("Execution fee token address"),
|
|
13
|
-
tpTriggerType: z.number().int().min(0).max(2).describe("TP trigger type"),
|
|
14
|
-
slTriggerType: z.number().int().min(0).max(2).describe("SL trigger type"),
|
|
15
|
-
slippagePct: z.string().
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
tpTriggerType: z.coerce.number().int().min(0).max(2).describe("TP trigger type"),
|
|
16
|
+
slTriggerType: z.coerce.number().int().min(0).max(2).describe("SL trigger type"),
|
|
17
|
+
slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
|
|
18
|
+
message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
|
|
19
|
+
}).describe(SLIPPAGE_PCT_4DP_DESC),
|
|
20
|
+
tpPrice: z.coerce.string().regex(/^\d+$/).optional().describe("TP price raw units (30 decimals)"),
|
|
21
|
+
tpSize: z.coerce.string().regex(/^\d+$/).optional().describe("TP size raw units"),
|
|
22
|
+
slPrice: z.coerce.string().regex(/^\d+$/).optional().describe("SL price raw units (30 decimals)"),
|
|
23
|
+
slSize: z.coerce.string().regex(/^\d+$/).optional().describe("SL size raw units"),
|
|
20
24
|
},
|
|
21
25
|
handler: async (args) => {
|
|
22
26
|
try {
|
|
23
|
-
const { client, address } = await resolveClient();
|
|
24
|
-
const
|
|
25
|
-
|
|
27
|
+
const { client, address, signer } = await resolveClient();
|
|
28
|
+
const raw = await setPositionTpSl(client, address, args);
|
|
29
|
+
const data = await finalizeMutationResult(raw, signer, "set_tp_sl");
|
|
30
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
26
31
|
}
|
|
27
32
|
catch (error) {
|
|
28
33
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { updateOrderTpSl } from "../services/tradeService.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
4
5
|
export const updateOrderTpSlTool = {
|
|
5
6
|
name: "update_order_tp_sl",
|
|
6
7
|
description: "Update an existing take profit or stop loss order.",
|
|
@@ -19,9 +20,10 @@ export const updateOrderTpSlTool = {
|
|
|
19
20
|
},
|
|
20
21
|
handler: async (args) => {
|
|
21
22
|
try {
|
|
22
|
-
const { client, address } = await resolveClient();
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const { client, address, signer } = await resolveClient();
|
|
24
|
+
const raw = await updateOrderTpSl(client, address, args);
|
|
25
|
+
const data = await finalizeMutationResult(raw, signer, "update_order_tp_sl");
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
25
27
|
}
|
|
26
28
|
catch (error) {
|
|
27
29
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
2
|
+
const TX_HASH_KEYS = new Set(["hash", "txHash", "transactionHash"]);
|
|
3
|
+
function isObject(value) {
|
|
4
|
+
return !!value && typeof value === "object";
|
|
5
|
+
}
|
|
6
|
+
function findTxHashDeep(input, depth = 0) {
|
|
7
|
+
if (!isObject(input) || depth > 4)
|
|
8
|
+
return undefined;
|
|
9
|
+
for (const [key, value] of Object.entries(input)) {
|
|
10
|
+
if (TX_HASH_KEYS.has(key) && typeof value === "string" && TX_HASH_RE.test(value)) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
for (const value of Object.values(input)) {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const item of value) {
|
|
17
|
+
const found = findTxHashDeep(item, depth + 1);
|
|
18
|
+
if (found)
|
|
19
|
+
return found;
|
|
20
|
+
}
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const found = findTxHashDeep(value, depth + 1);
|
|
24
|
+
if (found)
|
|
25
|
+
return found;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function assertSdkCode(result, actionName) {
|
|
30
|
+
if (!isObject(result))
|
|
31
|
+
return;
|
|
32
|
+
if (!Object.prototype.hasOwnProperty.call(result, "code"))
|
|
33
|
+
return;
|
|
34
|
+
const code = Number(result.code);
|
|
35
|
+
if (!Number.isFinite(code)) {
|
|
36
|
+
throw new Error(`${actionName} failed: invalid SDK code.`);
|
|
37
|
+
}
|
|
38
|
+
if (code !== 0) {
|
|
39
|
+
const msg = result.msg ?? result.message ?? "unknown error";
|
|
40
|
+
throw new Error(`${actionName} failed: code=${code}, msg=${String(msg)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function finalizeMutationResult(result, signer, actionName) {
|
|
44
|
+
assertSdkCode(result, actionName);
|
|
45
|
+
const txHash = findTxHashDeep(result);
|
|
46
|
+
if (!txHash) {
|
|
47
|
+
return { result };
|
|
48
|
+
}
|
|
49
|
+
const provider = signer?.provider;
|
|
50
|
+
if (!provider?.waitForTransaction) {
|
|
51
|
+
return { result, confirmation: { txHash, status: "submitted" } };
|
|
52
|
+
}
|
|
53
|
+
const receipt = await provider.waitForTransaction(txHash, 1, 120000);
|
|
54
|
+
if (!receipt) {
|
|
55
|
+
throw new Error(`${actionName} failed: tx not confirmed within timeout (${txHash}).`);
|
|
56
|
+
}
|
|
57
|
+
if (receipt.status !== 1) {
|
|
58
|
+
throw new Error(`${actionName} failed on-chain: tx reverted (${txHash}).`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
result,
|
|
62
|
+
confirmation: {
|
|
63
|
+
txHash,
|
|
64
|
+
blockNumber: receipt.blockNumber,
|
|
65
|
+
status: receipt.status,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const SLIPPAGE_PCT_4DP_RE = /^\d+$/;
|
|
2
|
+
export const SLIPPAGE_PCT_4DP_MAX = 10000n;
|
|
3
|
+
export const SLIPPAGE_PCT_4DP_DESC = "Slippage in 4-decimal precision raw units (1 = 0.01%, 10000 = 100%)";
|
|
4
|
+
export function isValidSlippagePct4dp(value) {
|
|
5
|
+
if (!SLIPPAGE_PCT_4DP_RE.test(value))
|
|
6
|
+
return false;
|
|
7
|
+
return BigInt(value) <= SLIPPAGE_PCT_4DP_MAX;
|
|
8
|
+
}
|
|
9
|
+
export function normalizeSlippagePct4dp(value, label = "slippagePct") {
|
|
10
|
+
const raw = String(value ?? "").trim();
|
|
11
|
+
if (!isValidSlippagePct4dp(raw)) {
|
|
12
|
+
throw new Error(`${label} must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).`);
|
|
13
|
+
}
|
|
14
|
+
return raw;
|
|
15
|
+
}
|