@michaleffffff/mcp-trading-server 3.0.16 → 3.0.21

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.
@@ -1,9 +1,25 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
- import { getOraclePrice, resolvePool } from "../services/marketService.js";
3
+ import { getFreshOraclePrice, resolvePool } from "../services/marketService.js";
4
4
  import { ensureUnits } from "../utils/units.js";
5
5
  import { finalizeMutationResult } from "../utils/mutationResult.js";
6
6
  import { normalizeSlippagePct4dpFlexible, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
7
+ const INTEGER_RE = /^\d+$/;
8
+ function resolvePositionSizeInput(position) {
9
+ const rawCandidates = [position?.sizeRaw, position?.positionSizeRaw];
10
+ for (const candidate of rawCandidates) {
11
+ const text = String(candidate ?? "").trim();
12
+ if (INTEGER_RE.test(text))
13
+ return `raw:${text}`;
14
+ }
15
+ const humanCandidates = [position?.size, position?.positionSize];
16
+ for (const candidate of humanCandidates) {
17
+ const text = String(candidate ?? "").trim();
18
+ if (text)
19
+ return text;
20
+ }
21
+ throw new Error(`Position size missing for positionId=${String(position?.positionId ?? position?.position_id ?? "").trim()}.`);
22
+ }
7
23
  export const closeAllPositionsTool = {
8
24
  name: "close_all_positions",
9
25
  description: "[TRADE] Emergency: close ALL open positions in a pool at once. Use for risk management.",
@@ -28,27 +44,18 @@ export const closeAllPositionsTool = {
28
44
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { message: "No open positions in this pool.", closed: 0 } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
29
45
  }
30
46
  // 2) 为每个仓位构建平仓参数
31
- const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "200");
32
- // 3) We need actual oracle prices to avoid Revert 0x613970e0 (InvalidParameter)
33
- const oraclePriceReq = await getOraclePrice(client, poolId).catch(() => null);
34
- let fallbackPrice = "0";
35
- if (oraclePriceReq && oraclePriceReq.price) {
36
- fallbackPrice = oraclePriceReq.price.toString();
37
- }
38
- else {
39
- const { getMarketPrice } = await import("../services/marketService.js");
40
- const marketData = await getMarketPrice(client, poolId).catch(() => null);
41
- if (marketData && marketData.price) {
42
- fallbackPrice = marketData.price.toString();
43
- }
44
- else {
45
- // Extreme fallback - but this may still revert
46
- fallbackPrice = poolPositions[0]?.entryPrice || "0";
47
- }
47
+ const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "100");
48
+ const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId });
49
+ const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
50
+ if (!marketDetail?.marketId) {
51
+ throw new Error(`Could not resolve market metadata for poolId=${poolId}.`);
48
52
  }
