@michaleffffff/mcp-trading-server 3.0.29 → 3.1.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/CHANGELOG.md +17 -0
- package/README.md +2 -5
- package/TOOL_EXAMPLES.md +1 -1
- package/dist/auth/resolveClient.js +1 -14
- package/dist/prompts/tradingGuide.js +1 -3
- package/dist/server.js +2 -2
- package/dist/services/marketService.js +46 -18
- package/dist/services/poolService.js +32 -74
- package/dist/tools/getAllTickers.js +1 -1
- package/dist/tools/getMyLpHoldings.js +2 -1
- package/dist/tools/listPools.js +2 -1
- package/dist/tools/manageLiquidity.js +1 -1
- package/dist/utils/slippage.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.1.0 - 2026-03-20
|
|
4
|
+
### Changed
|
|
5
|
+
- 发布 `3.1.0`,统一版本元数据到新的主次版本号。
|
|
6
|
+
- 运行环境现在只区分测试环境与正式环境,不再走 beta host / beta 地址分支。
|
|
7
|
+
- 修复测试环境下的市场发现链路,`list_pools` 恢复返回当前链池列表,`get_all_tickers` 在主接口不可用时继续走 fallback。
|
|
8
|
+
|
|
9
|
+
## 3.0.31 - 2026-03-20
|
|
10
|
+
### Fixed
|
|
11
|
+
- 修复 `normalizeSlippageRatio` 在处理 1% 到 100% 之间数值时的歧义问题。
|
|
12
|
+
- 为 ARB 池验证了 1% 滑点的存入操作(继续验证单参数调用的局限性)。
|
|
13
|
+
|
|
14
|
+
## [3.0.30] - 2026-03-20
|
|
15
|
+
### Fixed
|
|
16
|
+
- 完全对齐 SDK 流动性逻辑:移除 MCP 手动定义的预言机价格注入(适用于所有 State 0/1/2 池子)。
|
|
17
|
+
- 设置流动性操作默认滑点为 0.1%。
|
|
18
|
+
- 强制所有流动性交易调用 1 个参数的合约签名,以匹配 SDK v1.0.4-beta.4 的行为。
|
|
19
|
+
|
|
3
20
|
## [3.0.29] - 2026-03-20
|
|
4
21
|
- **Fix**: Implemented oracle validation bypass for State 0 (Cook) and State 1 (Primed) pools in liquidity management.
|
|
5
22
|
- This allows adding/removing liquidity on newly created pools that haven't initialized their oracle feeds yet.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
|
|
|
6
6
|
|
|
7
7
|
# Release Notes
|
|
8
8
|
|
|
9
|
-
- **Current release: 3.0
|
|
9
|
+
- **Current release: 3.1.0**
|
|
10
10
|
- **SDK baseline**: `@myx-trade/sdk@^1.0.4-beta.4` compatibility completed.
|
|
11
11
|
- **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
|
|
12
12
|
- **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
|
|
@@ -37,7 +37,6 @@ CHAIN_ID=...
|
|
|
37
37
|
BROKER_ADDRESS=0x...
|
|
38
38
|
QUOTE_TOKEN_ADDRESS=0x...
|
|
39
39
|
QUOTE_TOKEN_DECIMALS=...
|
|
40
|
-
IS_BETA_MODE=true
|
|
41
40
|
```
|
|
42
41
|
|
|
43
42
|
## Testnet `MYXBroker` Reference
|
|
@@ -46,7 +45,6 @@ IS_BETA_MODE=true
|
|
|
46
45
|
- Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
47
46
|
|
|
48
47
|
Use the broker that matches your active RPC and chain configuration.
|
|
49
|
-
If `IS_BETA_MODE` is omitted, MCP now auto-detects beta mode for the two testnet brokers above.
|
|
50
48
|
|
|
51
49
|
---
|
|
52
50
|
|
|
@@ -91,14 +89,13 @@ If `IS_BETA_MODE` is omitted, MCP now auto-detects beta mode for the two testnet
|
|
|
91
89
|
|
|
92
90
|
- **Explicit execution price**: `open_position_simple` no longer auto-fills a fresh Oracle price for `MARKET` orders; provide `price` explicitly when opening a position.
|
|
93
91
|
- **SDK-delegated funding delta**: MCP no longer performs its own increase-order margin/deposit reconciliation. New increase orders delegate collateral shortfall handling to SDK `createIncreaseOrder`.
|
|
94
|
-
- **Beta broker compatibility**: current Arbitrum Sepolia / Linea Sepolia test brokers auto-enable beta mode when `IS_BETA_MODE` is unset.
|
|
95
92
|
- **Exact approvals by default**: local fallback flows now prefer exact approvals instead of implicit unlimited approval.
|
|
96
93
|
- **Size semantics**: `size` always means base-asset quantity, not USD notional.
|
|
97
94
|
- **Pre-check semantics**: `check_account_ready` now uses SDK `getAvailableMarginBalance`. When that read fails, the response marks `summary.degraded=true` and includes diagnostics instead of silently trusting stale `freeMargin`.
|
|
98
95
|
- **Direction validation**: when a tool operates on an existing `positionId`, the supplied `direction` must match the live position.
|
|
99
96
|
- **TP/SL semantics**: LONG requires `tpPrice > entryPrice` and `slPrice < entryPrice`; SHORT uses the inverse.
|
|
100
97
|
- **LP safety**: LP preview failures are fail-close and no longer downgrade to `minAmountOut=0`.
|
|
101
|
-
- **LP slippage input**: `manage_liquidity.slippage`
|
|
98
|
+
- **LP slippage input**: `manage_liquidity.slippage` now matches SDK semantics and must be a ratio in `(0, 1]`; MCP still does not impose its old 5% business cap.
|
|
102
99
|
- **LP metadata safety**: `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
103
100
|
- **LP holdings semantics**: `get_my_lp_holdings` is an inventory view; returned rows are no longer ranked by mixed BASE/QUOTE LP raw balances.
|
|
104
101
|
- **LP token-source safety**: `get_my_lp_holdings` now falls back to live `poolInfo.basePool.poolToken` / `poolInfo.quotePool.poolToken` when market-detail token addresses are stale or mismatched.
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -304,7 +304,7 @@ Alias-friendly form also works:
|
|
|
304
304
|
|
|
305
305
|
LP preview failures now fail closed; the server no longer downgrades to `minAmountOut=0`.
|
|
306
306
|
Oracle-backed LP pricing requires a fresh price snapshot before execution.
|
|
307
|
-
`slippage` here
|
|
307
|
+
`slippage` here must be a ratio in `(0, 1]`, so `0.01 = 1%` and `0.005 = 0.5%`.
|
|
308
308
|
MCP no longer adds its own 5% LP slippage business cap on top of the provided value.
|
|
309
309
|
When the SDK LP path fails on a non-standard token `allowance()` read, MCP falls back to the direct router path automatically.
|
|
310
310
|
|
|
@@ -2,10 +2,6 @@ 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
|
-
const BETA_BROKERS_BY_CHAIN = {
|
|
6
|
-
421614: [],
|
|
7
|
-
59141: [],
|
|
8
|
-
};
|
|
9
5
|
function getDefaultBrokerByChainId(chainId) {
|
|
10
6
|
// Testnet mappings
|
|
11
7
|
if (chainId === 421614)
|
|
@@ -22,14 +18,6 @@ function getDefaultQuoteTokenByChainId(chainId) {
|
|
|
22
18
|
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b"; // Linea Sepolia
|
|
23
19
|
return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b";
|
|
24
20
|
}
|
|
25
|
-
function resolveIsBetaMode(chainId, brokerAddressRaw) {
|
|
26
|
-
const explicit = String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase();
|
|
27
|
-
if (explicit === "true")
|
|
28
|
-
return true;
|
|
29
|
-
if (explicit === "false")
|
|
30
|
-
return false;
|
|
31
|
-
return Boolean(BETA_BROKERS_BY_CHAIN[chainId]?.includes(String(brokerAddressRaw ?? "").trim().toLowerCase()));
|
|
32
|
-
}
|
|
33
21
|
export async function resolveClient() {
|
|
34
22
|
if (cached)
|
|
35
23
|
return cached;
|
|
@@ -50,7 +38,6 @@ export async function resolveClient() {
|
|
|
50
38
|
throw new Error("QUOTE_TOKEN_ADDRESS env var is required.");
|
|
51
39
|
const brokerAddress = normalizeAddress(brokerAddressRaw, "BROKER_ADDRESS");
|
|
52
40
|
const quoteToken = normalizeAddress(quoteTokenRaw, "QUOTE_TOKEN_ADDRESS");
|
|
53
|
-
const isBetaMode = resolveIsBetaMode(chainId, brokerAddress);
|
|
54
41
|
const provider = new JsonRpcProvider(rpcUrl);
|
|
55
42
|
const signer = new Wallet(privateKey, provider);
|
|
56
43
|
// Inject the EIP-1193 mock so SDK can sign transactions seamlessly
|
|
@@ -75,7 +62,7 @@ export async function resolveClient() {
|
|
|
75
62
|
signer: signer,
|
|
76
63
|
brokerAddress,
|
|
77
64
|
isTestnet,
|
|
78
|
-
isBetaMode,
|
|
65
|
+
isBetaMode: false,
|
|
79
66
|
walletClient: walletClient
|
|
80
67
|
});
|
|
81
68
|
cached = { client, address: signer.address, signer, chainId, quoteToken, quoteDecimals };
|
|
@@ -12,7 +12,7 @@ export const tradingGuidePrompt = {
|
|
|
12
12
|
content: {
|
|
13
13
|
type: "text",
|
|
14
14
|
text: `
|
|
15
|
-
# MYX Trading MCP Best Practices (v3.0
|
|
15
|
+
# MYX Trading MCP Best Practices (v3.1.0)
|
|
16
16
|
|
|
17
17
|
You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
|
|
18
18
|
|
|
@@ -36,7 +36,6 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
36
36
|
- **Execution Price**: \`open_position_simple\` no longer auto-fills a fresh Oracle price for \`MARKET\`; provide \`price\` explicitly when you want MCP to compute size / fee previews.
|
|
37
37
|
- **Funding Delta Ownership**: MCP no longer performs its own increase-order margin/deposit reconciliation. SDK \`createIncreaseOrder\` owns deposit-delta handling for new increase orders.
|
|
38
38
|
- **Pre-check Diagnostics**: \`check_account_ready\` now reports SDK \`availableMarginBalance\` first. If that read degrades, inspect \`summary.degraded\` and \`diagnostics.availableMarginError\` before trusting fallback account fields.
|
|
39
|
-
- **Beta Broker Mode**: Current Arbitrum Sepolia / Linea Sepolia test brokers auto-enable beta mode when \`IS_BETA_MODE\` is omitted.
|
|
40
39
|
- **Approval Safety**: Local fallback flows prefer exact approval sizing. Do not assume unlimited approvals are necessary.
|
|
41
40
|
- **Position Semantics**: \`size\` is BASE quantity, not USD notional. If a \`positionId\` is supplied, \`direction\` must match the live position.
|
|
42
41
|
- **TP/SL Semantics**: LONG should use \`tpPrice > entryPrice\` and \`slPrice < entryPrice\`; SHORT uses the inverse. Plain integer strings like \`"65000"\` are treated as human prices, not implicit raw 30-decimal values.
|
|
@@ -49,7 +48,6 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
49
48
|
- Arbitrum test: \`0x895C4ae2A22bB26851011d733A9355f663a1F939\`
|
|
50
49
|
- Linea test: \`0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2\`
|
|
51
50
|
- Always keep \`RPC_URL\`, \`CHAIN_ID\`, and \`BROKER_ADDRESS\` on the same network.
|
|
52
|
-
- If \`IS_BETA_MODE\` is unset, MCP auto-detects beta mode for those two testnet brokers.
|
|
53
51
|
|
|
54
52
|
Current Session:
|
|
55
53
|
- Wallet: ${address}
|
package/dist/server.js
CHANGED
|
@@ -461,7 +461,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
461
461
|
};
|
|
462
462
|
}
|
|
463
463
|
// ─── MCP Server ───
|
|
464
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0
|
|
464
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.1.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
465
465
|
// List tools
|
|
466
466
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
467
467
|
return {
|
|
@@ -582,7 +582,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
582
582
|
async function main() {
|
|
583
583
|
const transport = new StdioServerTransport();
|
|
584
584
|
await server.connect(transport);
|
|
585
|
-
logger.info("🚀 MYX Trading MCP Server v3.0
|
|
585
|
+
logger.info("🚀 MYX Trading MCP Server v3.1.0 running (stdio, pure on-chain, prod ready)");
|
|
586
586
|
}
|
|
587
587
|
main().catch((err) => {
|
|
588
588
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -31,10 +31,27 @@ function normalizeMarketState(value) {
|
|
|
31
31
|
const casted = Number(value);
|
|
32
32
|
return Number.isFinite(casted) ? casted : null;
|
|
33
33
|
}
|
|
34
|
+
function getMarketStatePriority(state) {
|
|
35
|
+
if (state === 2)
|
|
36
|
+
return 0;
|
|
37
|
+
if (state === 1)
|
|
38
|
+
return 1;
|
|
39
|
+
if (state === 0)
|
|
40
|
+
return 2;
|
|
41
|
+
if (state === null)
|
|
42
|
+
return 3;
|
|
43
|
+
return 4;
|
|
44
|
+
}
|
|
34
45
|
function normalizePoolId(row) {
|
|
35
46
|
const poolId = row?.poolId ?? row?.pool_id ?? "";
|
|
36
47
|
return String(poolId);
|
|
37
48
|
}
|
|
49
|
+
function matchesChainId(row, chainId) {
|
|
50
|
+
const raw = row?.chainId ?? row?.chain_id;
|
|
51
|
+
if (raw === undefined || raw === null || raw === "")
|
|
52
|
+
return true;
|
|
53
|
+
return Number(raw) === chainId;
|
|
54
|
+
}
|
|
38
55
|
function matchesKeyword(row, keywordUpper) {
|
|
39
56
|
const poolId = normalizePoolId(row);
|
|
40
57
|
const haystack = [
|
|
@@ -56,7 +73,7 @@ async function fetchApiMarketRows(client, chainId) {
|
|
|
56
73
|
// Preferred path: documented markets.searchMarket
|
|
57
74
|
try {
|
|
58
75
|
const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
|
|
59
|
-
const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
|
|
76
|
+
const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
60
77
|
if (searchRows.length > 0)
|
|
61
78
|
return searchRows;
|
|
62
79
|
}
|
|
@@ -65,7 +82,7 @@ async function fetchApiMarketRows(client, chainId) {
|
|
|
65
82
|
// Secondary path: documented markets.getPoolSymbolAll
|
|
66
83
|
try {
|
|
67
84
|
const symbolsRes = await client.markets.getPoolSymbolAll();
|
|
68
|
-
const symbolRows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
|
|
85
|
+
const symbolRows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
69
86
|
if (symbolRows.length > 0)
|
|
70
87
|
return symbolRows;
|
|
71
88
|
}
|
|
@@ -73,12 +90,12 @@ async function fetchApiMarketRows(client, chainId) {
|
|
|
73
90
|
}
|
|
74
91
|
// Legacy fallback: internal api namespace (for backward compatibility)
|
|
75
92
|
const marketListRes = await client.api?.getMarketList?.().catch(() => null);
|
|
76
|
-
const marketRows = extractMarketRows(marketListRes, chainId);
|
|
93
|
+
const marketRows = extractMarketRows(marketListRes, chainId).filter((row) => matchesChainId(row, chainId));
|
|
77
94
|
const marketRowsWithPoolId = marketRows.filter((row) => normalizePoolId(row));
|
|
78
95
|
if (marketRowsWithPoolId.length > 0)
|
|
79
96
|
return marketRowsWithPoolId;
|
|
80
97
|
const poolListRes = await client.api?.getPoolList?.().catch(() => null);
|
|
81
|
-
return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row));
|
|
98
|
+
return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
82
99
|
}
|
|
83
100
|
export async function getMarketPrice(client, poolId, chainIdOverride) {
|
|
84
101
|
const chainId = chainIdOverride ?? getChainId();
|
|
@@ -163,17 +180,23 @@ export async function searchMarket(client, keyword, limit = 1000, chainIdOverrid
|
|
|
163
180
|
dedupedByPoolId.set(poolId, row);
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
|
-
const
|
|
167
|
-
.
|
|
168
|
-
const
|
|
169
|
-
|
|
183
|
+
const orderedMarkets = Array.from(dedupedByPoolId.values())
|
|
184
|
+
.sort((left, right) => {
|
|
185
|
+
const leftState = normalizeMarketState(left?.state ?? left?.poolState);
|
|
186
|
+
const rightState = normalizeMarketState(right?.state ?? right?.poolState);
|
|
187
|
+
const byState = getMarketStatePriority(leftState) - getMarketStatePriority(rightState);
|
|
188
|
+
if (byState !== 0)
|
|
189
|
+
return byState;
|
|
190
|
+
const leftSymbol = String(left?.baseQuoteSymbol ?? left?.symbolName ?? left?.poolId ?? "");
|
|
191
|
+
const rightSymbol = String(right?.baseQuoteSymbol ?? right?.symbolName ?? right?.poolId ?? "");
|
|
192
|
+
return leftSymbol.localeCompare(rightSymbol);
|
|
170
193
|
})
|
|
171
194
|
.slice(0, requestedLimit);
|
|
172
195
|
// Get tickers for these pools to get price and change24h
|
|
173
196
|
let tickers = [];
|
|
174
|
-
if (
|
|
197
|
+
if (orderedMarkets.length > 0) {
|
|
175
198
|
try {
|
|
176
|
-
const poolIds =
|
|
199
|
+
const poolIds = orderedMarkets.map((market) => normalizePoolId(market));
|
|
177
200
|
const tickerRes = await client.markets.getTickerList({ chainId, poolIds });
|
|
178
201
|
tickers = Array.isArray(tickerRes) ? tickerRes : (tickerRes?.data || []);
|
|
179
202
|
}
|
|
@@ -181,7 +204,7 @@ export async function searchMarket(client, keyword, limit = 1000, chainIdOverrid
|
|
|
181
204
|
console.error("Failed to fetch tickers:", e);
|
|
182
205
|
}
|
|
183
206
|
}
|
|
184
|
-
return
|
|
207
|
+
return orderedMarkets.map((market) => {
|
|
185
208
|
const poolId = normalizePoolId(market);
|
|
186
209
|
const state = normalizeMarketState(market?.state ?? market?.poolState);
|
|
187
210
|
const ticker = tickers.find((t) => String(t.poolId).toLowerCase() === poolId.toLowerCase());
|
|
@@ -213,7 +236,7 @@ export async function getPoolList(client, chainIdOverride) {
|
|
|
213
236
|
const chainId = chainIdOverride ?? getChainId();
|
|
214
237
|
try {
|
|
215
238
|
const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
|
|
216
|
-
const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
|
|
239
|
+
const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
217
240
|
if (rows.length > 0)
|
|
218
241
|
return rows;
|
|
219
242
|
}
|
|
@@ -221,13 +244,14 @@ export async function getPoolList(client, chainIdOverride) {
|
|
|
221
244
|
}
|
|
222
245
|
try {
|
|
223
246
|
const symbolsRes = await client.markets.getPoolSymbolAll();
|
|
224
|
-
const rows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
|
|
247
|
+
const rows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
225
248
|
if (rows.length > 0)
|
|
226
249
|
return rows;
|
|
227
250
|
}
|
|
228
251
|
catch {
|
|
229
252
|
}
|
|
230
|
-
|
|
253
|
+
const poolListRes = await client.api?.getPoolList?.();
|
|
254
|
+
return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
|
|
231
255
|
}
|
|
232
256
|
/**
|
|
233
257
|
* 获取池子分级配置
|
|
@@ -279,9 +303,8 @@ export async function resolvePool(client, poolId, keyword, chainIdOverride) {
|
|
|
279
303
|
};
|
|
280
304
|
const rows = collect(poolListRes.data ?? poolListRes);
|
|
281
305
|
const searchKey = (kw || pid || "").toUpperCase();
|
|
282
|
-
const match = rows
|
|
283
|
-
|
|
284
|
-
return false;
|
|
306
|
+
const match = rows
|
|
307
|
+
.filter((row) => {
|
|
285
308
|
const base = String(row?.baseSymbol ?? "").toUpperCase();
|
|
286
309
|
const pair = String(row?.baseQuoteSymbol ?? "").toUpperCase();
|
|
287
310
|
const id = String(row?.poolId ?? row?.pool_id ?? "").toUpperCase();
|
|
@@ -292,7 +315,12 @@ export async function resolvePool(client, poolId, keyword, chainIdOverride) {
|
|
|
292
315
|
id === searchKey ||
|
|
293
316
|
baseToken === searchKey ||
|
|
294
317
|
quoteToken === searchKey;
|
|
295
|
-
})
|
|
318
|
+
})
|
|
319
|
+
.sort((left, right) => {
|
|
320
|
+
const leftState = normalizeMarketState(left?.state ?? left?.poolState);
|
|
321
|
+
const rightState = normalizeMarketState(right?.state ?? right?.poolState);
|
|
322
|
+
return getMarketStatePriority(leftState) - getMarketStatePriority(rightState);
|
|
323
|
+
})[0];
|
|
296
324
|
if (match) {
|
|
297
325
|
return String(match.poolId ?? match.pool_id);
|
|
298
326
|
}
|
|
@@ -8,31 +8,25 @@ import { logger } from "../utils/logger.js";
|
|
|
8
8
|
import { assertOracleFreshness, getFreshOraclePrice } from "./marketService.js";
|
|
9
9
|
const LP_DECIMALS = 18;
|
|
10
10
|
const POOL_MANAGER_BY_CHAIN = {
|
|
11
|
-
421614: "
|
|
11
|
+
421614: "0xB131655F326E82753b0e76c1ce853E257524f3a4",
|
|
12
12
|
59141: "0x85e869d98216221807A06636541Ec93C9c0a4B0c",
|
|
13
13
|
97: "0x4F917ef137b573D9790b87e3cF6dfb698cF00c9c",
|
|
14
14
|
56: "0x13F2130c2F3bfd612BBCBF35FB9E467dd32bAF3A",
|
|
15
15
|
};
|
|
16
|
-
const POOL_MANAGER_BY_CHAIN_BETA = {
|
|
17
|
-
421614: "0x05314a21Fc97B74f168730153b2B63A870D25dE5",
|
|
18
|
-
59141: "0xcf51a6895864c6D8E507fC31EF16b9011287c5f4",
|
|
19
|
-
97: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
20
|
-
56: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
21
|
-
};
|
|
22
16
|
const POOL_MANAGER_ABI = [
|
|
23
17
|
"function deployPool((bytes32 marketId,address baseToken))",
|
|
24
18
|
"function getMarketPool(bytes32 marketId,address asset) view returns ((bytes32 marketId,bytes32 poolId,address baseToken,address quoteToken,uint8 riskTier,uint8 state,address basePoolToken,address quotePoolToken,uint16 maxPriceDeviation,bool compoundEnabled,uint64 windowCapUsd,address poolVault,address tradingVault))",
|
|
25
19
|
];
|
|
26
20
|
const LP_ROUTER_BY_CHAIN = {
|
|
27
21
|
421614: {
|
|
28
|
-
router: "
|
|
29
|
-
basePool: "
|
|
30
|
-
quotePool: "
|
|
22
|
+
router: "0x0fb875c10fe2fF981e467765E3daaE4355b180D0",
|
|
23
|
+
basePool: "0xeC0b3C76cC1C47f9B29313c706c1F6FD8D1C023f",
|
|
24
|
+
quotePool: "0x608Bfd6B8Df0807aA6d1500B76a3eAb3C9DFd728",
|
|
31
25
|
},
|
|
32
26
|
59141: {
|
|
33
|
-
router: "
|
|
34
|
-
basePool: "
|
|
35
|
-
quotePool: "
|
|
27
|
+
router: "0xc5331ab0159379E6CfCC1f09b63360D0B9715D74",
|
|
28
|
+
basePool: "0x8440a78E65F07D8013dd5B0640E0E713c8fd9893",
|
|
29
|
+
quotePool: "0x50F1F7A772672FE0637cF89AD3aFc093584eD040",
|
|
36
30
|
},
|
|
37
31
|
97: {
|
|
38
32
|
router: "0xe9F2E58562aD1D50AfB1eD92EAa6D367A6D0e552",
|
|
@@ -45,28 +39,6 @@ const LP_ROUTER_BY_CHAIN = {
|
|
|
45
39
|
quotePool: "0xB1E6df749A602892FafB27bb39Fd4F044527121E",
|
|
46
40
|
},
|
|
47
41
|
};
|
|
48
|
-
const LP_ROUTER_BY_CHAIN_BETA = {
|
|
49
|
-
421614: {
|
|
50
|
-
router: "0xfb790ECE13Cd9e296b4a06ABF38D10431360c236",
|
|
51
|
-
basePool: "0x1F767BEa83EDe5E0C18904f439C579f52c2c0F1b",
|
|
52
|
-
quotePool: "0x50ad7da312c58c6689bEF028954a366cb5b8ee13",
|
|
53
|
-
},
|
|
54
|
-
59141: {
|
|
55
|
-
router: "0x0AE31989318565620c03d35889a0B2536b8fba5C",
|
|
56
|
-
basePool: "0xc9e3826c42183207B418116A16827C1605132c51",
|
|
57
|
-
quotePool: "0x9A9934D3b22103dE822d94A22A3177d728FE0e5a",
|
|
58
|
-
},
|
|
59
|
-
97: {
|
|
60
|
-
router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
|
|
61
|
-
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
62
|
-
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
63
|
-
},
|
|
64
|
-
56: {
|
|
65
|
-
router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
|
|
66
|
-
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
67
|
-
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
42
|
const PREVIEW_POOL_ABI = [
|
|
71
43
|
"function previewLpAmountOut(bytes32,uint256,uint256) view returns (uint256)",
|
|
72
44
|
"function previewQuoteAmountOut(bytes32,uint256,uint256) view returns (uint256)",
|
|
@@ -165,24 +137,19 @@ async function withMutedSdkAbiMismatchLogs(runner) {
|
|
|
165
137
|
console.error = original;
|
|
166
138
|
}
|
|
167
139
|
}
|
|
168
|
-
function isBetaModeEnabled() {
|
|
169
|
-
return String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
|
|
170
|
-
}
|
|
171
140
|
function getPoolManagerAddress(chainId) {
|
|
172
141
|
const envAddress = String(process.env.POOL_MANAGER_ADDRESS ?? "").trim();
|
|
173
142
|
if (envAddress) {
|
|
174
143
|
return normalizeAddress(envAddress, "POOL_MANAGER_ADDRESS");
|
|
175
144
|
}
|
|
176
|
-
const
|
|
177
|
-
const mapped = source[chainId];
|
|
145
|
+
const mapped = POOL_MANAGER_BY_CHAIN[chainId];
|
|
178
146
|
if (!mapped) {
|
|
179
|
-
throw new Error(`Pool manager address is not configured for chainId=${chainId}
|
|
147
|
+
throw new Error(`Pool manager address is not configured for chainId=${chainId}. Set POOL_MANAGER_ADDRESS env var.`);
|
|
180
148
|
}
|
|
181
149
|
return mapped;
|
|
182
150
|
}
|
|
183
151
|
function getLpAddresses(chainId) {
|
|
184
|
-
const
|
|
185
|
-
const matched = source[chainId];
|
|
152
|
+
const matched = LP_ROUTER_BY_CHAIN[chainId];
|
|
186
153
|
if (!matched) {
|
|
187
154
|
throw new Error(`Liquidity router config not found for chainId=${chainId}.`);
|
|
188
155
|
}
|
|
@@ -191,11 +158,11 @@ function getLpAddresses(chainId) {
|
|
|
191
158
|
function normalizeSlippageRatio(slippage) {
|
|
192
159
|
if (!Number.isFinite(slippage) || slippage < 0)
|
|
193
160
|
return 0;
|
|
194
|
-
if (slippage >
|
|
195
|
-
return slippage /
|
|
196
|
-
if (slippage
|
|
197
|
-
return 1
|
|
198
|
-
return slippage;
|
|
161
|
+
if (slippage > 100)
|
|
162
|
+
return slippage / 10000; // assume bps e.g. 200 = 2%
|
|
163
|
+
if (slippage >= 1)
|
|
164
|
+
return slippage / 100; // assume percent e.g. 1.5 = 1.5%
|
|
165
|
+
return slippage; // assume ratio e.g. 0.01 = 1%
|
|
199
166
|
}
|
|
200
167
|
function applyMinOutBySlippage(amountOut, slippage) {
|
|
201
168
|
if (amountOut <= 0n)
|
|
@@ -327,6 +294,7 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
327
294
|
const amountOut = await previewAmountOutForLiquidity(signer, chainId, poolId, poolType, action, amountIn, oraclePayload.referencePrice30);
|
|
328
295
|
const minAmountOut = applyMinOutBySlippage(amountOut, slippage);
|
|
329
296
|
const routerContract = new Contract(addresses.router, ROUTER_ABI, signer);
|
|
297
|
+
const oraclePriceTuples = oraclePayload.prices;
|
|
330
298
|
const txOverrides = {};
|
|
331
299
|
if (oraclePayload.value > 0n) {
|
|
332
300
|
txOverrides.value = oraclePayload.value;
|
|
@@ -340,21 +308,16 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
340
308
|
address,
|
|
341
309
|
[],
|
|
342
310
|
];
|
|
343
|
-
|
|
344
|
-
tx = await routerContract["depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
tx = await routerContract["depositQuote((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
|
|
348
|
-
}
|
|
311
|
+
tx = await routerContract["depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePriceTuples, paramsTuple, txOverrides);
|
|
349
312
|
}
|
|
350
313
|
else if (poolType === "QUOTE" && action === "withdraw") {
|
|
351
|
-
const paramsTuple = [
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
314
|
+
const paramsTuple = [
|
|
315
|
+
poolId,
|
|
316
|
+
amountIn,
|
|
317
|
+
minAmountOut,
|
|
318
|
+
address,
|
|
319
|
+
];
|
|
320
|
+
tx = await routerContract["withdrawQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePriceTuples, paramsTuple, txOverrides);
|
|
358
321
|
}
|
|
359
322
|
else if (poolType === "BASE" && action === "deposit") {
|
|
360
323
|
const paramsTuple = [
|
|
@@ -364,21 +327,16 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
364
327
|
address,
|
|
365
328
|
[],
|
|
366
329
|
];
|
|
367
|
-
|
|
368
|
-
tx = await routerContract["depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
tx = await routerContract["depositBase((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
|
|
372
|
-
}
|
|
330
|
+
tx = await routerContract["depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePriceTuples, paramsTuple, txOverrides);
|
|
373
331
|
}
|
|
374
332
|
else {
|
|
375
|
-
const paramsTuple = [
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
333
|
+
const paramsTuple = [
|
|
334
|
+
poolId,
|
|
335
|
+
amountIn,
|
|
336
|
+
minAmountOut,
|
|
337
|
+
address,
|
|
338
|
+
];
|
|
339
|
+
tx = await routerContract["withdrawBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePriceTuples, paramsTuple, txOverrides);
|
|
382
340
|
}
|
|
383
341
|
const txHash = String(tx?.hash ?? "").trim();
|
|
384
342
|
if (!txHash || !txHash.startsWith("0x")) {
|
|
@@ -15,7 +15,7 @@ export const getAllTickersTool = {
|
|
|
15
15
|
// Fallback for networks/environments where getAllTickers endpoint is unavailable.
|
|
16
16
|
const chainId = getChainId();
|
|
17
17
|
const poolList = await getPoolList(client, chainId);
|
|
18
|
-
const pools = Array.isArray(poolList
|
|
18
|
+
const pools = Array.isArray(poolList) ? poolList : (Array.isArray(poolList?.data) ? poolList.data : []);
|
|
19
19
|
const poolIds = pools.map((p) => p?.poolId ?? p?.pool_id).filter((id) => !!id);
|
|
20
20
|
if (poolIds.length === 0) {
|
|
21
21
|
throw new Error("Failed to fetch all tickers and no pools were available for fallback query.");
|
|
@@ -228,7 +228,8 @@ export const getMyLpHoldingsTool = {
|
|
|
228
228
|
let quoteCandidates = collectAddressCandidates(quotePoolToken);
|
|
229
229
|
let baseResolved = await resolveLpBalanceFromCandidates(provider, address, baseCandidates);
|
|
230
230
|
let quoteResolved = await resolveLpBalanceFromCandidates(provider, address, quoteCandidates);
|
|
231
|
-
const needPoolInfoEnrichment =
|
|
231
|
+
const needPoolInfoEnrichment = BigInt(baseResolved.balanceRaw) === 0n ||
|
|
232
|
+
BigInt(quoteResolved.balanceRaw) === 0n ||
|
|
232
233
|
!baseResolved.tokenAddress ||
|
|
233
234
|
!quoteResolved.tokenAddress;
|
|
234
235
|
if (needPoolInfoEnrichment) {
|
package/dist/tools/listPools.js
CHANGED
|
@@ -21,7 +21,8 @@ export const listPoolsTool = {
|
|
|
21
21
|
getPoolList(client),
|
|
22
22
|
client.markets.getPoolSymbolAll().catch(() => ({ data: [] }))
|
|
23
23
|
]);
|
|
24
|
-
const
|
|
24
|
+
const poolsSource = Array.isArray(poolListRes) ? poolListRes : poolListRes?.data ?? poolListRes;
|
|
25
|
+
const poolsRaw = collectRows(poolsSource);
|
|
25
26
|
const symbolsRaw = collectRows(symbolsRes?.data ?? symbolsRes);
|
|
26
27
|
const symbolMap = new Map(symbolsRaw
|
|
27
28
|
.filter((row) => row?.poolId || row?.pool_id)
|
|
@@ -69,7 +69,7 @@ export const manageLiquidityTool = {
|
|
|
69
69
|
poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
|
|
70
70
|
poolId: z.string().describe("Pool ID or Base Token Address"),
|
|
71
71
|
amount: z.coerce.string().describe("Amount in human-readable units string"),
|
|
72
|
-
slippage: z.coerce.number().
|
|
72
|
+
slippage: z.coerce.number().gt(0).max(1).describe("LP slippage ratio in (0, 1], e.g. 0.01 = 1%"),
|
|
73
73
|
chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
|
|
74
74
|
},
|
|
75
75
|
handler: async (args) => {
|
package/dist/utils/slippage.js
CHANGED
|
@@ -46,8 +46,8 @@ export function normalizeSlippagePct4dpFlexible(value, label = "slippagePct") {
|
|
|
46
46
|
}
|
|
47
47
|
export function normalizeLpSlippageRatio(value, label = "slippage") {
|
|
48
48
|
const numeric = Number(value);
|
|
49
|
-
if (!Number.isFinite(numeric) || numeric
|
|
50
|
-
throw new Error(`${label} must be a finite
|
|
49
|
+
if (!Number.isFinite(numeric) || numeric <= 0 || numeric > 1) {
|
|
50
|
+
throw new Error(`${label} must be a finite ratio in (0, 1].`);
|
|
51
51
|
}
|
|
52
|
-
return numeric
|
|
52
|
+
return numeric;
|
|
53
53
|
}
|