@michaleffffff/mcp-trading-server 3.0.1 → 3.0.2
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 +12 -0
- package/README.md +7 -1
- package/TOOL_EXAMPLES.md +5 -1
- package/dist/prompts/tradingGuide.js +2 -0
- package/dist/server.js +115 -14
- package/dist/services/tradeService.js +30 -23
- package/dist/tools/accountTransfer.js +2 -2
- package/dist/tools/adjustMargin.js +8 -2
- package/dist/tools/executeTrade.js +104 -13
- package/dist/utils/units.js +12 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.2 - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Integrated `verifyTradeOutcome` in `execute_trade` for post-transaction state validation.
|
|
7
|
+
- Automatic `tradingFee` calculation in `execute_trade` using `getUserTradingFeeRate`.
|
|
8
|
+
- `parseUserUnits` / `parseUserPrice30` utilities for more consistent human/raw input handling.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `execute_trade` now surfaces `preflight` normalization details in the successful response.
|
|
12
|
+
- `adjust_margin` now supports displaying `__normalized` data.
|
|
13
|
+
- Refined unit resolution in `account_deposit` and `account_withdraw`.
|
|
14
|
+
|
|
3
15
|
## 3.0.1 - 2026-03-17
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -291,7 +291,7 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
|
|
|
291
291
|
|
|
292
292
|
### Trading Operations
|
|
293
293
|
* **open_position_simple**: High-level open position helper (recommended). Computes price/size/tradingFee internally.
|
|
294
|
-
* **execute_trade**: Execute a new trade or add to an existing position.
|
|
294
|
+
* **execute_trade**: Execute a new trade or add to an existing position (includes parameter preflight, supports `human:`/`raw:` amount prefixes, auto-computes `tradingFee` when omitted).
|
|
295
295
|
* **close_position**: Close an open position.
|
|
296
296
|
* **close_all_positions**: Emergency: close ALL open positions in a pool at once.
|
|
297
297
|
* **cancel_order**: Cancel an open order by its order ID.
|
|
@@ -377,6 +377,12 @@ Use this query/mutation loop for limit-order management:
|
|
|
377
377
|
4. Check historical fills/cancellations with `get_order_history`.
|
|
378
378
|
5. Cancel one order via `cancel_order` or batch via `cancel_all_orders`.
|
|
379
379
|
|
|
380
|
+
## Unit Safety (P0)
|
|
381
|
+
|
|
382
|
+
- Monetary fields in major mutation tools support `human:` and `raw:` prefixes.
|
|
383
|
+
- Default behavior is human-readable units (e.g. `1` means `1.0` token), while `raw:<int>` enforces exact raw units.
|
|
384
|
+
- `execute_trade` and `adjust_margin` responses include normalized raw values for auditability.
|
|
385
|
+
|
|
380
386
|
Minimal query examples:
|
|
381
387
|
|
|
382
388
|
```json
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -80,11 +80,14 @@ It includes:
|
|
|
80
80
|
"slippagePct": "100",
|
|
81
81
|
"executionFeeToken": "0xQUOTE_TOKEN",
|
|
82
82
|
"leverage": 5,
|
|
83
|
-
"tradingFee": "raw:100000",
|
|
84
83
|
"marketId": "0xMARKET_ID"
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
```
|
|
87
|
+
Notes:
|
|
88
|
+
- `tradingFee` is optional (auto-computed if omitted).
|
|
89
|
+
- Amount-like fields support `human:` and `raw:` prefixes.
|
|
90
|
+
- `positionId: "0x000...000"` is auto-treated as a new position.
|
|
88
91
|
|
|
89
92
|
### `close_position`
|
|
90
93
|
```json
|
|
@@ -234,6 +237,7 @@ Raw precision mode:
|
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
239
|
```
|
|
240
|
+
Success payload includes normalized raw amount under `data.normalized.adjustAmountRaw`.
|
|
237
241
|
|
|
238
242
|
### `check_approval`
|
|
239
243
|
```json
|
|
@@ -26,8 +26,10 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
26
26
|
## 2. Parameter Tips
|
|
27
27
|
- **Position IDs**: When opening a NEW position, \`positionId\` MUST be an empty string \`""\`.
|
|
28
28
|
- **Decimals**: Human-readable units (e.g., "0.1" BTC) are default for \`open_position_simple\`. SDK-native tools often require raw units; use the \`raw:\` prefix if you need forced precision.
|
|
29
|
+
- **Unit Prefixes**: For mutation tools, prefer explicit prefixes: \`human:\` for readable amounts, \`raw:\` for exact on-chain units.
|
|
29
30
|
- **Slippage**: Default is 100 (1%). For volatile meme tokens, consider 200-300 (2-3%).
|
|
30
31
|
- **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
|
|
32
|
+
- **execute_trade**: Has built-in preflight normalization and can auto-compute \`tradingFee\` when omitted.
|
|
31
33
|
- **Balances**: Use \`get_account\` to clearly separate wallet balance vs trading-account balance (pass \`poolId\` for trading-account metrics).
|
|
32
34
|
- **Adjust Margin**: \`adjust_margin.adjustAmount\` supports human amount (e.g. "1"), and also supports exact raw amount via \`raw:\` prefix.
|
|
33
35
|
- **TP/SL Updates**: \`update_order_tp_sl\` accepts boolean-like values for \`useOrderCollateral\` and \`isTpSlOrder\`, but pending LIMIT/STOP orders can still be rejected by protocol rules; after fill, prefer \`set_tp_sl\`.
|
package/dist/server.js
CHANGED
|
@@ -103,6 +103,102 @@ function parseFirstText(result) {
|
|
|
103
103
|
const firstText = result.content.find((item) => item?.type === "text" && typeof item.text === "string");
|
|
104
104
|
return firstText?.text ?? null;
|
|
105
105
|
}
|
|
106
|
+
function unwrapSchema(schema) {
|
|
107
|
+
let current = schema;
|
|
108
|
+
for (let i = 0; i < 8; i++) {
|
|
109
|
+
const type = current?._def?.type;
|
|
110
|
+
if (!type)
|
|
111
|
+
break;
|
|
112
|
+
if (type === "optional" || type === "nullable" || type === "default" || type === "prefault" || type === "catch") {
|
|
113
|
+
current = current?._def?.innerType;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (type === "pipe") {
|
|
117
|
+
current = current?._def?.out;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
function coerceBooleanString(input) {
|
|
125
|
+
if (typeof input !== "string")
|
|
126
|
+
return input;
|
|
127
|
+
const normalized = input.trim().toLowerCase();
|
|
128
|
+
if (normalized === "true" || normalized === "1")
|
|
129
|
+
return true;
|
|
130
|
+
if (normalized === "false" || normalized === "0")
|
|
131
|
+
return false;
|
|
132
|
+
return input;
|
|
133
|
+
}
|
|
134
|
+
function coerceStringToStringArray(input) {
|
|
135
|
+
if (Array.isArray(input))
|
|
136
|
+
return input.map((item) => String(item).trim()).filter(Boolean);
|
|
137
|
+
if (typeof input !== "string")
|
|
138
|
+
return input;
|
|
139
|
+
const text = input.trim();
|
|
140
|
+
if (!text)
|
|
141
|
+
return input;
|
|
142
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(text);
|
|
145
|
+
if (Array.isArray(parsed)) {
|
|
146
|
+
return parsed.map((item) => String(item).trim()).filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (text.includes(",")) {
|
|
153
|
+
const split = text.split(",").map((item) => item.trim()).filter(Boolean);
|
|
154
|
+
if (split.length > 0)
|
|
155
|
+
return split;
|
|
156
|
+
}
|
|
157
|
+
return input;
|
|
158
|
+
}
|
|
159
|
+
function coerceBySchema(value, schema) {
|
|
160
|
+
const unwrapped = unwrapSchema(schema);
|
|
161
|
+
const type = unwrapped?._def?.type;
|
|
162
|
+
if (type === "boolean") {
|
|
163
|
+
return coerceBooleanString(value);
|
|
164
|
+
}
|
|
165
|
+
if (type === "array") {
|
|
166
|
+
const elementType = unwrapSchema(unwrapped?._def?.element)?._def?.type;
|
|
167
|
+
if (elementType === "string") {
|
|
168
|
+
return coerceStringToStringArray(value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (type === "union") {
|
|
172
|
+
const options = Array.isArray(unwrapped?._def?.options) ? unwrapped._def.options : [];
|
|
173
|
+
for (const option of options) {
|
|
174
|
+
const optionType = unwrapSchema(option)?._def?.type;
|
|
175
|
+
if (optionType === "boolean") {
|
|
176
|
+
const coerced = coerceBooleanString(value);
|
|
177
|
+
if (typeof coerced === "boolean")
|
|
178
|
+
return coerced;
|
|
179
|
+
}
|
|
180
|
+
if (optionType === "array") {
|
|
181
|
+
const coerced = coerceStringToStringArray(value);
|
|
182
|
+
if (Array.isArray(coerced))
|
|
183
|
+
return coerced;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
function normalizeToolArgsBySchema(rawArgs, schema) {
|
|
190
|
+
const source = rawArgs && typeof rawArgs === "object" ? { ...rawArgs } : {};
|
|
191
|
+
const normalized = {};
|
|
192
|
+
for (const key of Object.keys(source)) {
|
|
193
|
+
normalized[key] = source[key];
|
|
194
|
+
}
|
|
195
|
+
for (const [key, fieldSchema] of Object.entries(schema)) {
|
|
196
|
+
if (!Object.prototype.hasOwnProperty.call(source, key))
|
|
197
|
+
continue;
|
|
198
|
+
normalized[key] = coerceBySchema(source[key], fieldSchema);
|
|
199
|
+
}
|
|
200
|
+
return normalized;
|
|
201
|
+
}
|
|
106
202
|
function normalizeToolErrorResult(toolName, result) {
|
|
107
203
|
const rawText = parseFirstText(result);
|
|
108
204
|
if (!rawText) {
|
|
@@ -208,27 +304,30 @@ function normalizeSdkReadFailure(toolName, result) {
|
|
|
208
304
|
function zodSchemaToJsonSchema(zodSchema) {
|
|
209
305
|
const toPropSchema = (value) => {
|
|
210
306
|
const def = value?._def;
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
307
|
+
const zodType = def?.type;
|
|
308
|
+
if (zodType === "optional" || zodType === "nullable" || zodType === "default" || zodType === "prefault" || zodType === "catch") {
|
|
213
309
|
return toPropSchema(def?.innerType);
|
|
214
310
|
}
|
|
215
|
-
if (
|
|
311
|
+
if (zodType === "pipe") {
|
|
312
|
+
return toPropSchema(def?.out);
|
|
313
|
+
}
|
|
314
|
+
if (zodType === "string")
|
|
216
315
|
return { type: "string" };
|
|
217
|
-
if (
|
|
316
|
+
if (zodType === "number")
|
|
218
317
|
return { type: "number" };
|
|
219
|
-
if (
|
|
318
|
+
if (zodType === "boolean")
|
|
220
319
|
return { type: "boolean" };
|
|
221
|
-
if (
|
|
222
|
-
return { type: "array", items: toPropSchema(def?.
|
|
320
|
+
if (zodType === "array") {
|
|
321
|
+
return { type: "array", items: toPropSchema(def?.element) };
|
|
223
322
|
}
|
|
224
|
-
if (
|
|
323
|
+
if (zodType === "literal") {
|
|
225
324
|
return { const: def?.value };
|
|
226
325
|
}
|
|
227
|
-
if (
|
|
326
|
+
if (zodType === "union") {
|
|
228
327
|
const options = def?.options || [];
|
|
229
328
|
return { anyOf: options.map((opt) => toPropSchema(opt)) };
|
|
230
329
|
}
|
|
231
|
-
if (
|
|
330
|
+
if (zodType === "object") {
|
|
232
331
|
return { type: "object" };
|
|
233
332
|
}
|
|
234
333
|
return { type: "string" };
|
|
@@ -237,7 +336,8 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
237
336
|
const required = [];
|
|
238
337
|
for (const [key, value] of Object.entries(zodSchema)) {
|
|
239
338
|
const desc = value?._def?.description || "";
|
|
240
|
-
const
|
|
339
|
+
const fieldType = value?._def?.type;
|
|
340
|
+
const isOptional = value?.isOptional?.() || fieldType === "optional" || fieldType === "default";
|
|
241
341
|
const prop = toPropSchema(value);
|
|
242
342
|
if (desc)
|
|
243
343
|
prop.description = desc;
|
|
@@ -252,7 +352,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
252
352
|
};
|
|
253
353
|
}
|
|
254
354
|
// ─── MCP Server ───
|
|
255
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.
|
|
355
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
256
356
|
// List tools
|
|
257
357
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
258
358
|
return {
|
|
@@ -279,8 +379,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
279
379
|
try {
|
|
280
380
|
let validatedArgs = args ?? {};
|
|
281
381
|
if (tool.schema) {
|
|
382
|
+
const normalizedArgs = normalizeToolArgsBySchema(args ?? {}, tool.schema);
|
|
282
383
|
try {
|
|
283
|
-
validatedArgs = z.object(tool.schema).strict().parse(
|
|
384
|
+
validatedArgs = z.object(tool.schema).strict().parse(normalizedArgs);
|
|
284
385
|
}
|
|
285
386
|
catch (validationError) {
|
|
286
387
|
if (validationError instanceof ZodError) {
|
|
@@ -372,7 +473,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
372
473
|
async function main() {
|
|
373
474
|
const transport = new StdioServerTransport();
|
|
374
475
|
await server.connect(transport);
|
|
375
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
476
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.2 running (stdio, pure on-chain, prod ready)");
|
|
376
477
|
}
|
|
377
478
|
main().catch((err) => {
|
|
378
479
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -136,7 +136,7 @@ export async function openPosition(client, address, args) {
|
|
|
136
136
|
}
|
|
137
137
|
const baseDecimals = poolData.baseDecimals || 18;
|
|
138
138
|
const quoteDecimals = poolData.quoteDecimals || 6;
|
|
139
|
-
const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
139
|
+
const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount", { allowImplicitRaw: false });
|
|
140
140
|
// --- Pre-flight Check: minOrderSizeInUsd ---
|
|
141
141
|
try {
|
|
142
142
|
const levelRes = await client.markets.getPoolLevelConfig(args.poolId, chainId);
|
|
@@ -163,9 +163,9 @@ export async function openPosition(client, address, args) {
|
|
|
163
163
|
throw e;
|
|
164
164
|
console.warn(`[tradeService] Limit check skipped: ${e.message}`);
|
|
165
165
|
}
|
|
166
|
-
const sizeRaw = ensureUnits(args.size, baseDecimals, "size");
|
|
167
|
-
const priceRaw = ensureUnits(args.price, 30, "price");
|
|
168
|
-
const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee");
|
|
166
|
+
const sizeRaw = ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false });
|
|
167
|
+
const priceRaw = ensureUnits(args.price, 30, "price", { allowImplicitRaw: false });
|
|
168
|
+
const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee", { allowImplicitRaw: false });
|
|
169
169
|
// --- Auto-Deposit Logic (Strict) ---
|
|
170
170
|
const allowAutoDeposit = args.autoDeposit !== false;
|
|
171
171
|
console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
|
|
@@ -227,13 +227,13 @@ export async function openPosition(client, address, args) {
|
|
|
227
227
|
leverage: args.leverage,
|
|
228
228
|
};
|
|
229
229
|
if (args.tpSize)
|
|
230
|
-
orderParams.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize");
|
|
230
|
+
orderParams.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false });
|
|
231
231
|
if (args.tpPrice)
|
|
232
|
-
orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
|
|
232
|
+
orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false });
|
|
233
233
|
if (args.slSize)
|
|
234
|
-
orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize");
|
|
234
|
+
orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false });
|
|
235
235
|
if (args.slPrice)
|
|
236
|
-
orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
|
|
236
|
+
orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false });
|
|
237
237
|
return client.order.createIncreaseOrder(orderParams, tradingFeeRaw, args.marketId);
|
|
238
238
|
}
|
|
239
239
|
/**
|
|
@@ -263,9 +263,9 @@ export async function closePosition(client, address, args) {
|
|
|
263
263
|
orderType: args.orderType,
|
|
264
264
|
triggerType: resolveTriggerType(args.orderType, args.direction, true, args.triggerType),
|
|
265
265
|
direction: dir,
|
|
266
|
-
collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount"),
|
|
267
|
-
size: ensureUnits(args.size, baseDecimals, "size"),
|
|
268
|
-
price: ensureUnits(args.price, 30, "price"),
|
|
266
|
+
collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount", { allowImplicitRaw: false }),
|
|
267
|
+
size: ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false }),
|
|
268
|
+
price: ensureUnits(args.price, 30, "price", { allowImplicitRaw: false }),
|
|
269
269
|
timeInForce: args.timeInForce,
|
|
270
270
|
postOnly: args.postOnly,
|
|
271
271
|
slippagePct: normalizeSlippagePct4dp(args.slippagePct),
|
|
@@ -303,13 +303,13 @@ export async function setPositionTpSl(client, address, args) {
|
|
|
303
303
|
slippagePct: normalizeSlippagePct4dp(args.slippagePct),
|
|
304
304
|
};
|
|
305
305
|
if (args.tpPrice)
|
|
306
|
-
params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
|
|
306
|
+
params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false });
|
|
307
307
|
if (args.tpSize)
|
|
308
|
-
params.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize");
|
|
308
|
+
params.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false });
|
|
309
309
|
if (args.slPrice)
|
|
310
|
-
params.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
|
|
310
|
+
params.slPrice = ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false });
|
|
311
311
|
if (args.slSize)
|
|
312
|
-
params.slSize = ensureUnits(args.slSize, baseDecimals, "slSize");
|
|
312
|
+
params.slSize = ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false });
|
|
313
313
|
return client.order.createPositionTpSlOrder(params);
|
|
314
314
|
}
|
|
315
315
|
/**
|
|
@@ -334,7 +334,7 @@ export async function adjustMargin(client, address, args) {
|
|
|
334
334
|
catch {
|
|
335
335
|
// Fallback to env quote decimals if market detail is unavailable.
|
|
336
336
|
}
|
|
337
|
-
const adjustAmount = ensureUnits(adjustAmountInput, quoteDecimals, "adjustAmount");
|
|
337
|
+
const adjustAmount = ensureUnits(adjustAmountInput, quoteDecimals, "adjustAmount", { allowImplicitRaw: false });
|
|
338
338
|
if (!/^-?\d+$/.test(adjustAmount)) {
|
|
339
339
|
throw new Error("adjustAmount must resolve to an integer string (raw units).");
|
|
340
340
|
}
|
|
@@ -349,7 +349,14 @@ export async function adjustMargin(client, address, args) {
|
|
|
349
349
|
if (args.poolOracleType !== undefined) {
|
|
350
350
|
params.poolOracleType = Number(args.poolOracleType);
|
|
351
351
|
}
|
|
352
|
-
|
|
352
|
+
const result = await client.position.adjustCollateral(params);
|
|
353
|
+
if (result && typeof result === "object") {
|
|
354
|
+
result.__normalized = {
|
|
355
|
+
adjustAmountRaw: adjustAmount,
|
|
356
|
+
quoteToken,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
353
360
|
}
|
|
354
361
|
/**
|
|
355
362
|
* 平掉所有仓位
|
|
@@ -409,14 +416,14 @@ export async function updateOrderTpSl(client, address, args) {
|
|
|
409
416
|
const { baseDecimals } = await resolveDecimalsForUpdateOrder(client, chainId, marketId, args.poolId);
|
|
410
417
|
const params = {
|
|
411
418
|
orderId: args.orderId,
|
|
412
|
-
tpSize: ensureUnits(args.tpSize, baseDecimals, "tpSize"),
|
|
413
|
-
tpPrice: ensureUnits(args.tpPrice, 30, "tpPrice"),
|
|
414
|
-
slSize: ensureUnits(args.slSize, baseDecimals, "slSize"),
|
|
415
|
-
slPrice: ensureUnits(args.slPrice, 30, "slPrice"),
|
|
419
|
+
tpSize: ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false }),
|
|
420
|
+
tpPrice: ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false }),
|
|
421
|
+
slSize: ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false }),
|
|
422
|
+
slPrice: ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false }),
|
|
416
423
|
useOrderCollateral: Boolean(args.useOrderCollateral),
|
|
417
424
|
executionFeeToken: quoteToken,
|
|
418
|
-
size: ensureUnits(args.size, baseDecimals, "size"),
|
|
419
|
-
price: ensureUnits(args.price, 30, "price"),
|
|
425
|
+
size: ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false }),
|
|
426
|
+
price: ensureUnits(args.price, 30, "price", { allowImplicitRaw: false }),
|
|
420
427
|
};
|
|
421
428
|
try {
|
|
422
429
|
const result = await client.order.updateOrderTpSl(params, quoteToken, chainId, address, marketId, isTpSlOrder);
|
|
@@ -18,7 +18,7 @@ export const accountDepositTool = {
|
|
|
18
18
|
// ensureUnits handles 'raw:' prefix if absolute precision is needed.
|
|
19
19
|
const { ensureUnits } = await import("../utils/units.js");
|
|
20
20
|
const { getQuoteDecimals } = await import("../auth/resolveClient.js");
|
|
21
|
-
const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount");
|
|
21
|
+
const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount", { allowImplicitRaw: false });
|
|
22
22
|
const raw = await client.account.deposit({
|
|
23
23
|
amount,
|
|
24
24
|
tokenAddress,
|
|
@@ -48,7 +48,7 @@ export const accountWithdrawTool = {
|
|
|
48
48
|
const { getQuoteDecimals } = await import("../auth/resolveClient.js");
|
|
49
49
|
// Assuming 18 decimals for base and quoteDecimals for quote
|
|
50
50
|
const decimals = args.isQuoteToken ? getQuoteDecimals() : 18;
|
|
51
|
-
const amount = ensureUnits(args.amount, decimals, "amount");
|
|
51
|
+
const amount = ensureUnits(args.amount, decimals, "amount", { allowImplicitRaw: false });
|
|
52
52
|
const raw = await client.account.withdraw({
|
|
53
53
|
chainId,
|
|
54
54
|
receiver: address,
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { adjustMargin as adjustMarginSvc } from "../services/tradeService.js";
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
5
6
|
export const adjustMarginTool = {
|
|
6
7
|
name: "adjust_margin",
|
|
7
8
|
description: "Adjust the margin (collateral) of an open position.",
|
|
@@ -16,11 +17,16 @@ export const adjustMarginTool = {
|
|
|
16
17
|
try {
|
|
17
18
|
const { client, address, signer } = await resolveClient();
|
|
18
19
|
const raw = await adjustMarginSvc(client, address, args);
|
|
20
|
+
const normalized = raw?.__normalized;
|
|
21
|
+
if (raw && typeof raw === "object" && "__normalized" in raw) {
|
|
22
|
+
delete raw.__normalized;
|
|
23
|
+
}
|
|
19
24
|
const data = await finalizeMutationResult(raw, signer, "adjust_margin");
|
|
20
|
-
|
|
25
|
+
const payload = normalized ? { ...data, normalized } : data;
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
21
27
|
}
|
|
22
28
|
catch (error) {
|
|
23
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
29
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
24
30
|
}
|
|
25
31
|
},
|
|
26
32
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { resolveClient } from "../auth/resolveClient.js";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
import { openPosition } from "../services/tradeService.js";
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
-
import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
5
|
+
import { normalizeSlippagePct4dp, 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 { extractErrorMessage } from "../utils/errorMessage.js";
|
|
9
|
+
import { parseUserUnits } from "../utils/units.js";
|
|
8
10
|
const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
|
|
11
|
+
const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
|
|
9
12
|
export const executeTradeTool = {
|
|
10
13
|
name: "execute_trade",
|
|
11
14
|
description: "Create an increase order using SDK-native parameters.",
|
|
@@ -13,7 +16,7 @@ export const executeTradeTool = {
|
|
|
13
16
|
poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via get_pool_list."),
|
|
14
17
|
positionId: z.string().refine((value) => POSITION_ID_RE.test(value), {
|
|
15
18
|
message: "positionId must be empty string for new position, or a bytes32 hex string.",
|
|
16
|
-
}).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones."),
|
|
19
|
+
}).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones. 0x000..00 is auto-treated as NEW."),
|
|
17
20
|
orderType: z.union([z.number(), z.string()]).describe("Market/Limit/Stop. e.g. 0 or 'MARKET'."),
|
|
18
21
|
triggerType: z.union([z.number(), z.string()]).optional().describe("0=None (Market), 1=GTE, 2=LTE. e.g. 'GTE'."),
|
|
19
22
|
direction: z.union([z.number(), z.string()]).describe("0/LONG/BUY or 1/SHORT/SELL."),
|
|
@@ -24,33 +27,105 @@ export const executeTradeTool = {
|
|
|
24
27
|
postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
|
|
25
28
|
slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
|
|
26
29
|
executionFeeToken: z.string().describe("Address of token to pay gas/execution fees (typically USDC)."),
|
|
27
|
-
leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10 for 10x."),
|
|
30
|
+
leverage: z.coerce.number().positive().describe("Leverage multiplier, e.g., 10 for 10x."),
|
|
28
31
|
tpSize: z.union([z.string(), z.number()]).optional().describe("Take Profit size. Use '0' to disable."),
|
|
29
32
|
tpPrice: z.union([z.string(), z.number()]).optional().describe("Take Profit trigger price."),
|
|
30
33
|
slSize: z.union([z.string(), z.number()]).optional().describe("Stop Loss size. Use '0' to disable."),
|
|
31
34
|
slPrice: z.union([z.string(), z.number()]).optional().describe("Stop Loss trigger price."),
|
|
32
|
-
tradingFee: z.union([z.string(), z.number()]).describe("
|
|
35
|
+
tradingFee: z.union([z.string(), z.number()]).optional().describe("Trading fee in quote token units. Supports human/raw prefix. Optional: auto-computed via get_user_trading_fee_rate."),
|
|
36
|
+
assetClass: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup assetClass (default from pool config or 1)."),
|
|
37
|
+
riskTier: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup riskTier (default from pool config or 1)."),
|
|
33
38
|
marketId: z.string().describe("Specific Market Config Hash. Fetch via get_market_list."),
|
|
34
39
|
},
|
|
35
40
|
handler: async (args) => {
|
|
36
41
|
try {
|
|
37
42
|
const { client, address, signer } = await resolveClient();
|
|
43
|
+
const chainId = getChainId();
|
|
44
|
+
const normalizedPositionId = args.positionId === "0" || !args.positionId || ZERO_POSITION_ID_RE.test(String(args.positionId))
|
|
45
|
+
? ""
|
|
46
|
+
: String(args.positionId);
|
|
38
47
|
const poolId = args.poolId;
|
|
39
48
|
// Fetch pool detail to get quoteToken for execution fee
|
|
40
|
-
const poolResponse = await client.markets.getMarketDetail({ chainId
|
|
49
|
+
const poolResponse = await client.markets.getMarketDetail({ chainId, poolId });
|
|
41
50
|
const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
|
|
42
51
|
if (!poolData)
|
|
43
52
|
throw new Error(`Could not find pool metadata for ID: ${poolId}`);
|
|
53
|
+
const baseDecimals = Number(poolData.baseDecimals ?? 18);
|
|
54
|
+
const quoteDecimals = Number(poolData.quoteDecimals ?? 6);
|
|
55
|
+
const mappedDirection = mapDirection(args.direction);
|
|
56
|
+
const mappedOrderType = mapOrderType(args.orderType);
|
|
57
|
+
const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
|
|
58
|
+
const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
|
|
59
|
+
const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
60
|
+
const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
|
|
61
|
+
const priceRaw = parseUserUnits(args.price, 30, "price");
|
|
62
|
+
if (BigInt(collateralRaw) <= 0n)
|
|
63
|
+
throw new Error("collateralAmount must be > 0.");
|
|
64
|
+
if (BigInt(sizeRaw) <= 0n)
|
|
65
|
+
throw new Error("size must be > 0.");
|
|
66
|
+
if (BigInt(priceRaw) <= 0n)
|
|
67
|
+
throw new Error("price must be > 0.");
|
|
68
|
+
let tradingFeeRaw = "";
|
|
69
|
+
let tradingFeeMeta = { source: "user" };
|
|
70
|
+
const tradingFeeInput = String(args.tradingFee ?? "").trim();
|
|
71
|
+
if (tradingFeeInput) {
|
|
72
|
+
tradingFeeRaw = parseUserUnits(tradingFeeInput, quoteDecimals, "tradingFee");
|
|
73
|
+
if (BigInt(tradingFeeRaw) < 0n)
|
|
74
|
+
throw new Error("tradingFee must be >= 0.");
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
let poolAssetClass = 1;
|
|
78
|
+
let poolRiskTier = 1;
|
|
79
|
+
try {
|
|
80
|
+
const levelRes = await client.markets.getPoolLevelConfig(poolId, chainId);
|
|
81
|
+
const levelConfig = levelRes?.levelConfig || levelRes?.data?.levelConfig || {};
|
|
82
|
+
if (Number.isFinite(Number(levelConfig.assetClass))) {
|
|
83
|
+
poolAssetClass = Number(levelConfig.assetClass);
|
|
84
|
+
}
|
|
85
|
+
if (Number.isFinite(Number(levelConfig.riskTier))) {
|
|
86
|
+
poolRiskTier = Number(levelConfig.riskTier);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
}
|
|
91
|
+
const assetClass = Number(args.assetClass ?? poolAssetClass ?? 1);
|
|
92
|
+
const riskTier = Number(args.riskTier ?? poolRiskTier ?? 1);
|
|
93
|
+
const feeRes = await client.utils.getUserTradingFeeRate(assetClass, riskTier, chainId);
|
|
94
|
+
if (Number(feeRes?.code) !== 0 || !feeRes?.data) {
|
|
95
|
+
throw new Error(`Failed to compute tradingFee automatically (assetClass=${assetClass}, riskTier=${riskTier}). Provide tradingFee manually.`);
|
|
96
|
+
}
|
|
97
|
+
const rateRaw = args.postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
|
|
98
|
+
const rateBig = BigInt(String(rateRaw ?? "0"));
|
|
99
|
+
tradingFeeRaw = ((BigInt(collateralRaw) * rateBig) / 1000000n).toString();
|
|
100
|
+
tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: String(rateRaw ?? "0") };
|
|
101
|
+
}
|
|
44
102
|
const mappedArgs = {
|
|
45
103
|
...args,
|
|
46
|
-
direction:
|
|
47
|
-
orderType:
|
|
48
|
-
triggerType:
|
|
104
|
+
direction: mappedDirection,
|
|
105
|
+
orderType: mappedOrderType,
|
|
106
|
+
triggerType: mappedTriggerType,
|
|
49
107
|
// Normalize positionId
|
|
50
|
-
positionId:
|
|
108
|
+
positionId: normalizedPositionId,
|
|
51
109
|
// Enforce executionFeeToken as quoteToken
|
|
52
|
-
executionFeeToken: poolData.quoteToken || args.executionFeeToken
|
|
110
|
+
executionFeeToken: poolData.quoteToken || args.executionFeeToken,
|
|
111
|
+
collateralAmount: `raw:${collateralRaw}`,
|
|
112
|
+
size: `raw:${sizeRaw}`,
|
|
113
|
+
price: `raw:${priceRaw}`,
|
|
114
|
+
tradingFee: `raw:${tradingFeeRaw}`,
|
|
115
|
+
slippagePct: slippagePctNormalized,
|
|
53
116
|
};
|
|
117
|
+
if (args.tpSize !== undefined) {
|
|
118
|
+
mappedArgs.tpSize = `raw:${parseUserUnits(args.tpSize, baseDecimals, "tpSize")}`;
|
|
119
|
+
}
|
|
120
|
+
if (args.tpPrice !== undefined) {
|
|
121
|
+
mappedArgs.tpPrice = `raw:${parseUserUnits(args.tpPrice, 30, "tpPrice")}`;
|
|
122
|
+
}
|
|
123
|
+
if (args.slSize !== undefined) {
|
|
124
|
+
mappedArgs.slSize = `raw:${parseUserUnits(args.slSize, baseDecimals, "slSize")}`;
|
|
125
|
+
}
|
|
126
|
+
if (args.slPrice !== undefined) {
|
|
127
|
+
mappedArgs.slPrice = `raw:${parseUserUnits(args.slPrice, 30, "slPrice")}`;
|
|
128
|
+
}
|
|
54
129
|
const raw = await openPosition(client, address, mappedArgs);
|
|
55
130
|
const data = await finalizeMutationResult(raw, signer, "execute_trade");
|
|
56
131
|
const txHash = data.confirmation?.txHash;
|
|
@@ -58,11 +133,27 @@ export const executeTradeTool = {
|
|
|
58
133
|
if (txHash) {
|
|
59
134
|
verification = await verifyTradeOutcome(client, address, args.poolId, txHash);
|
|
60
135
|
}
|
|
61
|
-
const payload = {
|
|
136
|
+
const payload = {
|
|
137
|
+
...data,
|
|
138
|
+
verification,
|
|
139
|
+
preflight: {
|
|
140
|
+
normalized: {
|
|
141
|
+
collateralAmountRaw: collateralRaw,
|
|
142
|
+
sizeRaw,
|
|
143
|
+
priceRaw30: priceRaw,
|
|
144
|
+
tradingFeeRaw,
|
|
145
|
+
tpSizeRaw: mappedArgs.tpSize?.replace(/^raw:/i, "") ?? null,
|
|
146
|
+
tpPriceRaw30: mappedArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
|
|
147
|
+
slSizeRaw: mappedArgs.slSize?.replace(/^raw:/i, "") ?? null,
|
|
148
|
+
slPriceRaw30: mappedArgs.slPrice?.replace(/^raw:/i, "") ?? null,
|
|
149
|
+
},
|
|
150
|
+
tradingFeeMeta,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
62
153
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
63
154
|
}
|
|
64
155
|
catch (error) {
|
|
65
|
-
return { content: [{ type: "text", text: `Error: ${error
|
|
156
|
+
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
|
66
157
|
}
|
|
67
158
|
},
|
|
68
159
|
};
|
package/dist/utils/units.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseUnits } from "ethers";
|
|
2
2
|
const DECIMAL_RE = /^-?\d+(\.\d+)?$/;
|
|
3
3
|
const RAW_PREFIX_RE = /^raw:/i;
|
|
4
|
+
const HUMAN_PREFIX_RE = /^human:/i;
|
|
4
5
|
const INTEGER_RE = /^-?\d+$/;
|
|
5
6
|
function normalizeDecimal(input) {
|
|
6
7
|
let value = input.trim();
|
|
@@ -19,10 +20,14 @@ function normalizeDecimal(input) {
|
|
|
19
20
|
}
|
|
20
21
|
return `${sign}${intPart || "0"}.${fracPart}`;
|
|
21
22
|
}
|
|
22
|
-
export function ensureUnits(value, decimals, label = "value") {
|
|
23
|
+
export function ensureUnits(value, decimals, label = "value", options = {}) {
|
|
24
|
+
const allowImplicitRaw = options.allowImplicitRaw ?? true;
|
|
23
25
|
let str = String(value).trim();
|
|
24
26
|
if (!str)
|
|
25
27
|
throw new Error(`${label} is required.`);
|
|
28
|
+
if (HUMAN_PREFIX_RE.test(str)) {
|
|
29
|
+
str = str.replace(HUMAN_PREFIX_RE, "").trim();
|
|
30
|
+
}
|
|
26
31
|
if (RAW_PREFIX_RE.test(str)) {
|
|
27
32
|
const raw = str.replace(RAW_PREFIX_RE, "").trim();
|
|
28
33
|
if (!INTEGER_RE.test(raw))
|
|
@@ -39,9 +44,8 @@ export function ensureUnits(value, decimals, label = "value") {
|
|
|
39
44
|
str = `${parts[0]}.${parts[1].slice(0, decimals)}`;
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
if (!str.includes(".") && (str.length > 12 || str.length > decimals)) {
|
|
47
|
+
// Legacy compatibility: optionally treat very large integers as already raw.
|
|
48
|
+
if (allowImplicitRaw && !str.includes(".") && (str.length > 12 || str.length > decimals)) {
|
|
45
49
|
return str;
|
|
46
50
|
}
|
|
47
51
|
try {
|
|
@@ -53,9 +57,12 @@ export function ensureUnits(value, decimals, label = "value") {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
export function parseUserUnits(value, decimals, label = "value") {
|
|
56
|
-
|
|
60
|
+
let str = String(value).trim();
|
|
57
61
|
if (!str)
|
|
58
62
|
throw new Error(`${label} is required.`);
|
|
63
|
+
if (HUMAN_PREFIX_RE.test(str)) {
|
|
64
|
+
str = str.replace(HUMAN_PREFIX_RE, "").trim();
|
|
65
|
+
}
|
|
59
66
|
if (RAW_PREFIX_RE.test(str)) {
|
|
60
67
|
const raw = str.replace(RAW_PREFIX_RE, "").trim();
|
|
61
68
|
if (!INTEGER_RE.test(raw))
|