gate-wallet-cli 1.0.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.
@@ -0,0 +1,1243 @@
1
+ /**
2
+ * OpenAPI CLI 命令注册
3
+ * 通过 Gate DEX OpenAPI(AK/SK 认证)直接调用,无需 MCP
4
+ * 覆盖:Swap 交易、代币查询、市场行情
5
+ */
6
+ import { Command } from "commander";
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ import { createInterface } from "node:readline";
10
+ import { getOpenApiClient, resetOpenApiClient, } from "../core/openapi-client.js";
11
+ import { loadOpenApiConfig, maskSecretKey, getOpenApiConfigPath, saveOpenApiConfig, } from "../core/openapi-config.js";
12
+ import { getMcpClient } from "../core/mcp-client.js";
13
+ import { loadAuth } from "../core/token-store.js";
14
+ /** chain 名称 → chain_id 映射 */
15
+ const CHAIN_ID_MAP = {
16
+ eth: 1,
17
+ ethereum: 1,
18
+ bsc: 56,
19
+ polygon: 137,
20
+ arb: 42161,
21
+ arbitrum: 42161,
22
+ op: 10,
23
+ optimism: 10,
24
+ base: 8453,
25
+ avax: 43114,
26
+ avalanche: 43114,
27
+ fantom: 250,
28
+ ftm: 250,
29
+ cronos: 25,
30
+ linea: 59144,
31
+ scroll: 534352,
32
+ zksync: 324,
33
+ mantle: 5000,
34
+ gatelayer: 10088,
35
+ solana: 501,
36
+ sol: 501,
37
+ tron: 195,
38
+ trx: 195,
39
+ sui: 101,
40
+ ton: 607,
41
+ };
42
+ function resolveChainId(input) {
43
+ const n = Number(input);
44
+ if (!isNaN(n) && n > 0)
45
+ return n;
46
+ return CHAIN_ID_MAP[input.toLowerCase()] ?? 0;
47
+ }
48
+ /** 格式化打印 OpenAPI 返回结果 */
49
+ function printResult(res) {
50
+ if (res.code !== 0) {
51
+ const msg = res.message ?? res.msg ?? "Unknown error";
52
+ console.error(chalk.red(`OpenAPI Error [${res.code}]: ${msg}`));
53
+ return;
54
+ }
55
+ console.log(JSON.stringify(res.data, null, 2));
56
+ }
57
+ const NO_CONFIG_MSG = [
58
+ "OpenAPI 尚未配置 AK/SK,请先配置:",
59
+ "",
60
+ ` pnpm cli openapi-config --set-ak YOUR_AK --set-sk YOUR_SK`,
61
+ "",
62
+ ` 或直接编辑 ~/.gate-wallet/openapi.json`,
63
+ ` 获取 AK/SK: https://web3.gate.com/zh/api-manage`,
64
+ ].join("\n");
65
+ function requireClient() {
66
+ const client = getOpenApiClient();
67
+ if (!client) {
68
+ console.error(chalk.yellow(NO_CONFIG_MSG));
69
+ return null;
70
+ }
71
+ return client;
72
+ }
73
+ export function registerOpenApiCommands(program) {
74
+ // ─── 配置管理 ──────────────────────────────────────────
75
+ program
76
+ .command("openapi-config")
77
+ .description("查看 / 更新 OpenAPI AK/SK 配置")
78
+ .option("--set-ak <ak>", "设置 Trade 通道 API Key")
79
+ .option("--set-sk <sk>", "设置 Trade 通道 Secret Key")
80
+ .option("--set-query-ak <ak>", "设置 Query 通道 API Key")
81
+ .option("--set-query-sk <sk>", "设置 Query 通道 Secret Key")
82
+ .action(async (opts) => {
83
+ if (opts.setAk || opts.setSk || opts.setQueryAk || opts.setQuerySk) {
84
+ const config = loadOpenApiConfig() ?? {
85
+ trade: { api_key: "", secret_key: "" },
86
+ query: { api_key: "", secret_key: "" },
87
+ default_slippage: 0.03,
88
+ default_slippage_type: 1,
89
+ };
90
+ if (opts.setAk)
91
+ config.trade.api_key = opts.setAk;
92
+ if (opts.setSk)
93
+ config.trade.secret_key = opts.setSk;
94
+ if (opts.setQueryAk)
95
+ config.query.api_key = opts.setQueryAk;
96
+ if (opts.setQuerySk)
97
+ config.query.secret_key = opts.setQuerySk;
98
+ saveOpenApiConfig(config);
99
+ resetOpenApiClient();
100
+ console.log(chalk.green("OpenAPI 配置已更新"));
101
+ const spinner = ora("验证凭证...").start();
102
+ try {
103
+ const client = requireClient();
104
+ if (!client) {
105
+ spinner.stop();
106
+ return;
107
+ }
108
+ const res = await client.swapChains();
109
+ if (res.code === 0) {
110
+ spinner.succeed("凭证验证通过");
111
+ }
112
+ else {
113
+ spinner.fail(`凭证验证失败: [${res.code}] ${res.message ?? res.msg}`);
114
+ }
115
+ }
116
+ catch (err) {
117
+ spinner.fail(`验证请求失败: ${err.message}`);
118
+ }
119
+ return;
120
+ }
121
+ const config = loadOpenApiConfig();
122
+ if (!config) {
123
+ console.error(chalk.yellow(NO_CONFIG_MSG));
124
+ return;
125
+ }
126
+ console.log(chalk.bold("OpenAPI Configuration"));
127
+ console.log(` File: ${chalk.gray(getOpenApiConfigPath())}`);
128
+ console.log(chalk.bold("\n Trade Channel (trade.swap.*)"));
129
+ console.log(` API Key: ${config.trade.api_key}`);
130
+ console.log(` Secret Key: ${maskSecretKey(config.trade.secret_key)}`);
131
+ console.log(chalk.bold("\n Query Channel (base.token.* / market.*)"));
132
+ console.log(` API Key: ${config.query.api_key}`);
133
+ console.log(` Secret Key: ${maskSecretKey(config.query.secret_key)}`);
134
+ if (config.trade.endpoint) {
135
+ console.log(`\n Trade Endpoint: ${config.trade.endpoint}`);
136
+ }
137
+ if (config.query.endpoint) {
138
+ console.log(` Query Endpoint: ${config.query.endpoint}`);
139
+ }
140
+ if (config.default_slippage !== undefined) {
141
+ console.log(`\n Default Slippage: ${(config.default_slippage * 100).toFixed(1)}%`);
142
+ }
143
+ console.log(chalk.gray("\n Upgrade at https://web3.gate.com/zh/api-manage"));
144
+ });
145
+ // ─── Swap 交易类 ───────────────────────────────────────
146
+ program
147
+ .command("openapi-chains")
148
+ .description("[OpenAPI] 查询支持的链列表")
149
+ .action(async () => {
150
+ const client = requireClient();
151
+ if (!client)
152
+ return;
153
+ const spinner = ora("查询链列表...").start();
154
+ try {
155
+ const res = await client.swapChains();
156
+ spinner.stop();
157
+ printResult(res);
158
+ }
159
+ catch (err) {
160
+ spinner.fail(err.message);
161
+ }
162
+ });
163
+ program
164
+ .command("openapi-gas")
165
+ .description("[OpenAPI] 查询 Gas 价格")
166
+ .option("--chain <chain>", "链名或 chain_id", "eth")
167
+ .action(async (opts) => {
168
+ const chainId = resolveChainId(opts.chain);
169
+ if (!chainId) {
170
+ console.error(chalk.red(`未知链: ${opts.chain}`));
171
+ return;
172
+ }
173
+ const spinner = ora("查询 Gas 价格...").start();
174
+ try {
175
+ const client = requireClient();
176
+ if (!client) {
177
+ spinner.stop();
178
+ return;
179
+ }
180
+ const res = await client.swapGasPrice(chainId);
181
+ spinner.stop();
182
+ printResult(res);
183
+ }
184
+ catch (err) {
185
+ spinner.fail(err.message);
186
+ }
187
+ });
188
+ program
189
+ .command("openapi-quote")
190
+ .description("[OpenAPI] 获取 Swap 报价")
191
+ .requiredOption("--chain <chain>", "链名或 chain_id")
192
+ .requiredOption("--from <token>", "源 token 地址,原生币用 -")
193
+ .requiredOption("--to <token>", "目标 token 合约地址")
194
+ .requiredOption("--amount <amount>", "数量(人类可读格式)")
195
+ .requiredOption("--wallet <address>", "钱包地址")
196
+ .option("--slippage <pct>", "滑点 (0.03=3%)")
197
+ .option("--slippage-type <1|2>", "1=百分比, 2=固定值")
198
+ .action(async (opts) => {
199
+ const chainId = resolveChainId(opts.chain);
200
+ if (!chainId) {
201
+ console.error(chalk.red(`未知链: ${opts.chain}`));
202
+ return;
203
+ }
204
+ const config = loadOpenApiConfig();
205
+ const client = requireClient();
206
+ if (!client)
207
+ return;
208
+ const spinner = ora("获取报价...").start();
209
+ try {
210
+ const res = await client.swapQuote({
211
+ chain_id: chainId,
212
+ token_in: opts.from,
213
+ token_out: opts.to,
214
+ amount_in: opts.amount,
215
+ slippage: Number(opts.slippage ?? config?.default_slippage ?? 0.03),
216
+ slippage_type: Number(opts.slippageType ?? config?.default_slippage_type ?? 1),
217
+ user_wallet: opts.wallet,
218
+ });
219
+ spinner.stop();
220
+ printResult(res);
221
+ }
222
+ catch (err) {
223
+ spinner.fail(err.message);
224
+ }
225
+ });
226
+ program
227
+ .command("openapi-build")
228
+ .description("[OpenAPI] 构建 Swap 未签名交易")
229
+ .requiredOption("--chain <chain>", "链名或 chain_id")
230
+ .requiredOption("--from <token>", "源 token 地址,原生币用 -")
231
+ .requiredOption("--to <token>", "目标 token 合约地址")
232
+ .requiredOption("--amount <amount>", "数量")
233
+ .requiredOption("--wallet <address>", "钱包地址")
234
+ .option("--receiver <address>", "接收地址(默认同 wallet)")
235
+ .option("--quote-id <id>", "报价 ID(建议传入)")
236
+ .option("--slippage <pct>", "滑点 (0.03=3%)")
237
+ .option("--slippage-type <1|2>", "1=百分比, 2=固定值")
238
+ .action(async (opts) => {
239
+ const chainId = resolveChainId(opts.chain);
240
+ if (!chainId) {
241
+ console.error(chalk.red(`未知链: ${opts.chain}`));
242
+ return;
243
+ }
244
+ const config = loadOpenApiConfig();
245
+ const client = requireClient();
246
+ if (!client)
247
+ return;
248
+ const spinner = ora("构建交易...").start();
249
+ try {
250
+ const params = {
251
+ chain_id: chainId,
252
+ amount_in: opts.amount,
253
+ token_in: opts.from,
254
+ token_out: opts.to,
255
+ slippage: String(opts.slippage ?? config?.default_slippage ?? 0.03),
256
+ slippage_type: String(opts.slippageType ?? config?.default_slippage_type ?? 1),
257
+ user_wallet: opts.wallet,
258
+ receiver: opts.receiver ?? opts.wallet,
259
+ };
260
+ if (opts.quoteId)
261
+ params.quote_id = opts.quoteId;
262
+ const res = await client.call("trade.swap.build", params);
263
+ spinner.stop();
264
+ printResult(res);
265
+ }
266
+ catch (err) {
267
+ spinner.fail(err.message);
268
+ }
269
+ });
270
+ program
271
+ .command("openapi-approve")
272
+ .description("[OpenAPI] 获取 ERC20 approve calldata")
273
+ .requiredOption("--wallet <address>", "钱包地址")
274
+ .requiredOption("--amount <amount>", "授权数量(人类可读格式)")
275
+ .requiredOption("--quote-id <id>", "报价 ID")
276
+ .action(async (opts) => {
277
+ const spinner = ora("获取 approve calldata...").start();
278
+ try {
279
+ const client = requireClient();
280
+ if (!client) {
281
+ spinner.stop();
282
+ return;
283
+ }
284
+ const res = await client.swapApproveTransaction({
285
+ user_wallet: opts.wallet,
286
+ approve_amount: opts.amount,
287
+ quote_id: opts.quoteId,
288
+ });
289
+ spinner.stop();
290
+ printResult(res);
291
+ }
292
+ catch (err) {
293
+ spinner.fail(err.message);
294
+ }
295
+ });
296
+ program
297
+ .command("openapi-submit")
298
+ .description("[OpenAPI] 提交已签名交易")
299
+ .requiredOption("--order-id <id>", "订单 ID(build 返回)")
300
+ .option("--signed-tx <json>", "签名交易 JSON 数组字符串,如 '[\"0x02f8...\"]'")
301
+ .option("--tx-hash <hash>", "交易哈希(自行广播后上报)")
302
+ .option("--signed-approve-tx <json>", "签名 approve 交易 JSON 数组字符串")
303
+ .action(async (opts) => {
304
+ if (!opts.signedTx && !opts.txHash) {
305
+ console.error(chalk.red("--signed-tx 和 --tx-hash 必须传入其中一个"));
306
+ return;
307
+ }
308
+ const spinner = ora("提交交易...").start();
309
+ try {
310
+ const client = requireClient();
311
+ if (!client) {
312
+ spinner.stop();
313
+ return;
314
+ }
315
+ const params = {
316
+ order_id: opts.orderId,
317
+ };
318
+ if (opts.signedTx)
319
+ params.signed_tx_string = opts.signedTx;
320
+ if (opts.txHash)
321
+ params.tx_hash = opts.txHash;
322
+ if (opts.signedApproveTx)
323
+ params.signed_approve_tx_string = opts.signedApproveTx;
324
+ const res = await client.call("trade.swap.submit", params);
325
+ spinner.stop();
326
+ printResult(res);
327
+ }
328
+ catch (err) {
329
+ spinner.fail(err.message);
330
+ }
331
+ });
332
+ program
333
+ .command("openapi-status")
334
+ .description("[OpenAPI] 查询 Swap 订单状态")
335
+ .requiredOption("--chain <chain>", "链名或 chain_id")
336
+ .requiredOption("--order-id <id>", "订单 ID")
337
+ .option("--tx-hash <hash>", "交易哈希", "")
338
+ .action(async (opts) => {
339
+ const chainId = resolveChainId(opts.chain);
340
+ if (!chainId) {
341
+ console.error(chalk.red(`未知链: ${opts.chain}`));
342
+ return;
343
+ }
344
+ const spinner = ora("查询订单状态...").start();
345
+ try {
346
+ const client = requireClient();
347
+ if (!client) {
348
+ spinner.stop();
349
+ return;
350
+ }
351
+ const res = await client.swapStatus({
352
+ chain_id: chainId,
353
+ order_id: opts.orderId,
354
+ tx_hash: opts.txHash,
355
+ });
356
+ spinner.stop();
357
+ printResult(res);
358
+ }
359
+ catch (err) {
360
+ spinner.fail(err.message);
361
+ }
362
+ });
363
+ program
364
+ .command("openapi-history")
365
+ .description("[OpenAPI] 查询 Swap 历史订单")
366
+ .requiredOption("--wallet <address>", "钱包地址(可逗号分隔多个)")
367
+ .option("--page <n>", "页码", "1")
368
+ .option("--limit <n>", "每页条数", "20")
369
+ .option("--chain <chain>", "按链过滤")
370
+ .action(async (opts) => {
371
+ const spinner = ora("查询历史订单...").start();
372
+ try {
373
+ const client = requireClient();
374
+ if (!client) {
375
+ spinner.stop();
376
+ return;
377
+ }
378
+ const params = {
379
+ user_wallet: opts.wallet.split(",").map((s) => s.trim()),
380
+ page_number: Number(opts.page),
381
+ page_size: Number(opts.limit),
382
+ };
383
+ if (opts.chain) {
384
+ params.chain_id = resolveChainId(opts.chain);
385
+ }
386
+ const res = await client.call("trade.swap.history", params);
387
+ spinner.stop();
388
+ printResult(res);
389
+ }
390
+ catch (err) {
391
+ spinner.fail(err.message);
392
+ }
393
+ });
394
+ // ─── 代币查询类 ────────────────────────────────────────
395
+ program
396
+ .command("openapi-swap-tokens")
397
+ .description("[OpenAPI] 查询链上可 Swap 代币列表")
398
+ .option("--chain <chain>", "链名或 chain_id")
399
+ .option("--search <keyword>", "搜索(symbol 或合约地址)")
400
+ .option("--tag <tag>", "列表类型: favorite | recommend")
401
+ .option("--wallet <address>", "钱包地址(展示余额/收藏)")
402
+ .action(async (opts) => {
403
+ const spinner = ora("查询代币列表...").start();
404
+ try {
405
+ const client = requireClient();
406
+ if (!client) {
407
+ spinner.stop();
408
+ return;
409
+ }
410
+ const params = {};
411
+ if (opts.chain)
412
+ params.chain_id = String(resolveChainId(opts.chain));
413
+ if (opts.search)
414
+ params.search = opts.search;
415
+ if (opts.tag)
416
+ params.tag = opts.tag;
417
+ if (opts.wallet)
418
+ params.wallet = opts.wallet;
419
+ const res = await client.call("base.token.swap_list", params);
420
+ spinner.stop();
421
+ printResult(res);
422
+ }
423
+ catch (err) {
424
+ spinner.fail(err.message);
425
+ }
426
+ });
427
+ program
428
+ .command("openapi-token-rank")
429
+ .description("[OpenAPI] 代币排行榜")
430
+ .option("--chain <chain>", "链名或 chain_id")
431
+ .option("--sort <field>", "排序字段", "trend_info.price_change_24h")
432
+ .option("--order <asc|desc>", "排序方向", "desc")
433
+ .option("--limit <n>", "返回条数", "10")
434
+ .action(async (opts) => {
435
+ const spinner = ora("查询排行榜...").start();
436
+ try {
437
+ const client = requireClient();
438
+ if (!client) {
439
+ spinner.stop();
440
+ return;
441
+ }
442
+ const params = {
443
+ sort: [{ field: opts.sort, order: opts.order }],
444
+ limit: Number(opts.limit),
445
+ };
446
+ if (opts.chain) {
447
+ params.chain_id = { eq: String(resolveChainId(opts.chain)) };
448
+ }
449
+ const res = await client.call("base.token.ranking", params);
450
+ spinner.stop();
451
+ printResult(res);
452
+ }
453
+ catch (err) {
454
+ spinner.fail(err.message);
455
+ }
456
+ });
457
+ program
458
+ .command("openapi-new-tokens")
459
+ .description("[OpenAPI] 按创建时间筛选新币")
460
+ .requiredOption("--start <time>", "开始时间 (RFC3339)")
461
+ .option("--end <time>", "结束时间 (RFC3339)")
462
+ .option("--chain <chain>", "链名或 chain_id")
463
+ .option("--limit <n>", "返回数量", "20")
464
+ .action(async (opts) => {
465
+ const spinner = ora("查询新代币...").start();
466
+ try {
467
+ const client = requireClient();
468
+ if (!client) {
469
+ spinner.stop();
470
+ return;
471
+ }
472
+ const params = {
473
+ start: opts.start,
474
+ end: opts.end ?? new Date().toISOString(),
475
+ limit: opts.limit,
476
+ };
477
+ if (opts.chain)
478
+ params.chain_id = String(resolveChainId(opts.chain));
479
+ const res = await client.call("base.token.range_by_created_at", params);
480
+ spinner.stop();
481
+ printResult(res);
482
+ }
483
+ catch (err) {
484
+ spinner.fail(err.message);
485
+ }
486
+ });
487
+ program
488
+ .command("openapi-token-risk")
489
+ .description("[OpenAPI] 代币安全审计")
490
+ .requiredOption("--chain <chain>", "链名或 chain_id")
491
+ .requiredOption("--address <addr>", "代币合约地址")
492
+ .option("--lan <lang>", "语言 (en/zh)", "en")
493
+ .action(async (opts) => {
494
+ const chainId = resolveChainId(opts.chain);
495
+ if (!chainId) {
496
+ console.error(chalk.red(`未知链: ${opts.chain}`));
497
+ return;
498
+ }
499
+ const spinner = ora("查询安全审计...").start();
500
+ try {
501
+ const client = requireClient();
502
+ if (!client) {
503
+ spinner.stop();
504
+ return;
505
+ }
506
+ const res = await client.tokenRiskInfos({
507
+ chain_id: String(chainId),
508
+ address: opts.address,
509
+ lan: opts.lan,
510
+ ignore: "true",
511
+ });
512
+ spinner.stop();
513
+ printResult(res);
514
+ }
515
+ catch (err) {
516
+ spinner.fail(err.message);
517
+ }
518
+ });
519
+ program
520
+ .command("openapi-bridge-tokens")
521
+ .description("[OpenAPI] 查询跨链桥目标代币")
522
+ .requiredOption("--src-chain <chain>", "源链")
523
+ .requiredOption("--src-token <address>", "源代币合约地址")
524
+ .requiredOption("--dest-chain <chain>", "目标链")
525
+ .option("--search <keyword>", "搜索关键词")
526
+ .action(async (opts) => {
527
+ const srcChainId = resolveChainId(opts.srcChain);
528
+ const destChainId = resolveChainId(opts.destChain);
529
+ if (!srcChainId || !destChainId) {
530
+ console.error(chalk.red("未知链"));
531
+ return;
532
+ }
533
+ const spinner = ora("查询跨链桥代币...").start();
534
+ try {
535
+ const client = requireClient();
536
+ if (!client) {
537
+ spinner.stop();
538
+ return;
539
+ }
540
+ const params = {
541
+ source_chain_id: String(srcChainId),
542
+ source_address: opts.srcToken,
543
+ chain_id: String(destChainId),
544
+ };
545
+ if (opts.search)
546
+ params.search = opts.search;
547
+ const res = await client.call("base.token.bridge_list", params);
548
+ spinner.stop();
549
+ printResult(res);
550
+ }
551
+ catch (err) {
552
+ spinner.fail(err.message);
553
+ }
554
+ });
555
+ // ─── 市场行情类 ────────────────────────────────────────
556
+ program
557
+ .command("openapi-volume")
558
+ .description("[OpenAPI] 查询交易量统计 (5m/1h/4h/24h)")
559
+ .requiredOption("--chain <chain>", "链名或 chain_id")
560
+ .requiredOption("--address <addr>", "代币合约地址")
561
+ .option("--pair <addr>", "交易对地址")
562
+ .action(async (opts) => {
563
+ const chainId = resolveChainId(opts.chain);
564
+ if (!chainId) {
565
+ console.error(chalk.red(`未知链: ${opts.chain}`));
566
+ return;
567
+ }
568
+ const spinner = ora("查询交易量...").start();
569
+ try {
570
+ const client = requireClient();
571
+ if (!client) {
572
+ spinner.stop();
573
+ return;
574
+ }
575
+ const params = {
576
+ chain_id: chainId,
577
+ token_address: opts.address,
578
+ };
579
+ if (opts.pair)
580
+ params.pair_address = opts.pair;
581
+ const res = await client.call("market.volume_stats", params);
582
+ spinner.stop();
583
+ printResult(res);
584
+ }
585
+ catch (err) {
586
+ spinner.fail(err.message);
587
+ }
588
+ });
589
+ program
590
+ .command("openapi-liquidity")
591
+ .description("[OpenAPI] 查询流动性池事件")
592
+ .requiredOption("--chain <chain>", "链名或 chain_id")
593
+ .requiredOption("--address <addr>", "代币合约地址")
594
+ .option("--page <n>", "页码", "1")
595
+ .option("--limit <n>", "每页数量 (最大 15)", "15")
596
+ .action(async (opts) => {
597
+ const chainId = resolveChainId(opts.chain);
598
+ if (!chainId) {
599
+ console.error(chalk.red(`未知链: ${opts.chain}`));
600
+ return;
601
+ }
602
+ const spinner = ora("查询流动性事件...").start();
603
+ try {
604
+ const client = requireClient();
605
+ if (!client) {
606
+ spinner.stop();
607
+ return;
608
+ }
609
+ const res = await client.marketLiquidityList({
610
+ chain_id: chainId,
611
+ token_address: opts.address,
612
+ page_index: Number(opts.page),
613
+ page_size: Number(opts.limit),
614
+ });
615
+ spinner.stop();
616
+ printResult(res);
617
+ }
618
+ catch (err) {
619
+ spinner.fail(err.message);
620
+ }
621
+ });
622
+ // ─── 通用调用 ──────────────────────────────────────────
623
+ program
624
+ .command("openapi-call <action> [json]")
625
+ .description("[OpenAPI] 直接调用任意 OpenAPI action")
626
+ .action(async (action, json) => {
627
+ const client = requireClient();
628
+ if (!client)
629
+ return;
630
+ try {
631
+ const params = json
632
+ ? JSON.parse(json)
633
+ : {};
634
+ const res = await client.call(action, params);
635
+ printResult(res);
636
+ }
637
+ catch (err) {
638
+ console.error(chalk.red(err.message));
639
+ }
640
+ });
641
+ // ─── Hybrid Swap (OpenAPI + MCP signing) ──────────────────
642
+ program
643
+ .command("openapi-swap")
644
+ .description("[OpenAPI+MCP] Hybrid Swap: OpenAPI quote/build/submit + MCP custodial signing")
645
+ .requiredOption("--chain <chain>", "链名或 chain ID (如 ARB, ETH, 42161)")
646
+ .requiredOption("--from <token>", "源代币地址,原生代币用 -")
647
+ .requiredOption("--to <token>", "目标代币合约地址")
648
+ .requiredOption("--amount <n>", "兑换数量 (人类可读格式)")
649
+ .option("--slippage <pct>", "滑点 (0.03 = 3%)", "0.03")
650
+ .option("-y, --yes", "跳过确认直接执行")
651
+ .action(async (opts) => {
652
+ const chainId = resolveChainId(opts.chain);
653
+ if (!chainId) {
654
+ console.error(chalk.red(`未知链: ${opts.chain}`));
655
+ return;
656
+ }
657
+ const client = requireClient();
658
+ if (!client)
659
+ return;
660
+ const slippage = parseFloat(opts.slippage);
661
+ // Step 0: Get wallet address via MCP
662
+ const authSpinner = ora("连接 MCP 获取钱包地址...").start();
663
+ let mcp;
664
+ try {
665
+ mcp = await getMcpClient();
666
+ const stored = loadAuth();
667
+ if (!stored) {
668
+ authSpinner.fail("未登录,请先执行 login");
669
+ return;
670
+ }
671
+ mcp.setMcpToken(stored.mcp_token);
672
+ const addrResult = await mcp.callTool("wallet.get_addresses", {});
673
+ const addrData = extractMcpJson(addrResult);
674
+ const isSolanaChain = resolveChainId(opts.chain) === 501;
675
+ const wallet = isSolanaChain
676
+ ? addrData?.addresses?.SOL
677
+ : addrData?.addresses?.EVM;
678
+ if (!wallet) {
679
+ authSpinner.fail(`无法获取 ${isSolanaChain ? "SOL" : "EVM"} 钱包地址`);
680
+ return;
681
+ }
682
+ authSpinner.succeed(`钱包: ${wallet}`);
683
+ // Step 1: Quote
684
+ const quoteSpinner = ora("获取报价...").start();
685
+ const quoteRes = await client.call("trade.swap.quote", {
686
+ chain_id: chainId,
687
+ token_in: opts.from,
688
+ token_out: opts.to,
689
+ amount_in: opts.amount,
690
+ user_wallet: wallet,
691
+ slippage,
692
+ slippage_type: 1,
693
+ });
694
+ if (quoteRes.code !== 0) {
695
+ quoteSpinner.fail(`报价失败 [${quoteRes.code}]: ${quoteRes.message}`);
696
+ return;
697
+ }
698
+ const q = quoteRes.data;
699
+ quoteSpinner.succeed("报价成功");
700
+ console.log(chalk.cyan("\n========== Swap 报价 =========="));
701
+ console.log(` 卖出: ${q.amount_in} ${q.from_token.token_symbol}`);
702
+ console.log(` 买入: ≈ ${q.amount_out} ${q.to_token.token_symbol}`);
703
+ console.log(` 最少: ${q.min_amount_out} ${q.to_token.token_symbol}`);
704
+ console.log(` 滑点: ${(parseFloat(q.slippage) * 100).toFixed(1)}%`);
705
+ const routes = q.protocols?.[0]?.[0]
706
+ ?.map((p) => `${p.name}(${p.part}%)`)
707
+ .join(" → ") ?? "N/A";
708
+ console.log(` 路由: ${routes}`);
709
+ console.log(chalk.cyan("===============================\n"));
710
+ // Confirm
711
+ if (!opts.yes) {
712
+ const confirmed = await askConfirm("确认执行 Swap?");
713
+ if (!confirmed) {
714
+ console.log(chalk.yellow("已取消"));
715
+ return;
716
+ }
717
+ }
718
+ // Steps 2+: chain-specific flow
719
+ const isSolana = chainId === 501;
720
+ const chainParam = resolveChainParam(chainId);
721
+ let execSpinner = ora("准备交易...").start();
722
+ if (isSolana) {
723
+ // ═══════ Solana Flow: OpenAPI quote → build → MCP sign(base58) → OpenAPI submit ═══════
724
+ // Solana Step 2: Build (no ERC20 approve needed)
725
+ execSpinner.text = "获取最新报价...";
726
+ const solQuoteRes = await client.call("trade.swap.quote", {
727
+ chain_id: chainId,
728
+ token_in: opts.from,
729
+ token_out: opts.to,
730
+ amount_in: opts.amount,
731
+ user_wallet: wallet,
732
+ slippage,
733
+ slippage_type: 1,
734
+ });
735
+ if (solQuoteRes.code !== 0) {
736
+ execSpinner.fail(`报价失败 [${solQuoteRes.code}]: ${solQuoteRes.message}`);
737
+ return;
738
+ }
739
+ const solQ = solQuoteRes.data;
740
+ execSpinner.text = "Build Solana 交易...";
741
+ const solBuildRes = await client.call("trade.swap.build", {
742
+ chain_id: chainId,
743
+ token_in: opts.from,
744
+ token_out: opts.to,
745
+ amount_in: opts.amount,
746
+ user_wallet: wallet,
747
+ slippage,
748
+ slippage_type: 1,
749
+ quote_id: solQ.quote_id,
750
+ });
751
+ if (solBuildRes.code !== 0) {
752
+ execSpinner.fail(`Build 失败 [${solBuildRes.code}]: ${solBuildRes.message}`);
753
+ return;
754
+ }
755
+ const solUtx = solBuildRes.data.unsigned_tx;
756
+ const solOrderId = solBuildRes.data.order_id;
757
+ const unsignedTxBase64 = solUtx.data;
758
+ // Solana Step 3: Sign via MCP wallet.sign_transaction
759
+ // MCP expects base58-encoded VersionedTransaction for SOL
760
+ execSpinner.text = "签名 Solana 交易...";
761
+ const unsignedTxBase58 = base58Encode(Buffer.from(unsignedTxBase64, "base64"));
762
+ const solSignRaw = await mcp.callTool("wallet.sign_transaction", {
763
+ chain: "SOL",
764
+ raw_tx: unsignedTxBase58,
765
+ });
766
+ const solSignResult = extractMcpJson(solSignRaw);
767
+ // signedTransaction is base58-encoded signed VersionedTransaction
768
+ const signedSolTx = solSignResult?.signedTransaction ?? "";
769
+ if (!signedSolTx) {
770
+ execSpinner.fail("Solana 签名失败");
771
+ console.log(chalk.gray("Raw:"), JSON.stringify(solSignRaw, null, 2).slice(0, 800));
772
+ return;
773
+ }
774
+ execSpinner.succeed("签名成功");
775
+ // Solana Step 4: Submit - signed_tx_string for Solana is JSON array of base58 strings
776
+ execSpinner = ora("提交 Solana 交易...").start();
777
+ const solSubmitRes = await client.call("trade.swap.submit", {
778
+ order_id: solOrderId,
779
+ signed_tx_string: JSON.stringify([signedSolTx]),
780
+ });
781
+ if (solSubmitRes.code !== 0) {
782
+ execSpinner.fail(`Submit 失败 [${solSubmitRes.code}]: ${solSubmitRes.message}`);
783
+ return;
784
+ }
785
+ const solTxHash = solSubmitRes.data.tx_hash;
786
+ execSpinner.succeed(`交易已提交: ${solTxHash}`);
787
+ // Solana Step 5: Poll status
788
+ await pollSwapStatus(client, execSpinner, chainId, solOrderId, solTxHash, solQ.to_token, mcp, chainParam);
789
+ }
790
+ else {
791
+ // ═══════ EVM Flow ═══════
792
+ // EVM Step 2: Check ERC20 Approve (before build, so quote won't expire)
793
+ const isNativeIn = opts.from === "-" || q.from_token.is_native_token === 1;
794
+ if (!isNativeIn) {
795
+ execSpinner.text = "检查 ERC20 授权...";
796
+ // Query on-chain allowance: use quote's route_address as spender
797
+ // We use a preliminary build to discover the actual spender, or use the approve_transaction API
798
+ // which handles spender internally. First check with approve_transaction.
799
+ const approveRes = await client.call("trade.swap.approve_transaction", {
800
+ user_wallet: wallet,
801
+ approve_amount: opts.amount,
802
+ quote_id: q.quote_id,
803
+ });
804
+ if (approveRes.code === 0 && approveRes.data) {
805
+ const approveTx = approveRes.data;
806
+ // Check on-chain allowance against the approve_address (spender)
807
+ const ownerPadded = wallet
808
+ .replace("0x", "")
809
+ .toLowerCase()
810
+ .padStart(64, "0");
811
+ const spenderPadded = approveTx.approve_address
812
+ .replace("0x", "")
813
+ .toLowerCase()
814
+ .padStart(64, "0");
815
+ const allowanceData = `0xdd62ed3e${ownerPadded}${spenderPadded}`;
816
+ const allowanceResult = extractMcpJson((await mcp.callTool("rpc.call", {
817
+ chain: chainParam,
818
+ method: "eth_call",
819
+ params: [{ to: opts.from, data: allowanceData }, "latest"],
820
+ })));
821
+ const allowanceRaw = BigInt(allowanceResult?.result ?? "0x0");
822
+ const amountRaw = BigInt(Math.floor(parseFloat(opts.amount) * 10 ** q.from_token.decimal));
823
+ if (allowanceRaw < amountRaw) {
824
+ execSpinner.text = "授权不足,签名 approve 交易...";
825
+ // Get nonce + gas for approve tx
826
+ const approveNonceResult = extractMcpJson((await mcp.callTool("rpc.call", {
827
+ chain: chainParam,
828
+ method: "eth_getTransactionCount",
829
+ params: [wallet, "pending"],
830
+ })));
831
+ const approveNonce = parseInt(approveNonceResult.result, 16);
832
+ const approveGasPriceResult = extractMcpJson((await mcp.callTool("rpc.call", {
833
+ chain: chainParam,
834
+ method: "eth_gasPrice",
835
+ params: [],
836
+ })));
837
+ const approveGasPrice = Math.floor(parseInt(approveGasPriceResult.result, 16) * 1.2);
838
+ const approvePriorityFeeResult = extractMcpJson((await mcp.callTool("rpc.call", {
839
+ chain: chainParam,
840
+ method: "eth_maxPriorityFeePerGas",
841
+ params: [],
842
+ })));
843
+ const approvePriorityFee = Math.max(Math.floor(parseInt(approvePriorityFeeResult?.result ?? "0x1", 16) *
844
+ 1.2), 1);
845
+ const rawApproveTx = "0x02" +
846
+ rlpEncodeEIP1559({
847
+ chainId,
848
+ nonce: approveNonce,
849
+ maxPriorityFeePerGas: approvePriorityFee,
850
+ maxFeePerGas: approveGasPrice,
851
+ gasLimit: parseInt(approveTx.gas_limit, 10) || 100000,
852
+ to: opts.from, // approve tx must target the token contract, not the spender
853
+ value: BigInt(0),
854
+ data: approveTx.data,
855
+ });
856
+ const approveSignResult = extractMcpJson((await mcp.callTool("wallet.sign_transaction", {
857
+ chain: "EVM",
858
+ raw_tx: rawApproveTx,
859
+ })));
860
+ let signedApproveTx = approveSignResult.signedTransaction;
861
+ if (!signedApproveTx.startsWith("0x"))
862
+ signedApproveTx = "0x" + signedApproveTx;
863
+ // Broadcast approve tx via RPC and wait for on-chain confirmation
864
+ execSpinner.text = "广播 approve 交易,等待链上确认...";
865
+ const sendApproveResult = extractMcpJson((await mcp.callTool("rpc.call", {
866
+ chain: chainParam,
867
+ method: "eth_sendRawTransaction",
868
+ params: [signedApproveTx],
869
+ })));
870
+ const approveTxHash = sendApproveResult?.result;
871
+ if (!approveTxHash) {
872
+ execSpinner.fail(`Approve 广播失败: ${JSON.stringify(sendApproveResult)}`);
873
+ return;
874
+ }
875
+ execSpinner.text = `Approve 已广播 (${approveTxHash.slice(0, 10)}...), 等待确认...`;
876
+ let approveConfirmed = false;
877
+ for (let i = 0; i < 30; i++) {
878
+ await sleep(3000);
879
+ const receiptResult = extractMcpJson((await mcp.callTool("rpc.call", {
880
+ chain: chainParam,
881
+ method: "eth_getTransactionReceipt",
882
+ params: [approveTxHash],
883
+ })));
884
+ if (receiptResult?.result?.status === "0x1") {
885
+ approveConfirmed = true;
886
+ break;
887
+ }
888
+ else if (receiptResult?.result?.status === "0x0") {
889
+ execSpinner.fail("Approve 交易链上执行失败 (reverted)");
890
+ return;
891
+ }
892
+ execSpinner.text = `等待 approve 确认... (${i + 1}/30)`;
893
+ }
894
+ if (!approveConfirmed) {
895
+ execSpinner.fail("Approve 确认超时,请稍后重试");
896
+ return;
897
+ }
898
+ execSpinner.succeed(`Approve 已确认 (${approveTxHash})`);
899
+ execSpinner = ora("重新获取报价并构建 swap 交易...").start();
900
+ }
901
+ else {
902
+ execSpinner.text = "授权充足,跳过 approve";
903
+ }
904
+ }
905
+ // If approve_transaction returns error (e.g. native token), just proceed
906
+ }
907
+ // Step 3: Re-quote (fresh quote_id after possible approve delay)
908
+ execSpinner.text = "获取最新报价...";
909
+ const freshQuoteRes = await client.call("trade.swap.quote", {
910
+ chain_id: chainId,
911
+ token_in: opts.from,
912
+ token_out: opts.to,
913
+ amount_in: opts.amount,
914
+ user_wallet: wallet,
915
+ slippage,
916
+ slippage_type: 1,
917
+ });
918
+ if (freshQuoteRes.code !== 0) {
919
+ execSpinner.fail(`报价失败 [${freshQuoteRes.code}]: ${freshQuoteRes.message}`);
920
+ return;
921
+ }
922
+ const freshQ = freshQuoteRes.data;
923
+ // Step 4: Build
924
+ execSpinner.text = "Build...";
925
+ const buildRes = await client.call("trade.swap.build", {
926
+ chain_id: chainId,
927
+ token_in: opts.from,
928
+ token_out: opts.to,
929
+ amount_in: opts.amount,
930
+ user_wallet: wallet,
931
+ slippage,
932
+ slippage_type: 1,
933
+ quote_id: freshQ.quote_id,
934
+ });
935
+ if (buildRes.code !== 0) {
936
+ execSpinner.fail(`Build 失败 [${buildRes.code}]: ${buildRes.message}`);
937
+ return;
938
+ }
939
+ const { unsigned_tx: utx, order_id } = buildRes.data;
940
+ // Step 5: Nonce + Gas for swap tx
941
+ execSpinner.text = "获取 nonce + gasPrice...";
942
+ const nonceResult = extractMcpJson((await mcp.callTool("rpc.call", {
943
+ chain: chainParam,
944
+ method: "eth_getTransactionCount",
945
+ params: [wallet, "pending"],
946
+ })));
947
+ const nonce = parseInt(nonceResult.result, 16);
948
+ const gasPriceResult = extractMcpJson((await mcp.callTool("rpc.call", {
949
+ chain: chainParam,
950
+ method: "eth_gasPrice",
951
+ params: [],
952
+ })));
953
+ const gasPrice = Math.floor(parseInt(gasPriceResult.result, 16) * 1.2);
954
+ const priorityFeeResult = extractMcpJson((await mcp.callTool("rpc.call", {
955
+ chain: chainParam,
956
+ method: "eth_maxPriorityFeePerGas",
957
+ params: [],
958
+ })));
959
+ const priorityFee = Math.max(Math.floor(parseInt(priorityFeeResult?.result ?? "0x1", 16) * 1.2), 1);
960
+ execSpinner.text = "签名交易...";
961
+ // Step 6: RLP encode EIP-1559 unsigned tx
962
+ const rawTx = "0x02" +
963
+ rlpEncodeEIP1559({
964
+ chainId: utx.chain_id,
965
+ nonce,
966
+ maxPriorityFeePerGas: priorityFee,
967
+ maxFeePerGas: gasPrice,
968
+ gasLimit: utx.gas_limit,
969
+ to: utx.to,
970
+ value: BigInt(utx.value),
971
+ data: utx.data,
972
+ });
973
+ // Step 7: MCP sign
974
+ const signResult = extractMcpJson((await mcp.callTool("wallet.sign_transaction", {
975
+ chain: "EVM",
976
+ raw_tx: rawTx,
977
+ })));
978
+ let signedTx = signResult.signedTransaction;
979
+ if (!signedTx.startsWith("0x"))
980
+ signedTx = "0x" + signedTx;
981
+ execSpinner.text = "提交交易...";
982
+ // Step 8: Submit
983
+ const submitRes = await client.call("trade.swap.submit", {
984
+ order_id,
985
+ signed_tx_string: JSON.stringify([signedTx]),
986
+ });
987
+ if (submitRes.code !== 0) {
988
+ execSpinner.fail(`Submit 失败 [${submitRes.code}]: ${submitRes.message}`);
989
+ return;
990
+ }
991
+ const txHash = submitRes.data.tx_hash;
992
+ execSpinner.succeed(`交易已提交: ${txHash}`);
993
+ // EVM Step 9: Poll status
994
+ await pollSwapStatus(client, execSpinner, chainId, order_id, txHash, freshQ.to_token, mcp, chainParam);
995
+ } // end EVM flow
996
+ }
997
+ catch (err) {
998
+ console.error(chalk.red(err.message));
999
+ }
1000
+ });
1001
+ }
1002
+ // ─── Hybrid Swap helpers ───────────────────────────────────
1003
+ function extractMcpJson(result) {
1004
+ if ("content" in result && Array.isArray(result.content)) {
1005
+ for (const item of result.content) {
1006
+ if (item.type === "text") {
1007
+ try {
1008
+ return JSON.parse(item.text);
1009
+ }
1010
+ catch {
1011
+ /* skip */
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+ return null;
1017
+ }
1018
+ function askConfirm(msg) {
1019
+ return new Promise((resolve) => {
1020
+ const rl = createInterface({
1021
+ input: process.stdin,
1022
+ output: process.stdout,
1023
+ });
1024
+ rl.question(`${msg} (y/N): `, (answer) => {
1025
+ rl.close();
1026
+ resolve(answer.trim().toLowerCase() === "y");
1027
+ });
1028
+ });
1029
+ }
1030
+ function sleep(ms) {
1031
+ return new Promise((resolve) => setTimeout(resolve, ms));
1032
+ }
1033
+ const CHAIN_ID_TO_PARAM = {
1034
+ 1: "ETH",
1035
+ 56: "BSC",
1036
+ 137: "POLYGON",
1037
+ 42161: "ARB",
1038
+ 8453: "BASE",
1039
+ 10: "OP",
1040
+ 43114: "AVAX",
1041
+ 501: "SOL",
1042
+ };
1043
+ function resolveChainParam(chainId) {
1044
+ return CHAIN_ID_TO_PARAM[chainId] ?? "ETH";
1045
+ }
1046
+ const CHAIN_EXPLORER = {
1047
+ 1: "https://etherscan.io",
1048
+ 56: "https://bscscan.com",
1049
+ 137: "https://polygonscan.com",
1050
+ 42161: "https://arbiscan.io",
1051
+ 8453: "https://basescan.org",
1052
+ 10: "https://optimistic.etherscan.io",
1053
+ 43114: "https://snowtrace.io",
1054
+ 250: "https://ftmscan.com",
1055
+ 59144: "https://lineascan.build",
1056
+ 534352: "https://scrollscan.com",
1057
+ 324: "https://explorer.zksync.io",
1058
+ 5000: "https://explorer.mantle.xyz",
1059
+ 501: "https://solscan.io",
1060
+ };
1061
+ function getExplorerTxUrl(chainId, txHash) {
1062
+ const base = CHAIN_EXPLORER[chainId] ?? "https://etherscan.io";
1063
+ return `${base}/tx/${txHash}`;
1064
+ }
1065
+ function formatTokenAmount(raw, decimals) {
1066
+ if (!raw || raw === "0")
1067
+ return "0";
1068
+ const n = BigInt(raw);
1069
+ const d = BigInt(10 ** decimals);
1070
+ const whole = n / d;
1071
+ const frac = n % d;
1072
+ const fracStr = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
1073
+ return fracStr ? `${whole}.${fracStr}` : `${whole}`;
1074
+ }
1075
+ // ─── Minimal RLP encoder for EIP-1559 ──────────────────────
1076
+ function rlpEncodeLength(len, offset) {
1077
+ if (len < 56)
1078
+ return Buffer.from([len + offset]);
1079
+ const hexLen = len.toString(16);
1080
+ const lenBytes = Buffer.from(hexLen.length % 2 ? "0" + hexLen : hexLen, "hex");
1081
+ return Buffer.concat([
1082
+ Buffer.from([offset + 55 + lenBytes.length]),
1083
+ lenBytes,
1084
+ ]);
1085
+ }
1086
+ function rlpEncodeItem(data) {
1087
+ if (data.length === 1 && data[0] < 0x80)
1088
+ return data;
1089
+ return Buffer.concat([rlpEncodeLength(data.length, 0x80), data]);
1090
+ }
1091
+ function rlpEncodeList(items) {
1092
+ const payload = Buffer.concat(items);
1093
+ return Buffer.concat([rlpEncodeLength(payload.length, 0xc0), payload]);
1094
+ }
1095
+ function bigintToBuffer(n) {
1096
+ if (n === 0n)
1097
+ return Buffer.alloc(0);
1098
+ let hex = n.toString(16);
1099
+ if (hex.length % 2)
1100
+ hex = "0" + hex;
1101
+ return Buffer.from(hex, "hex");
1102
+ }
1103
+ function intToBuffer(n) {
1104
+ return bigintToBuffer(BigInt(n));
1105
+ }
1106
+ function rlpEncodeEIP1559(tx) {
1107
+ const items = [
1108
+ rlpEncodeItem(intToBuffer(tx.chainId)),
1109
+ rlpEncodeItem(intToBuffer(tx.nonce)),
1110
+ rlpEncodeItem(intToBuffer(tx.maxPriorityFeePerGas)),
1111
+ rlpEncodeItem(intToBuffer(tx.maxFeePerGas)),
1112
+ rlpEncodeItem(intToBuffer(tx.gasLimit)),
1113
+ rlpEncodeItem(Buffer.from(tx.to.replace("0x", ""), "hex")),
1114
+ rlpEncodeItem(bigintToBuffer(tx.value)),
1115
+ rlpEncodeItem(Buffer.from(tx.data.replace("0x", ""), "hex")),
1116
+ rlpEncodeList([]), // access_list = []
1117
+ ];
1118
+ return rlpEncodeList(items).toString("hex");
1119
+ }
1120
+ // ─── Base58 encoder (Bitcoin alphabet) ──────────────────────
1121
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1122
+ function base58Encode(bytes) {
1123
+ let leadingZeros = 0;
1124
+ for (const b of bytes) {
1125
+ if (b !== 0)
1126
+ break;
1127
+ leadingZeros++;
1128
+ }
1129
+ let num = BigInt("0x" + (bytes.length > 0 ? bytes.toString("hex") : "0"));
1130
+ const chars = [];
1131
+ while (num > 0n) {
1132
+ const rem = Number(num % 58n);
1133
+ num = num / 58n;
1134
+ chars.unshift(BASE58_ALPHABET[rem]);
1135
+ }
1136
+ return "1".repeat(leadingZeros) + chars.join("");
1137
+ }
1138
+ async function pollSwapStatus(client, _spinner, chainId, orderId, txHash, toToken, mcp, chainParam) {
1139
+ const pollSpinner = ora("等待链上确认...").start();
1140
+ let finalStatus = "";
1141
+ const isSolana = chainId === 501;
1142
+ for (let i = 0; i < 24; i++) {
1143
+ await sleep(5000);
1144
+ // EVM: check on-chain receipt directly via MCP RPC (faster than OpenAPI status)
1145
+ if (!isSolana && mcp && chainParam) {
1146
+ try {
1147
+ const receiptResult = extractMcpJson((await mcp.callTool("rpc.call", {
1148
+ chain: chainParam,
1149
+ method: "eth_getTransactionReceipt",
1150
+ params: [txHash],
1151
+ })));
1152
+ if (receiptResult?.result?.status === "0x1") {
1153
+ pollSpinner.succeed(`Swap 成功! (链上已确认)`);
1154
+ finalStatus = "success";
1155
+ break;
1156
+ }
1157
+ else if (receiptResult?.result?.status === "0x0") {
1158
+ pollSpinner.fail("Swap 失败: 链上交易 reverted");
1159
+ finalStatus = "failed";
1160
+ break;
1161
+ }
1162
+ }
1163
+ catch {
1164
+ // RPC check failed, fall through to OpenAPI status
1165
+ }
1166
+ }
1167
+ // Solana: query tx detail via MCP (faster than OpenAPI status)
1168
+ if (isSolana && mcp) {
1169
+ try {
1170
+ const rawDetail = (await mcp.callTool("tx.detail", {
1171
+ hash_id: txHash,
1172
+ }));
1173
+ const detailResult = extractMcpJson(rawDetail);
1174
+ const detail = Array.isArray(detailResult) ? detailResult[0] : detailResult;
1175
+ const detailStatus = (detail?.status ??
1176
+ detail?.state ??
1177
+ detail?.tx_status ??
1178
+ "").toLowerCase();
1179
+ if (detailStatus === "success" ||
1180
+ detailStatus === "confirmed" ||
1181
+ detailStatus === "finalized") {
1182
+ pollSpinner.succeed(`Swap 成功! (链上已确认)`);
1183
+ finalStatus = "success";
1184
+ break;
1185
+ }
1186
+ else if (detailStatus === "failed" ||
1187
+ detailStatus === "error" ||
1188
+ detailStatus === "reverted") {
1189
+ pollSpinner.fail(`Swap 失败: 链上交易 ${detailStatus}`);
1190
+ finalStatus = "failed";
1191
+ break;
1192
+ }
1193
+ }
1194
+ catch {
1195
+ // MCP detail check failed, fall through to OpenAPI status
1196
+ }
1197
+ }
1198
+ // Fallback: use OpenAPI status API
1199
+ try {
1200
+ const statusRes = await client.call("trade.swap.status", {
1201
+ chain_id: chainId,
1202
+ order_id: orderId,
1203
+ tx_hash: txHash,
1204
+ });
1205
+ const sd = statusRes.data;
1206
+ if (sd) {
1207
+ // status: 0=pending, 1=success, 2/3=failed; legacy: 200=success, 300/400=failed
1208
+ if (sd.status === 200 || sd.status === 1) {
1209
+ const outHuman = sd.amount_out
1210
+ ? formatTokenAmount(sd.amount_out, toToken.decimal)
1211
+ : null;
1212
+ pollSpinner.succeed(outHuman
1213
+ ? `Swap 成功! 收到 ${outHuman} ${toToken.token_symbol}`
1214
+ : `Swap 成功! (链上已确认)`);
1215
+ finalStatus = "success";
1216
+ break;
1217
+ }
1218
+ else if (sd.status === 300 ||
1219
+ sd.status === 400 ||
1220
+ sd.status === 2 ||
1221
+ sd.status === 3) {
1222
+ pollSpinner.fail(`Swap 失败: ${sd.error_msg || "unknown error"}`);
1223
+ finalStatus = "failed";
1224
+ break;
1225
+ }
1226
+ else if (sd.error_code && sd.error_code !== 0) {
1227
+ pollSpinner.fail(`Swap 失败: ${sd.error_msg}`);
1228
+ finalStatus = "failed";
1229
+ break;
1230
+ }
1231
+ }
1232
+ }
1233
+ catch {
1234
+ // OpenAPI status check failed, continue polling
1235
+ }
1236
+ pollSpinner.text = `等待链上确认... (${i + 1}/24)`;
1237
+ }
1238
+ if (!finalStatus) {
1239
+ pollSpinner.warn("轮询超时,请稍后查询状态");
1240
+ }
1241
+ console.log(chalk.cyan(`\nExplorer: ${getExplorerTxUrl(chainId, txHash)}`));
1242
+ }
1243
+ //# sourceMappingURL=openapi.cmd.js.map