@bankr/cli 0.3.1 → 0.3.8

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.
@@ -5,8 +5,7 @@ import { spawn } from "child_process";
5
5
  import { CLI_USER_AGENT, getApiUrl, getLlmKey, getLlmUrl, requireApiKey, } from "../lib/config.js";
6
6
  import { getBalances } from "../lib/api.js";
7
7
  import * as output from "../lib/output.js";
8
- import { bankrTheme } from "../lib/output.js";
9
- const ERR_LLM_NOT_ENABLED = "LLM Gateway not enabled on this API key. Enable at bankr.bot/api";
8
+ const ERR_LLM_NOT_ENABLED = "LLM Gateway not enabled on this API key. Enable at bankr.bot/api-keys";
10
9
  /**
11
10
  * Fallback model catalog — used when the gateway is unreachable or unauthenticated.
12
11
  * resolveModels() fetches live data from GET /v1/models; this list is the offline safety net.
@@ -171,6 +170,15 @@ const GATEWAY_MODELS = [
171
170
  cost: { input: 0.05, output: 0.4, cacheRead: 0.005, cacheWrite: 0 },
172
171
  },
173
172
  // Other providers
173
+ {
174
+ id: "grok-4.3",
175
+ name: "Grok 4.3",
176
+ owned_by: "x-ai",
177
+ contextWindow: 1000000,
178
+ maxTokens: 1000000,
179
+ input: IMAGE_INPUT,
180
+ cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
181
+ },
174
182
  {
175
183
  id: "grok-4.1-fast",
176
184
  name: "Grok 4.1 Fast",
@@ -198,6 +206,24 @@ const GATEWAY_MODELS = [
198
206
  input: TEXT_INPUT,
199
207
  cost: { input: 0.12, output: 0.75, cacheRead: 0.06, cacheWrite: 0 },
200
208
  },
209
+ {
210
+ id: "qwen3.7-plus",
211
+ name: "Qwen3.7 Plus",
212
+ owned_by: "qwen",
213
+ contextWindow: 1000000,
214
+ maxTokens: 65536,
215
+ input: TEXT_INPUT,
216
+ cost: { input: 0.4, output: 1.6, cacheRead: 0.04, cacheWrite: 0 },
217
+ },
218
+ {
219
+ id: "qwen3.6-flash",
220
+ name: "Qwen3.6 Flash",
221
+ owned_by: "qwen",
222
+ contextWindow: 1000000,
223
+ maxTokens: 65536,
224
+ input: TEXT_INPUT,
225
+ cost: { input: 0.25, output: 1.5, cacheRead: 0, cacheWrite: 0 },
226
+ },
201
227
  {
202
228
  id: "qwen3.5-plus",
203
229
  name: "Qwen3.5 Plus",
@@ -216,6 +242,15 @@ const GATEWAY_MODELS = [
216
242
  input: TEXT_INPUT,
217
243
  cost: { input: 0.1, output: 0.4, cacheRead: 0, cacheWrite: 0 },
218
244
  },
245
+ {
246
+ id: "kimi-k2.7-code",
247
+ name: "Kimi K2.7 Code",
248
+ owned_by: "moonshotai",
249
+ contextWindow: 262144,
250
+ maxTokens: 262144,
251
+ input: TEXT_INPUT,
252
+ cost: { input: 0.95, output: 4.0, cacheRead: 0.19, cacheWrite: 0 },
253
+ },
219
254
  {
220
255
  id: "kimi-k2.6",
221
256
  name: "Kimi K2.6",
@@ -422,6 +457,89 @@ export async function creditsCommand() {
422
457
  }
423
458
  }
424
459
  /* ─────────────────────── bankr llm credits add ──────────────────────────── */