53
+ const baseDecimals = Number(marketDetail.baseDecimals ?? 18);
54
+ const quoteToken = String(marketDetail.quoteToken ?? "").trim() || getQuoteToken();
55
+ const oracle = await getFreshOraclePrice(client, poolId, chainId);
56
+ const freshOraclePrice = oracle.price.toString();
49
57
  const closeParams = poolPositions.map((pos) => {
50
- const rawSize = pos.size || pos.positionSize || "0";
51
- const rawPrice = pos.markPrice || pos.mark_price || fallbackPrice;
58
+ const sizeInput = resolvePositionSizeInput(pos);
52
59
  return {
53
60
  chainId,
54
61
  address,
@@ -59,11 +66,11 @@ export const closeAllPositionsTool = {
59
66
  timeInForce: 0, // IOC
60
67
  direction: pos.direction ?? 0,
61
68
  collateralAmount: "0",
62
- size: ensureUnits(rawSize, 18, "size"),
63
- price: ensureUnits(rawPrice, 30, "price"),
69
+ size: ensureUnits(sizeInput, baseDecimals, "size", { allowImplicitRaw: false }),
70
+ price: ensureUnits(freshOraclePrice, 30, "price", { allowImplicitRaw: false }),
64
71
  postOnly: false,
65
72
  slippagePct,
66
- executionFeeToken: pos.quoteToken || pos.quote_token || getQuoteToken(),
73
+ executionFeeToken: quoteToken,
67
74
  leverage: pos.userLeverage ?? pos.leverage ?? 1,
68
75
  };
69
76
  });
@@ -95,6 +95,20 @@ export const closePositionTool = {
95
95
  triggerType: preparedArgs.triggerType !== undefined ? mapTriggerType(preparedArgs.triggerType) : undefined,
96
96
  executionFeeToken: poolData.quoteToken || preparedArgs.executionFeeToken
97
97
  };
98
+ const positionsRes = await client.position.listPositions(address);
99
+ const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
100
+ const target = positions.find((position) => {
101
+ const pid = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
102
+ const pool = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
103
+ return pid === String(preparedArgs.positionId ?? "").trim().toLowerCase() && pool === String(poolId).toLowerCase();
104
+ });
105
+ if (!target) {
106
+ throw new Error(`Could not find live position snapshot for positionId=${preparedArgs.positionId} in poolId=${poolId}.`);
107
+ }
108
+ const liveDirection = Number(target?.direction);
109
+ if (!Number.isFinite(liveDirection) || liveDirection !== mappedArgs.direction) {
110
+ throw new Error(`direction mismatch for positionId=${preparedArgs.positionId}: input=${mappedArgs.direction}, live=${String(target?.direction ?? "unknown")}.`);
111
+ }
98
112
  const raw = await closePos(client, address, mappedArgs);
99
113
  const data = await finalizeMutationResult(raw, signer, "close_position");
100
114
  const txHash = data.confirmation?.txHash;
@@ -10,6 +10,14 @@ import { parseUserUnits } from "../utils/units.js";
10
10
  import { isZeroAddress } from "../utils/address.js";
11
11
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
12
12
  const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
13
+ function pow10(decimals) {
14
+ return 10n ** BigInt(decimals);
15
+ }
16
+ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimals) {
17
+ const numerator = sizeRaw * priceRaw30 * pow10(quoteDecimals);
18
+ const denominator = pow10(baseDecimals + 30);
19
+ return numerator / denominator;
20
+ }
13
21
  export const executeTradeTool = {
14
22
  name: "execute_trade",
15
23
  description: "[TRADE] Create an increase order (open or add to position) using SDK-native parameters.",
@@ -76,6 +84,22 @@ export const executeTradeTool = {
76
84
  throw new Error("size must be > 0.");
77
85
  if (BigInt(priceRaw) <= 0n)
78
86
  throw new Error("price must be > 0.");
87
+ if (normalizedPositionId) {
88
+ const positionsRes = await client.position.listPositions(address);
89
+ const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
90
+ const target = positions.find((position) => {
91
+ const positionId = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
92
+ const positionPoolId = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
93
+ return positionId === normalizedPositionId.toLowerCase() && positionPoolId === String(poolId).toLowerCase();
94
+ });
95
+ if (!target) {
96
+ throw new Error(`Could not find live position for positionId=${normalizedPositionId} in poolId=${poolId}.`);
97
+ }
98
+ const liveDirection = Number(target?.direction);
99
+ if (!Number.isFinite(liveDirection) || liveDirection !== mappedDirection) {
100
+ throw new Error(`direction mismatch for positionId=${normalizedPositionId}: input=${mappedDirection}, live=${String(target?.direction ?? "unknown")}.`);
101
+ }
102
+ }
79
103
  let tradingFeeRaw = "";
80
104
  let tradingFeeMeta = { source: "user" };
81
105
  const tradingFeeInput = String(args.tradingFee ?? "").trim();
@@ -107,7 +131,8 @@ export const executeTradeTool = {
107
131
  }
108
132
  const rateRaw = args.postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
109
133
  const rateBig = BigInt(String(rateRaw ?? "0"));
110
- tradingFeeRaw = ((BigInt(collateralRaw) * rateBig) / 1000000n).toString();
134
+ const notionalQuoteRaw = computeQuoteNotionalRaw(BigInt(sizeRaw), BigInt(priceRaw), baseDecimals, quoteDecimals);
135
+ tradingFeeRaw = ((notionalQuoteRaw * rateBig) / 1000000n).toString();
111
136
  tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: String(rateRaw ?? "0") };
112
137
  }