460
+ // Keep in sync with packages/shared/src/chain-config-base/chainConfigBase.ts.
461
+ // CLI is standalone-published and cannot depend on @bankr/shared.
462
+ const TOPUP_CHAINS = ["base", "polygon", "mainnet", "arbitrum", "bnb"];
463
+ const CHAIN_DISPLAY_NAME = {
464
+ base: "Base",
465
+ polygon: "Polygon",
466
+ mainnet: "Ethereum",
467
+ arbitrum: "Arbitrum",
468
+ bnb: "BNB Chain",
469
+ };
470
+ // Polygon's native POL is at a precompile, not the 0xeee placeholder.
471
+ const NATIVE_TOKEN_ADDRESS_BY_CHAIN = {
472
+ base: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
473
+ polygon: "0x0000000000000000000000000000000000001010",
474
+ mainnet: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
475
+ arbitrum: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
476
+ bnb: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
477
+ };
478
+ const BLOCK_EXPLORER_URL = {
479
+ base: "https://basescan.org",
480
+ polygon: "https://polygonscan.com",
481
+ mainnet: "https://etherscan.io",
482
+ arbitrum: "https://arbiscan.io",
483
+ bnb: "https://bscscan.com",
484
+ };
485
+ /**
486
+ * Resolve user input (symbol or 0x address) to the (chain, token) it should
487
+ * pay with. Scans every supported chain and picks the highest USD balance,
488
+ * so the user never has to think about chains. Returns null if nothing matches.
489
+ */
490
+ function resolveTokenAcrossChains(input, balances) {
491
+ const isAddress = input.startsWith("0x");
492
+ const sym = isAddress ? null : input.toUpperCase();
493
+ const candidates = [];
494
+ for (const chain of TOPUP_CHAINS) {
495
+ const chainBal = balances[chain];
496
+ if (!chainBal)
497
+ continue;
498
+ if (!isAddress) {
499
+ const nativeUsd = parseFloat(chainBal.nativeUsd || "0");
500
+ const nativeSym = nativeSymbolFor(chain);
501
+ if (sym === nativeSym && nativeUsd > 0) {
502
+ candidates.push({
503
+ chain,
504
+ address: NATIVE_TOKEN_ADDRESS_BY_CHAIN[chain],
505
+ symbol: nativeSym,
506
+ name: nativeSym,
507
+ decimals: 18,
508
+ balance: parseFloat(chainBal.nativeBalance || "0"),
509
+ balanceUsd: nativeUsd,
510
+ });
511
+ }
512
+ }
513
+ for (const tb of chainBal.tokenBalances) {
514
+ const matchesSym = sym != null && tb.token.baseToken.symbol.toUpperCase() === sym;
515
+ const matchesAddr = isAddress &&
516
+ tb.token.baseToken.address.toLowerCase() === input.toLowerCase();
517
+ if (!matchesSym && !matchesAddr)
518
+ continue;
519
+ candidates.push({
520
+ chain,
521
+ address: tb.token.baseToken.address,
522
+ symbol: tb.token.baseToken.symbol,
523
+ name: tb.token.baseToken.name,
524
+ decimals: tb.token.baseToken.decimals,
525
+ balance: tb.token.balance,
526
+ balanceUsd: tb.token.balanceUSD,
527
+ imageUrl: tb.token.baseToken.imgUrl,
528
+ });
529
+ }
530
+ }
531
+ if (candidates.length === 0)
532
+ return null;
533
+ candidates.sort((a, b) => b.balanceUsd - a.balanceUsd);
534
+ return candidates[0];
535
+ }
536
+ function nativeSymbolFor(chain) {
537
+ if (chain === "polygon")
538
+ return "POL";
539
+ if (chain === "bnb")
540
+ return "BNB";
541
+ return "ETH";
542
+ }
425
543
  export async function creditsAddCommand(amount, opts) {
426
544
  const apiKey = requireApiKey();
427
545
  const apiUrl = getApiUrl();
@@ -433,9 +551,9 @@ export async function creditsAddCommand(amount, opts) {
433
551
  output.error("Amount must be between $1 and $1,000");
434
552
  process.exit(1);
435
553
  }
436
- // Fetch current balance for display (skip when --yes since user won't see it)
554
+ // Fetch current balance for display (skip when --yes/--not-interactive since user won't see it)
437
555
  let currentBalance = "unknown";
438
- if (llmKey && !opts.yes) {
556
+ if (llmKey && !opts.yes && !output.isNonInteractive()) {
439
557
  const balSpin = output.spinner("Fetching balance…");
440
558
  try {
441
559
  const balRes = await fetch(`${llmUrl}/v1/credits`, {
@@ -457,18 +575,41 @@ export async function creditsAddCommand(amount, opts) {
457
575
  output.dim(` (Could not fetch balance: ${err.message})`);
458
576
  }
459
577
  }
578
+ // When --token is passed, the chain holding the highest USD balance of
579
+ // that symbol/address wins. Default (no --token) is Base USDC.
580
+ let resolved = null;
581
+ if (opts.token) {
582
+ const resolveSpin = output.spinner("Resolving token…");
583
+ try {
584
+ const balData = await getBalances([...TOPUP_CHAINS]);
585
+ resolved = resolveTokenAcrossChains(opts.token, balData.balances);
586
+ resolveSpin.stop();
587
+ }
588
+ catch (err) {
589
+ resolveSpin.stop();
590
+ output.warn(`Could not fetch balances: ${err.message}`);
591
+ }
592
+ if (!resolved) {
593
+ output.error(`Token "${opts.token}" not found in your wallet on any supported chain.`);
594
+ process.exit(1);
595
+ }
596
+ }
597
+ const targetChain = resolved?.chain ?? "base";
598
+ const sourceTokenLabel = resolved
599
+ ? `${resolved.symbol} on ${CHAIN_DISPLAY_NAME[targetChain]}`
600
+ : `USDC on ${CHAIN_DISPLAY_NAME.base} (default)`;
460
601
  // Show confirmation
461
602
  console.log();
462
603
  output.label("Add Credits", `$${amountUsd.toFixed(2)}`);
463
- output.keyValue("Source token", opts.token ?? "USDC (default)");
604
+ output.keyValue("Pay with", sourceTokenLabel);
464
605
  output.keyValue("Current balance", currentBalance);
465
606
  console.log();
466
- if (!opts.yes) {
607
+ if (!opts.yes && !output.isNonInteractive()) {
467
608
  const { confirm } = await import("@inquirer/prompts");
468
609
  const proceed = await confirm({
469
610
  message: "Proceed?",
470
611
  default: true,
471
- theme: bankrTheme,
612
+ theme: output.bankrTheme,
472
613
  });
473
614
  if (!proceed) {
474
615
  output.dim("Cancelled.");
@@ -477,9 +618,12 @@ export async function creditsAddCommand(amount, opts) {
477
618
  }
478
619
  const spin = output.spinner("Adding credits…");
479
620
  try {
480
- const body = { amountUsd };
481
- if (opts.token) {
482
- body.sourceToken = opts.token;
621
+ const body = {
622
+ amountUsd,
623
+ chain: targetChain,
624
+ };
625
+ if (resolved) {
626
+ body.sourceToken = resolved.address;
483
627
  }
484
628
  const res = await fetch(`${apiUrl}/llm/credits/topup`, {
485
629
  method: "POST",
@@ -516,7 +660,7 @@ export async function creditsAddCommand(amount, opts) {
516
660
  output.success(`Added $${result.creditsGranted.toFixed(2)} credits`);
517
661
  output.label("New Balance", `$${result.newBalance.toFixed(2)}`);
518
662
  if (result.txHash) {
519
- output.keyValue("Transaction", `https://basescan.org/tx/${result.txHash}`);
663
+ output.keyValue("Transaction", `${BLOCK_EXPLORER_URL[targetChain]}/tx/${result.txHash}`);
520
664
  }
521
665
  }
522
666
  catch (err) {
@@ -632,45 +776,27 @@ export async function creditsAutoCommand(opts) {
632
776
  process.exit(1);
633
777
  }
634
778
  const resolved = [];
635
- // Fetch balances to resolve both symbols and addresses
779
+ // Highest USD balance wins when the same symbol exists on multiple chains.
636
780
  const spin = output.spinner("Resolving tokens…");
637
- let baseTokens = [];
638
- let hasNativeEth = false;
781
+ let balances = {};
639
782
  try {
640
- const balData = await getBalances(["base"]);
641
- const baseChain = balData.balances.base;
642
- baseTokens = baseChain?.tokenBalances ?? [];
643
- hasNativeEth = !!baseChain && parseFloat(baseChain.nativeBalance) > 0;
783
+ const balData = await getBalances([...TOPUP_CHAINS]);
784
+ balances = balData.balances;
644
785
  }
645
786
  catch {
646
787
  output.warn("Could not fetch balances for token resolution");
647
788
  }
648
789
  spin.stop();
649
790
  for (const input of inputs) {
650
- const isAddress = input.startsWith("0x");
651
- const sym = isAddress ? null : input.toUpperCase();
652
- // Native ETH
653
- if (sym === "ETH" && hasNativeEth) {
654
- resolved.push({
655
- address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
656
- chain: "base",
657
- symbol: "ETH",
658
- name: "Ethereum",
659
- decimals: 18,
660
- });
661
- continue;
662
- }
663
- const tb = baseTokens.find((t) => isAddress
664
- ? t.token.baseToken.address.toLowerCase() === input.toLowerCase()
665
- : t.token.baseToken.symbol.toUpperCase() === sym);
666
- if (tb) {
791
+ const hit = resolveTokenAcrossChains(input, balances);
792
+ if (hit) {
667
793
  resolved.push({
668
- address: tb.token.baseToken.address,
669
- chain: "base",
670
- symbol: tb.token.baseToken.symbol,
671
- name: tb.token.baseToken.name,
672
- decimals: tb.token.baseToken.decimals,
673
- imageUrl: tb.token.baseToken.imgUrl,
794
+ address: hit.address,
795
+ chain: hit.chain,
796
+ symbol: hit.symbol,
797
+ name: hit.name,
798
+ decimals: hit.decimals,
799
+ imageUrl: hit.imageUrl,
674
800
  });
675
801
  }
676
802
  else {
@@ -13,7 +13,6 @@ export declare function loginCommand(opts: {
13
13
  url?: boolean;
14
14
  siwe?: boolean;
15
15
  privateKey?: string;
16
- partnerKey?: string;
17
16
  allowedIps?: string;
18
17
  allowedRecipients?: string;
19
18
  }): Promise<void>;
@@ -4,7 +4,7 @@ import open from "open";
4
4
  import { isAddress } from "viem";
5
5
  import { CLI_USER_AGENT, DEFAULT_API_URL, getApiUrl, getConfigPath, readConfig, writeConfig, } from "../lib/config.js";
6
6
  import * as output from "../lib/output.js";
7
- const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api";
7
+ const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api-keys";
8
8
  const MAX_OTP_ATTEMPTS = 3;
9
9
  const INVALID_CODE_STATUSES = [400, 401, 422];
10
10
  function fatal(message) {
@@ -15,7 +15,8 @@ function deriveDashboardUrl(apiUrl) {
15
15
  if (apiUrl === DEFAULT_API_URL) {
16
16
  return DEFAULT_DASHBOARD_URL;
17
17
  }
18
- return (apiUrl.replace(/\/+$/, "").replace("api.", "").replace("api-", "") + "/api");
18
+ return (apiUrl.replace(/\/+$/, "").replace("api.", "").replace("api-", "") +
19
+ "/api-keys");
19
20
  }
20
21
  async function fetchPrivyConfig(apiUrl) {
21
22
  const res = await fetch(`${apiUrl}/cli/config`, {
@@ -220,8 +221,8 @@ async function emailLoginFlow(apiUrl, opts) {
220
221
  if (wallet.solAddress) {
221
222
  output.dim(` SOL: ${wallet.solAddress}`);
222
223
  }
223
- // Headless mode: when --code is provided, skip all interactive prompts
224
- const headless = !!opts.code;
224
+ // Headless mode: when --code is provided or --ni is set, skip prompts
225
+ const headless = !!opts.code || output.isNonInteractive();
225
226
  // Step 3: Accept terms if needed
226
227
  if (!wallet.hasAcceptedTerms) {
227
228
  output.blank();
@@ -263,7 +264,7 @@ async function emailLoginFlow(apiUrl, opts) {
263
264
  // Fine-grained API flags. walletApi and tokenLaunch default to enabled.
264
265
  // --read-write only controls readOnly, not which APIs are enabled.
265
266
  const enableWallet = opts.walletApi ?? true;
266
- // Agent API must be enabled from the website (bankr.bot/api), not the CLI.
267
+ // Agent API must be enabled from the website (bankr.bot/api-keys), not the CLI.
267
268
  const enableAgent = false;
268
269
  const enableTokenLaunch = opts.tokenLaunch ?? true;
269
270
  const enableLlm = opts.llm ??
@@ -317,8 +318,8 @@ async function emailLoginFlow(apiUrl, opts) {
317
318
  ];
318
319
  output.dim(` Recipients: ${parts.join(", ")}`);
319
320
  }
320
- output.dim(" Agent API: enable at bankr.bot/api");
321
- output.dim(" Manage keys at bankr.bot/api");
321
+ output.dim(" Agent API: enable at bankr.bot/api-keys");
322
+ output.dim(" Manage keys at bankr.bot/api-keys");
322
323
  return apiKeyResult.apiKey;
323
324
  }
324
325
  // ── SIWE login flow ─────────────────────────────────────────────────
@@ -365,7 +366,6 @@ async function siweLoginFlow(apiUrl, opts) {
365
366
  body: JSON.stringify({
366
367
  message,
367
368
  signature,
368
- partnerApiKey: opts.partnerKey,
369
369
  keyName: opts.keyName ?? `SIWE-${new Date().toISOString().slice(0, 10)}`,
370
370
  readOnly: opts.readOnly ?? false,
371
371
  walletApiEnabled: opts.walletApi ?? true,
@@ -395,7 +395,7 @@ async function siweLoginFlow(apiUrl, opts) {
395
395
  async function validateApiKey(apiUrl, apiKey) {
396
396
  const spin = output.spinner("Validating Bankr API key...");
397
397
  try {
398
- const res = await fetch(`${apiUrl}/agent/me`, {
398
+ const res = await fetch(`${apiUrl}/wallet/me`, {
399
399
  headers: { "X-API-Key": apiKey, "User-Agent": CLI_USER_AGENT },
400
400
  });
401
401
  if (res.ok) {
@@ -404,20 +404,14 @@ async function validateApiKey(apiUrl, apiKey) {
404
404
  else if (res.status === 401 || res.status === 403) {
405
405
  const body = (await res.json().catch(() => ({})));
406
406
  const errorType = body.error ?? "";
407
- if (res.status === 403 || errorType === "Agent API access not enabled") {
408
- spin.warn("Agent API access is not enabled");
409
- output.warn("Your API key is valid but does not have Agent API access enabled.");
410
- output.info("Go to https://bankr.bot/api and enable Agent API access for this key.");
411
- output.info("Your key has been saved — once enabled, the CLI will work without re-login.");
412
- }
413
- else if (errorType === "Authentication required") {
407
+ if (errorType === "Authentication required") {
414
408
  spin.fail("API key is not linked to a wallet");
415
- output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api");
409
+ output.error("This API key is not associated with a wallet. Generate a new key at https://bankr.bot/api-keys");
416
410
  process.exit(1);
417
411
  }
418
412
  else {
419
413
  spin.fail("Invalid Bankr API key");
420
- output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api");
414
+ output.error("The provided API key is invalid or inactive. Generate a new key at https://bankr.bot/api-keys");
421
415
  process.exit(1);
422
416
  }
423
417
  }
@@ -497,13 +491,28 @@ export async function loginCommand(opts) {
497
491
  if (opts.apiKey !== undefined && !opts.apiKey.trim()) {
498
492
  fatal("--api-key requires a non-empty value");
499
493
  }
494
+ // Non-interactive mode requires a headless flag combo. Without one, the
495
+ // command would hang on a prompt — fail fast with the available paths.
496
+ // Email step 1 (`login email <addr>` without --code) is also headless:
497
+ // it sends the OTP and exits.
498
+ if (output.isNonInteractive()) {
499
+ const hasHeadlessFlag = opts.apiKey ||
500
+ opts.url ||
501
+ (opts.siwe && opts.privateKey) ||
502
+ (opts.email && opts.email.trim());
503
+ if (!hasHeadlessFlag) {
504
+ output.error("Non-interactive login requires one of: --api-key <key>, login siwe --private-key <key>, or login email <address> [--code <otp>]");
505
+ process.exit(1);
506
+ }
507
+ }
500
508
  const config = readConfig();
501
509
  const resolvedApiUrl = opts.apiUrl ?? getApiUrl();
502
510
  if (resolvedApiUrl !== DEFAULT_API_URL) {
503
511
  const nonInteractive = !!(opts.apiKey ||
504
512
  opts.url ||
505
513
  opts.email !== undefined ||
506
- opts.siwe);
514
+ opts.siwe ||
515
+ output.isNonInteractive());
507
516
  if (nonInteractive) {
508
517
  config.apiUrl = resolvedApiUrl;
509
518
  }
@@ -557,7 +566,6 @@ export async function loginCommand(opts) {
557
566
  }
558
567
  const { apiKey } = await siweLoginFlow(apiUrl, {
559
568
  privateKey: opts.privateKey,
560
- partnerKey: opts.partnerKey,
561
569
  keyName: opts.keyName,
562
570
  readOnly: !opts.readWrite,
563
571
  walletApi: opts.walletApi,
@@ -565,9 +573,6 @@ export async function loginCommand(opts) {
565
573
  ...getParsedRestrictions(),
566
574
  });
567
575
  config.apiKey = apiKey;
568
- if (opts.partnerKey) {
569
- config.partnerKey = opts.partnerKey;
570
- }
571
576
  writeConfig(config);
572
577
  output.success(`Credentials saved to ${getConfigPath()}`);
573
578
  return;
@@ -6,6 +6,9 @@ import * as output from "../lib/output.js";
6
6
  * Must be manually kept in sync with models in the LLM gateway DB. */
7
7
  const VALID_MAX_MODE_MODELS = new Set([
8
8
  // Claude (Vertex AI)
9
+ "claude-fable-5",
10
+ "claude-opus-4.8",
11
+ "claude-opus-4.7",
9
12
  "claude-opus-4.6",
10
13
  "claude-opus-4.5",
11
14
  "claude-sonnet-4.6",
@@ -26,11 +29,13 @@ const VALID_MAX_MODE_MODELS = new Set([
26
29
  "deepseek-v3.2",
27
30
  // Qwen (OpenRouter)
28
31
  "qwen3-coder",
32
+ "qwen3.7-plus",
29
33
  "qwen3.5-plus",
30
34
  // MiniMax (direct)
31
35
  "minimax-m2.5",
32
36
  "minimax-m2.7",
33
37
  // Other (OpenRouter)
38
+ "kimi-k2.7-code",
34
39
  "kimi-k2.6",
35
40
  "kimi-k2.5",
36
41
  "glm-5",
@@ -0,0 +1,8 @@
1
+ export declare function swapCommand(opts: {
2
+ from: string;
3
+ to: string;
4
+ amount: string;
5
+ chain?: string;
6
+ quoteOnly?: boolean;
7
+ }): Promise<void>;
8
+ //# sourceMappingURL=swap.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { getSwapQuote, executeSwap } from "../lib/api.js";
2
+ import * as output from "../lib/output.js";
3
+ import { NATIVE_SYMBOLS, VALID_CHAINS } from "../lib/chains.js";
4
+ import { ZERO_ADDRESS, resolveTokenSymbol } from "../lib/tokens.js";
5
+ /**
6
+ * Resolve a token symbol or address to a contract address for swapping.
7
+ * - The chain's native ticker (ETH/POL/BNB) or the zero address → native, sent
8
+ * as the zero address (the API detects + sanitizes native tokens).
9
+ * - Otherwise → symbol/address resolution via the shared resolver.
10
+ */
11
+ async function resolveSwapToken(tokenInput, chain) {
12
+ const input = tokenInput.trim();
13
+ const nativeSymbol = NATIVE_SYMBOLS[chain] ?? "ETH";
14
+ const upper = input.toUpperCase();
15
+ const isNative = upper === nativeSymbol.toUpperCase() ||
16
+ input.toLowerCase() === ZERO_ADDRESS;
17
+ if (isNative) {
18
+ return { address: ZERO_ADDRESS, symbol: nativeSymbol };
19
+ }
20
+ return resolveTokenSymbol(input, chain);
21
+ }
22
+ export async function swapCommand(opts) {
23
+ const chain = opts.chain ?? "base";
24
+ if (!VALID_CHAINS.has(chain) || chain === "solana") {
25
+ output.error(`Unsupported chain "${chain}". Swaps support EVM chains: base, polygon, mainnet, unichain, worldchain, arbitrum, bnb.`);
26
+ process.exit(1);
27
+ }
28
+ // Validate amount client-side so a bad value gives a clear error instead of a
29
+ // generic 500 from the API's parseUnits(amount, decimals). Number() alone
30
+ // accepts "1e-3" / "0x10" / "Infinity" that parseUnits rejects, so require a
31
+ // plain decimal string.
32
+ if (!/^\d*\.?\d+$/.test(opts.amount) || Number(opts.amount) <= 0) {
33
+ output.error(`Invalid amount "${opts.amount}". Provide a positive decimal number, e.g. --amount 0.5.`);
34
+ process.exit(1);
35
+ }
36
+ // Resolve from/to tokens (symbol → address, native → zero address)
37
+ let fromToken;
38
+ let toToken;
39
+ const resolveSpin = output.spinner(`Resolving ${opts.from} → ${opts.to} on ${chain}...`);
40
+ try {
41
+ [fromToken, toToken] = await Promise.all([
42
+ resolveSwapToken(opts.from, chain),
43
+ resolveSwapToken(opts.to, chain),
44
+ ]);
45
+ resolveSpin.stop();
46
+ }
47
+ catch (err) {
48
+ resolveSpin.fail(err.message);
49
+ process.exit(1);
50
+ }
51
+ // Quote
52
+ const quoteSpin = output.spinner(`Fetching quote for ${opts.amount} ${fromToken.symbol} → ${toToken.symbol}...`);
53
+ let quote;
54
+ try {
55
+ quote = await getSwapQuote({
56
+ fromChain: chain,
57
+ fromToken: fromToken.address,
58
+ toChain: chain,
59
+ toToken: toToken.address,
60
+ amount: opts.amount,
61
+ });
62
+ quoteSpin.stop();
63
+ }
64
+ catch (err) {
65
+ quoteSpin.fail(`Quote failed: ${err.message}`);
66
+ process.exit(1);
67
+ }
68
+ output.blank();
69
+ output.label("You pay", `${output.formatBalance(quote.from.formattedAmount)} ${quote.from.symbol} (${output.formatUsd(quote.from.usdValue)})`);
70
+ output.label("You receive", `~${output.formatBalance(quote.to.formattedAmount)} ${quote.to.symbol} (${output.formatUsd(quote.to.usdValue)})`);
71
+ output.keyValue("Min received", `${output.formatBalance(quote.minBuyAmount)} ${quote.to.symbol}`);
72
+ output.blank();
73
+ if (opts.quoteOnly) {
74
+ return;
75
+ }
76
+ // Execute (uses the minBuyAmount from the quote as slippage protection)
77
+ const swapSpin = output.spinner(`Swapping ${opts.amount} ${fromToken.symbol} → ${toToken.symbol}...`);
78
+ try {
79
+ const result = await executeSwap({
80
+ fromChain: chain,
81
+ fromToken: fromToken.address,
82
+ toChain: chain,
83
+ toToken: toToken.address,
84
+ amount: opts.amount,
85
+ minBuyAmount: quote.minBuyAmount,
86
+ });
87
+ if (!result.success) {
88
+ swapSpin.fail(`Swap failed: ${result.error ?? "unknown error"}`);
89
+ // A reverted tx can come back as { success: false } with a hash — surface
90
+ // it so the user can inspect the failed transaction on-chain.
91
+ if (result.hash) {
92
+ output.label("Tx Hash", result.hash);
93
+ }
94
+ process.exit(1);
95
+ }
96
+ swapSpin.succeed("Swap successful");
97
+ output.label("Tx Hash", result.hash ?? "unknown");
98
+ if (result.amountReceived != null) {
99
+ output.label("Received", `${output.formatBalance(result.amountReceived)} ${toToken.symbol}`);
100
+ }
101
+ }
102
+ catch (err) {
103
+ swapSpin.fail(`Swap failed: ${err.message}`);
104
+ process.exit(1);
105
+ }
106
+ }
107
+ //# sourceMappingURL=swap.js.map