113
138
  const mappedArgs = {
@@ -1,10 +1,12 @@
1
1
  import { z } from "zod";
2
+ import { formatUnits } from "ethers";
2
3
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
4
  import { getMarketDetail, resolvePool } from "../services/marketService.js";
4
5
  import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
5
6
  import { extractErrorMessage } from "../utils/errorMessage.js";
6
7
  import { parseUserPrice30 } from "../utils/units.js";
7
8
  const INTEGER_RE = /^\d+$/;
9
+ const SIGNED_INTEGER_RE = /^-?\d+$/;
8
10
  const DECIMAL_RE = /^\d+(\.\d+)?$/;
9
11
  function normalizeMarketPrice30Input(value) {
10
12
  const text = String(value ?? "").trim();
@@ -29,6 +31,180 @@ function compactWarning(scope, err) {
29
31
  }
30
32
  return `${scope}: ${flat}`;
31
33
  }
34
+ function formatRawValue(value, decimals) {
35
+ const text = String(value ?? "").trim();
36
+ if (!INTEGER_RE.test(text))
37
+ return null;
38
+ try {
39
+ return formatUnits(text, decimals);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function buildFormattedAmount(value, decimals, symbol) {
46
+ const raw = String(value ?? "");
47
+ const formatted = formatRawValue(value, decimals);
48
+ return {
49
+ raw,
50
+ decimals,
51
+ formatted,
52
+ display: formatted && symbol ? `${formatted} ${symbol}` : formatted,
53
+ symbol: symbol ?? null,
54
+ };
55
+ }
56
+ function buildFormattedPrice30(value, quoteSymbol) {
57
+ const raw = String(value ?? "");
58
+ const formatted = formatRawValue(value, 30);
59
+ return {
60
+ raw,
61
+ decimals: 30,
62
+ formatted,
63
+ display: formatted && quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
64
+ symbol: quoteSymbol ?? null,
65
+ };
66
+ }
67
+ function buildFormattedRatio18(value) {
68
+ const raw = String(value ?? "");
69
+ const formatted = formatRawValue(value, 18);
70
+ return {
71
+ raw,
72
+ decimals: 18,
73
+ formatted,
74
+ display: formatted,
75
+ };
76
+ }
77
+ function buildRawScalar(value) {
78
+ const raw = String(value ?? "").trim();
79
+ if (!SIGNED_INTEGER_RE.test(raw)) {
80
+ return { raw, withCommas: raw || null };
81
+ }
82
+ try {
83
+ return {
84
+ raw,
85
+ withCommas: BigInt(raw).toLocaleString("en-US"),
86
+ };
87
+ }
88
+ catch {
89
+ return { raw, withCommas: raw };
90
+ }
91
+ }
92
+ function formatScaledIntegerString(raw, decimals) {
93
+ if (!SIGNED_INTEGER_RE.test(raw))
94
+ return null;
95
+ const negative = raw.startsWith("-");
96
+ const digits = negative ? raw.slice(1) : raw;
97
+ const padded = digits.padStart(decimals + 1, "0");
98
+ const intPart = padded.slice(0, -decimals) || "0";
99
+ const fracPart = padded.slice(-decimals).replace(/0+$/, "");
100
+ const normalized = fracPart ? `${intPart}.${fracPart}` : intPart;
101
+ return negative ? `-${normalized}` : normalized;
102
+ }
103
+ function buildFundingRateInfo(value) {
104
+ const base = buildRawScalar(value);
105
+ const raw = String(value ?? "").trim();
106
+ if (!SIGNED_INTEGER_RE.test(raw))
107
+ return base;
108
+ try {
109
+ const rawBig = BigInt(raw);
110
+ const perDayRaw = rawBig * 86400n;
111
+ const percentPerSecond = formatScaledIntegerString(raw, 12);
112
+ const percentPerDay = formatScaledIntegerString(perDayRaw.toString(), 12);
113
+ return {
114
+ ...base,
115
+ scale: "1e12 => percent",
116
+ percentPerSecond,
117
+ displayPerSecond: percentPerSecond ? `${percentPerSecond}%/秒` : null,
118
+ percentPerDay,
119
+ displayPerDay: percentPerDay ? `${percentPerDay}%/天` : null,
120
+ };
121
+ }
122
+ catch {
123
+ return base;
124
+ }
125
+ }
126
+ function buildTimestampInfo(value) {
127
+ const raw = String(value ?? "").trim();
128
+ const base = buildRawScalar(value);
129
+ if (!INTEGER_RE.test(raw)) {
130
+ return base;
131
+ }
132
+ const seconds = Number(raw);
133
+ if (!Number.isFinite(seconds) || seconds <= 0) {
134
+ return base;
135
+ }
136
+ const isoUtc = new Date(seconds * 1000).toISOString();
137
+ const secondsUntil = seconds - Math.floor(Date.now() / 1000);
138
+ return {
139
+ ...base,
140
+ isoUtc,
141
+ secondsUntil,
142
+ };
143
+ }
144
+ function computeQuoteNotionalDisplay(baseRaw, price30Raw, baseDecimals, quoteDecimals, quoteSymbol) {
145
+ const baseText = String(baseRaw ?? "").trim();
146
+ const priceText = String(price30Raw ?? "").trim();
147
+ if (!INTEGER_RE.test(baseText) || !INTEGER_RE.test(priceText))
148
+ return null;
149
+ try {
150
+ const notionalRaw = (BigInt(baseText) * BigInt(priceText) * (10n ** BigInt(quoteDecimals))) /
151
+ (10n ** BigInt(baseDecimals + 30));
152
+ return buildFormattedAmount(notionalRaw.toString(), quoteDecimals, quoteSymbol);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function formatPoolInfoSnapshot(poolInfo, marketDetail) {
159
+ if (!poolInfo || typeof poolInfo !== "object")
160
+ return null;
161
+ const baseSymbol = String(marketDetail?.baseSymbol ?? "BASE");
162
+ const quoteSymbol = String(marketDetail?.quoteSymbol ?? "QUOTE");
163
+ const baseDecimals = Number(marketDetail?.baseDecimals ?? 18);
164
+ const quoteDecimals = Number(marketDetail?.quoteDecimals ?? 6);
165
+ return {
166
+ quotePool: poolInfo.quotePool ? {
167
+ poolToken: poolInfo.quotePool.poolToken ?? null,
168
+ exchangeRate: buildFormattedRatio18(poolInfo.quotePool.exchangeRate),
169
+ poolTokenPrice: buildFormattedPrice30(poolInfo.quotePool.poolTokenPrice, quoteSymbol),
170
+ poolTokenSupply: buildFormattedAmount(poolInfo.quotePool.poolTokenSupply, 18, `m${quoteSymbol}.${baseSymbol}`),
171
+ totalDebt: buildFormattedAmount(poolInfo.quotePool.totalDebt, quoteDecimals, quoteSymbol),
172
+ baseCollateral: buildFormattedAmount(poolInfo.quotePool.baseCollateral, baseDecimals, baseSymbol),
173
+ } : null,
174
+ basePool: poolInfo.basePool ? {
175
+ poolToken: poolInfo.basePool.poolToken ?? null,
176
+ exchangeRate: buildFormattedRatio18(poolInfo.basePool.exchangeRate),
177
+ poolTokenPrice: buildFormattedPrice30(poolInfo.basePool.poolTokenPrice, quoteSymbol),
178
+ poolTokenSupply: buildFormattedAmount(poolInfo.basePool.poolTokenSupply, 18, `m${baseSymbol}.${quoteSymbol}`),
179
+ totalDebt: buildFormattedAmount(poolInfo.basePool.totalDebt, quoteDecimals, quoteSymbol),
180
+ baseCollateral: buildFormattedAmount(poolInfo.basePool.baseCollateral, baseDecimals, baseSymbol),
181
+ } : null,
182
+ reserveInfo: poolInfo.reserveInfo ? {
183
+ baseTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.baseTotalAmount, baseDecimals, baseSymbol),
184
+ baseReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.baseReservedAmount, baseDecimals, baseSymbol),
185
+ quoteTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteTotalAmount, quoteDecimals, quoteSymbol),
186
+ quoteReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteReservedAmount, quoteDecimals, quoteSymbol),
187
+ } : null,
188
+ fundingInfo: poolInfo.fundingInfo ? {
189
+ nextFundingRate: buildFundingRateInfo(poolInfo.fundingInfo.nextFundingRate),
190
+ lastFundingFeeTracker: buildRawScalar(poolInfo.fundingInfo.lastFundingFeeTracker),
191
+ nextEpochTime: buildTimestampInfo(poolInfo.fundingInfo.nextEpochTime),
192
+ } : null,
193
+ ioTracker: poolInfo.ioTracker ? {
194
+ tracker: buildFormattedAmount(poolInfo.ioTracker.tracker, baseDecimals, baseSymbol),
195
+ longSize: buildFormattedAmount(poolInfo.ioTracker.longSize, baseDecimals, baseSymbol),
196
+ shortSize: buildFormattedAmount(poolInfo.ioTracker.shortSize, baseDecimals, baseSymbol),
197
+ poolEntryPrice: buildFormattedPrice30(poolInfo.ioTracker.poolEntryPrice, quoteSymbol),
198
+ trackerNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.tracker, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
199
+ longNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.longSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
200
+ shortNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.shortSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
201
+ } : null,
202
+ liquidityInfo: poolInfo.liquidityInfo ? {
203
+ windowCaps: buildFormattedAmount(poolInfo.liquidityInfo.windowCaps, quoteDecimals, quoteSymbol),
204
+ openInterest: buildFormattedAmount(poolInfo.liquidityInfo.openInterest, 18, quoteSymbol),
205
+ } : null,
206
+ };
207
+ }
32
208
  export const getPoolMetadataTool = {
33
209
  name: "get_pool_metadata",
34
210
  description: "[MARKET] Get comprehensive metadata for a pool (market detail, on-chain info, liquidity, and limits).",
@@ -57,6 +233,11 @@ export const getPoolMetadataTool = {
57
233
  // 2. Pool Info (Reserves, Utilization)
58
234
  try {
59
235
  results.poolInfo = await getPoolInfo(poolId, chainId, client);
236
+ const rawMarketDetail = results.marketDetail?.data ?? results.marketDetail;
237
+ const formatted = formatPoolInfoSnapshot(results.poolInfo, rawMarketDetail);
238
+ if (formatted) {
239
+ results.poolInfoFormatted = formatted;
240
+ }
60
241
  }
61
242
  catch (err) {
62
243
  errors.push(compactWarning("poolInfo", err));
@@ -1,9 +1,11 @@
1
1
  import { z } from "zod";
2
+ import { formatUnits } from "ethers";
2
3
  import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
3
4
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
5
  import { resolvePool } from "../services/marketService.js";
5
6
  import { finalizeMutationResult } from "../utils/mutationResult.js";
6
7
  import { extractErrorMessage } from "../utils/errorMessage.js";
8
+ import { normalizeLpSlippageRatio } from "../utils/slippage.js";
7
9
  function normalizeAssetSymbol(value) {
8
10
  const text = String(value ?? "").trim();
9
11
  if (!text)
@@ -45,6 +47,20 @@ function resolveLpAssetNames(detail) {
45
47
  quoteLpAssetName,
46
48
  };
47
49
  }
50
+ function formatLpPricePayload(value, quoteSymbol) {
51
+ const raw = String(value ?? "").trim();
52
+ if (!/^\d+$/.test(raw)) {
53
+ return { raw, formatted: null, decimals: 30, symbol: quoteSymbol ?? null };
54
+ }
55
+ const formatted = formatUnits(raw, 30);
56
+ return {
57
+ raw,
58
+ formatted,
59
+ decimals: 30,
60
+ symbol: quoteSymbol ?? null,
61
+ display: quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
62
+ };
63
+ }
48
64
  export const manageLiquidityTool = {
49
65
  name: "manage_liquidity",
50
66
  description: "[LIQUIDITY] Add or withdraw liquidity from a BASE or QUOTE pool. Success response includes LP naming metadata: base `mBASE.QUOTE`, quote `mQUOTE.BASE`, plus `operatedLpAssetName` based on poolType.",
@@ -52,7 +68,7 @@ export const manageLiquidityTool = {
52
68
  action: z.coerce.string().describe("'deposit' or 'withdraw' (aliases: add/remove/increase/decrease; case-insensitive)"),
53
69
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
54
70
  poolId: z.string().describe("Pool ID or Base Token Address"),
55
- amount: z.coerce.number().positive().describe("Amount in human-readable units"),
71
+ amount: z.coerce.string().describe("Amount in human-readable units string"),
56
72
  slippage: z.coerce.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
57
73
  chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
58
74
  },
@@ -60,9 +76,13 @@ export const manageLiquidityTool = {
60
76
  try {
61
77
  const { client, signer } = await resolveClient();
62
78
  let { action, poolType, poolId } = args;
63
- const { amount, slippage } = args;
79
+ const amount = String(args.amount ?? "").trim();
80
+ const slippage = normalizeLpSlippageRatio(args.slippage);
64
81
  const chainId = args.chainId ?? getChainId();
65
82
  action = String(action ?? "").trim().toLowerCase();
83
+ if (!amount) {
84
+ throw new Error("amount is required.");
85
+ }
66
86
  const validActions = new Set(["deposit", "withdraw", "add", "remove", "increase", "decrease"]);
67
87
  if (!validActions.has(action)) {
68
88
  throw new Error(`Invalid action: ${args.action}. Use deposit/withdraw or aliases add/remove/increase/decrease.`);
@@ -135,8 +155,20 @@ export const getLpPriceTool = {
135
155
  },
136
156
  handler: async (args) => {
137
157
  try {
138
- const data = await getLpPrice(args.poolType, args.poolId, args.chainId);
139
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
158
+ const { client } = await resolveClient();
159
+ const chainId = args.chainId ?? getChainId();
160
+ const poolId = await resolvePool(client, args.poolId, undefined, chainId);
161
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId }).catch(() => null);
162
+ const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
163
+ const quoteSymbol = String(detail?.quoteSymbol ?? "").trim() || null;
164
+ const rawPrice = await getLpPrice(args.poolType, poolId, chainId);
165
+ const payload = {
166
+ raw: String(rawPrice ?? ""),
167
+ formatted: formatLpPricePayload(rawPrice, quoteSymbol ?? undefined),
168
+ poolType: args.poolType,
169
+ poolId,
170
+ };
171
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
140
172
  }
141
173
  catch (error) {
142
174
  return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
@@ -29,7 +29,7 @@ function readSnapshotText(snapshot, keys) {
29
29
  }
30
30
  return "";
31
31
  }
32
- function normalizeUnitsInput(text) {
32
+ function normalizeSizeInput(text) {
33
33
  const raw = String(text ?? "").trim();
34
34
  if (!raw)
35
35
  return "";
@@ -39,6 +39,17 @@ function normalizeUnitsInput(text) {
39
39
  return `raw:${raw}`;
40
40
  return raw;
41
41
  }
42
+ function normalizePriceInput(text, source = "user") {
43
+ const raw = String(text ?? "").trim();
44
+ if (!raw)
45
+ return "";
46
+ if (/^(raw|human):/i.test(raw))
47
+ return raw;
48
+ if (source === "snapshot" && /^\d+$/.test(raw) && raw.length > 12) {
49
+ return `raw:${raw}`;
50
+ }
51
+ return raw;
52
+ }
42
53
  function isExplicitZeroValue(value) {
43
54
  if (value === undefined || value === null)
44
55
  return false;
@@ -160,6 +171,58 @@ function resolvePositionSizeRaw(positionSnapshot, baseDecimals) {
160
171
  }
161
172
  return "";
162
173
  }
174
+ function resolveEntryPriceInput(positionSnapshot) {
175
+ const rawCandidates = [
176
+ positionSnapshot?.entryPriceRaw,
177
+ positionSnapshot?.openPriceRaw,
178
+ positionSnapshot?.avgPriceRaw,
179
+ positionSnapshot?.averageOpenPriceRaw,
180
+ ];
181
+ for (const value of rawCandidates) {
182
+ const text = String(value ?? "").trim();
183
+ if (INTEGER_RE.test(text))
184
+ return `raw:${text}`;
185
+ }
186
+ const humanCandidates = [
187
+ positionSnapshot?.entryPrice,
188
+ positionSnapshot?.openPrice,
189
+ positionSnapshot?.avgPrice,
190
+ positionSnapshot?.averageOpenPrice,
191
+ ];
192
+ for (const value of humanCandidates) {
193
+ const text = String(value ?? "").trim();
194
+ if (text)
195
+ return text;
196
+ }
197
+ return "";
198
+ }
199
+ function validateTpSlPriceSemantics(direction, entryPriceInput, tpPriceInput, slPriceInput) {
200
+ if (!entryPriceInput) {
201
+ throw new Error("Unable to resolve entryPrice for TP/SL validation.");
202
+ }
203
+ const entryPriceRaw = BigInt(parseUserUnits(entryPriceInput, 30, "entryPrice"));
204
+ if (entryPriceRaw <= 0n) {
205
+ throw new Error("entryPrice must be > 0 for TP/SL validation.");
206
+ }
207
+ if (tpPriceInput) {
208
+ const tpPriceRaw = BigInt(parseUserUnits(tpPriceInput, 30, "tpPrice"));
209
+ if (direction === 0 && tpPriceRaw <= entryPriceRaw) {
210
+ throw new Error("LONG TP must be greater than entryPrice.");
211
+ }
212
+ if (direction === 1 && tpPriceRaw >= entryPriceRaw) {
213
+ throw new Error("SHORT TP must be less than entryPrice.");
214
+ }
215
+ }
216
+ if (slPriceInput) {
217
+ const slPriceRaw = BigInt(parseUserUnits(slPriceInput, 30, "slPrice"));
218
+ if (direction === 0 && slPriceRaw >= entryPriceRaw) {
219
+ throw new Error("LONG SL must be less than entryPrice.");
220
+ }
221
+ if (direction === 1 && slPriceRaw <= entryPriceRaw) {
222
+ throw new Error("SHORT SL must be greater than entryPrice.");
223
+ }
224
+ }
225
+ }
163
226
  async function cancelTpSlByIntent(client, address, signer, chainId, args) {
164
227
  if (args.orderId) {
165
228
  const raw = await client.order.cancelAllOrders([String(args.orderId)], chainId);
@@ -265,12 +328,21 @@ export const manageTpSlTool = {
265
328
  const snapshotTpSize = readSnapshotText(orderSnapshot, ["tpSize", "takeProfitSize"]);
266
329
  const snapshotSlPrice = readSnapshotText(orderSnapshot, ["slPrice", "stopLossPrice", "slTriggerPrice"]);
267
330
  const snapshotSlSize = readSnapshotText(orderSnapshot, ["slSize", "stopLossSize"]);
331
+ const orderPositionId = readSnapshotText(orderSnapshot, ["positionId", "position_id"]);
268
332
  const size = isNonEmpty(args.size) ? String(args.size) : snapshotSize;
269
333
  const price = isNonEmpty(args.price) ? String(args.price) : snapshotPrice;
270
334
  const tpPrice = isNonEmpty(args.tpPrice) ? String(args.tpPrice) : snapshotTpPrice;
271
335
  const tpSize = isNonEmpty(args.tpSize) ? String(args.tpSize) : snapshotTpSize;
272
336
  const slPrice = isNonEmpty(args.slPrice) ? String(args.slPrice) : snapshotSlPrice;
273
337
  const slSize = isNonEmpty(args.slSize) ? String(args.slSize) : snapshotSlSize;
338
+ const updateDirection = normalizeDirectionInput(args.direction ?? orderSnapshot?.direction);
339
+ const positionSnapshot = orderPositionId
340
+ ? await findPositionSnapshot(client, address, args.poolId, orderPositionId)
341
+ : null;
342
+ if (updateDirection !== undefined) {
343
+ const entryPriceInput = resolveEntryPriceInput(positionSnapshot);
344
+ validateTpSlPriceSemantics(updateDirection, entryPriceInput, tpPrice || undefined, slPrice || undefined);
345
+ }
274
346
  if (!size || !price) {
275
347
  throw new Error("size and price are required for update. Provide them explicitly, or ensure orderId can be found via get_orders so they can be auto-resolved.");
276
348
  }
@@ -288,12 +360,12 @@ export const manageTpSlTool = {
288
360
  orderId: args.orderId,
289
361
  marketId: market.marketId,
290
362
  poolId: args.poolId,
291
- size: normalizeUnitsInput(size),
292
- price: normalizeUnitsInput(price),
293
- tpPrice: tpPrice ? normalizeUnitsInput(tpPrice) : "0",
294
- tpSize: tpSize ? normalizeUnitsInput(tpSize) : "0",
295
- slPrice: slPrice ? normalizeUnitsInput(slPrice) : "0",
296
- slSize: slSize ? normalizeUnitsInput(slSize) : "0",
363
+ size: normalizeSizeInput(size),
364
+ price: normalizePriceInput(price, isNonEmpty(args.price) ? "user" : "snapshot"),
365
+ tpPrice: tpPrice ? normalizePriceInput(tpPrice, isNonEmpty(args.tpPrice) ? "user" : "snapshot") : "0",
366
+ tpSize: tpSize ? normalizeSizeInput(tpSize) : "0",
367
+ slPrice: slPrice ? normalizePriceInput(slPrice, isNonEmpty(args.slPrice) ? "user" : "snapshot") : "0",
368
+ slSize: slSize ? normalizeSizeInput(slSize) : "0",
297
369
  quoteToken: market.quoteToken,
298
370
  useOrderCollateral: args.useOrderCollateral ?? true
299
371
  }, chainId);
@@ -326,6 +398,8 @@ export const manageTpSlTool = {
326
398
  if (resolvedDirection === undefined || !resolvedLeverage || Number(resolvedLeverage) <= 0) {
327
399
  throw new Error("direction and leverage are required when creating new TP/SL.");
328
400
  }
401
+ const entryPriceInput = resolveEntryPriceInput(positionSnapshot);
402
+ validateTpSlPriceSemantics(resolvedDirection, entryPriceInput, isNonEmpty(args.tpPrice) ? String(args.tpPrice) : undefined, isNonEmpty(args.slPrice) ? String(args.slPrice) : undefined);
329
403
  let tpSizeInput = isNonEmpty(args.tpSize) ? String(args.tpSize) : "";
330
404
  let slSizeInput = isNonEmpty(args.slSize) ? String(args.slSize) : "";
331
405
  const needsTpSize = isNonEmpty(args.tpPrice) && !tpSizeInput;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { OrderType } from "@myx-trade/sdk";
3
3
  import { formatUnits } from "ethers";
4
4
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
5
- import { resolvePool } from "../services/marketService.js";
5
+ import { resolvePool, getFreshOraclePrice } from "../services/marketService.js";
6
6
  import { openPosition } from "../services/tradeService.js";
7
7
  import { isZeroAddress, normalizeAddress } from "../utils/address.js";
8
8
  import { finalizeMutationResult } from "../utils/mutationResult.js";
@@ -25,6 +25,29 @@ function asBigint(raw, label) {
25
25
  throw new Error(`${label} must be an integer string.`);
26
26
  }
27
27
  }
28
+ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimals) {
29
+ const numerator = sizeRaw * priceRaw30 * pow10(quoteDecimals);
30
+ const denominator = pow10(baseDecimals + 30);
31
+ return numerator / denominator;
32
+ }
33
+ async function getRequiredApprovalSpendRaw(client, marketId, args, chainId) {
34
+ const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
35
+ if (!/^\d+$/.test(networkFeeText)) {
36
+ throw new Error(`Failed to resolve networkFee for marketId=${marketId}.`);
37
+ }
38
+ const baseNetworkFeeRaw = BigInt(networkFeeText);
39
+ if (baseNetworkFeeRaw <= 0n) {
40
+ throw new Error(`networkFee must be > 0 for marketId=${marketId}.`);
41
+ }
42
+ let executionOrderCount = 1n;
43
+ if (args.tpPrice && BigInt(String(args.tpSize ?? "0")) > 0n)
44
+ executionOrderCount += 1n;
45
+ if (args.slPrice && BigInt(String(args.slSize ?? "0")) > 0n)
46
+ executionOrderCount += 1n;
47
+ return (args.collateralRaw +
48
+ BigInt(args.tradingFeeRaw) +
49
+ (baseNetworkFeeRaw * executionOrderCount)).toString();
50
+ }
28
51
  function pickMarketDetail(res) {
29
52
  if (!res)
30
53
  return null;
@@ -128,19 +151,9 @@ export const openPositionSimpleTool = {
128
151
  let price30;
129
152
  let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
130
153
  if (orderType === OrderType.MARKET) {
131
- try {
132
- const oracle = await client.utils.getOraclePrice(poolId, chainId);
133
- price30 = parseUserPrice30(oracle.price, "oraclePrice");
134
- priceMeta = { source: "oracle", publishTime: oracle.publishTime, oracleType: oracle.oracleType, human: oracle.price };
135
- }
136
- catch (e) {
137
- const tickers = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
138
- const row = Array.isArray(tickers) ? tickers[0] : tickers?.data?.[0];
139
- if (!row?.price)
140
- throw new Error(`Failed to fetch oracle and ticker price for poolId=${poolId}: ${e?.message || e}`);
141
- price30 = parseUserPrice30(row.price, "marketPrice");
142
- priceMeta = { source: "ticker", publishTime: null, oracleType: null, human: row.price };
143
- }
154
+ const oracle = await getFreshOraclePrice(client, poolId, chainId);
155
+ price30 = parseUserPrice30(oracle.price, "oraclePrice");
156
+ priceMeta = { source: "oracle", publishTime: oracle.publishTime, oracleType: oracle.oracleType, human: oracle.price };
144
157
  }
145
158
  else {
146
159
  const userPrice = String(args.price ?? "").trim();
@@ -219,7 +232,8 @@ export const openPositionSimpleTool = {
219
232
  const rateRaw = postOnly ? feeData.makerFeeRate : feeData.takerFeeRate;
220
233
  tradingFeeMeta.feeRate = rateRaw;
221
234
  const rate = asBigint(String(rateRaw), "feeRate");
222
- const fee = (collateralRawBig * rate) / 1000000n;
235
+ const notionalQuoteRaw = computeQuoteNotionalRaw(sizeRawBig, price30Big, baseDecimals, quoteDecimals);
236
+ const fee = (notionalQuoteRaw * rate) / 1000000n;
223
237
  tradingFeeRaw = fee.toString();
224
238
  }
225
239
  else {
@@ -274,9 +288,14 @@ export const openPositionSimpleTool = {
274
288
  // 7) Optional approval
275
289
  let approval = null;
276
290
  if (args.autoApprove) {
277
- const requiredApprovalRaw = tradingFeeRaw && /^\d+$/.test(String(tradingFeeRaw))
278
- ? (collateralRawBig + asBigint(String(tradingFeeRaw), "tradingFee")).toString()
279
- : collateralRaw;
291
+ const requiredApprovalRaw = await getRequiredApprovalSpendRaw(client, marketId, {
292
+ collateralRaw: collateralRawBig,
293
+ tradingFeeRaw: String(tradingFeeRaw),
294
+ tpPrice: prep.tpPrice,
295
+ tpSize: prep.tpSize,
296
+ slPrice: prep.slPrice,
297
+ slSize: prep.slSize,
298
+ }, chainId);
280
299
  const needApproval = await client.utils.needsApproval(address, chainId, quoteToken, requiredApprovalRaw);
281
300
  if (needApproval) {
282
301
  const approveAmount = args.approveMax ? MAX_UINT256 : requiredApprovalRaw;