@bankr/cli 0.1.3 → 0.2.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.
@@ -125,6 +125,24 @@ const GATEWAY_MODELS = [
125
125
  input: IMAGE_INPUT,
126
126
  cost: { input: 2.5, output: 15.0, cacheRead: 0.25, cacheWrite: 0 },
127
127
  },
128
+ {
129
+ id: "gpt-5.4-mini",
130
+ name: "GPT 5.4 Mini",
131
+ owned_by: "openai",
132
+ contextWindow: 400000,
133
+ maxTokens: 128000,
134
+ input: IMAGE_INPUT,
135
+ cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
136
+ },
137
+ {
138
+ id: "gpt-5.4-nano",
139
+ name: "GPT 5.4 Nano",
140
+ owned_by: "openai",
141
+ contextWindow: 400000,
142
+ maxTokens: 128000,
143
+ input: IMAGE_INPUT,
144
+ cost: { input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0 },
145
+ },
128
146
  {
129
147
  id: "gpt-5.2",
130
148
  name: "GPT 5.2",
@@ -225,6 +243,24 @@ const GATEWAY_MODELS = [
225
243
  input: TEXT_INPUT,
226
244
  cost: { input: 0.27, output: 0.95, cacheRead: 0.03, cacheWrite: 0 },
227
245
  },
246
+ {
247
+ id: "minimax-m2.7",
248
+ name: "MiniMax M2.7",
249
+ owned_by: "minimax",
250
+ contextWindow: 204800,
251
+ maxTokens: 131072,
252
+ input: TEXT_INPUT,
253
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
254
+ },
255
+ {
256
+ id: "glm-5",
257
+ name: "GLM-5",
258
+ owned_by: "z-ai",
259
+ contextWindow: 202752,
260
+ maxTokens: 131072,
261
+ input: TEXT_INPUT,
262
+ cost: { input: 0.72, output: 2.3, cacheRead: 0, cacheWrite: 0 },
263
+ },
228
264
  ];
229
265
  /** Fetch live model list from the gateway; falls back to hardcoded catalog. */
230
266
  async function resolveModels() {
@@ -261,6 +297,7 @@ async function resolveModels() {
261
297
  cacheRead: 0,
262
298
  cacheWrite: 0,
263
299
  }),
300
+ ...(m.deprecation && { deprecation: m.deprecation }),
264
301
  };
265
302
  });
266
303
  return { models, live: true };
@@ -306,10 +343,17 @@ export async function modelsCommand() {
306
343
  output.brandBold("Bankr LLM Gateway — Available Models");
307
344
  console.log();
308
345
  const COL = { id: 24, name: 24, provider: 12 };
309
- console.log(` ${output.fmt.brandBold("Model ID".padEnd(COL.id))} ${output.fmt.brandBold("Name".padEnd(COL.name))} ${output.fmt.brandBold("Provider")}`);
310
- console.log(output.fmt.dim(` ${"".repeat(COL.id + COL.name + COL.provider + 2)}`));
346
+ const hasDeprecated = models.some((m) => m.deprecation);
347
+ console.log(` ${output.fmt.brandBold("Model ID".padEnd(COL.id))} ${output.fmt.brandBold("Name".padEnd(COL.name))} ${output.fmt.brandBold("Provider".padEnd(COL.provider))}${hasDeprecated ? ` ${output.fmt.brandBold("Notes")}` : ""}`);
348
+ console.log(output.fmt.dim(` ${"─".repeat(COL.id + COL.name + COL.provider + (hasDeprecated ? 24 : 2))}`));
311
349
  for (const m of models) {
312
- console.log(` ${output.fmt.brand(m.id.padEnd(COL.id))} ${m.name.padEnd(COL.name)} ${output.fmt.dim(m.owned_by)}`);
350
+ let notes = "";
351
+ if (m.deprecation) {
352
+ notes = m.deprecation.replaced_by
353
+ ? `deprecated → ${m.deprecation.replaced_by}`
354
+ : "deprecated";
355
+ }
356
+ console.log(` ${output.fmt.brand(m.id.padEnd(COL.id))} ${m.name.padEnd(COL.name)} ${output.fmt.dim(m.owned_by.padEnd(COL.provider))} ${notes ? output.fmt.dim(notes) : ""}`);
313
357
  }
314
358
  console.log();
315
359
  output.dim(` Gateway: ${getLlmUrl()}`);
@@ -6,11 +6,16 @@ export declare function loginCommand(opts: {
6
6
  acceptTerms?: boolean;
7
7
  keyName?: string;
8
8
  readWrite?: boolean;
9
+ walletApi?: boolean;
10
+ agentApi?: boolean;
11
+ tokenLaunch?: boolean;
9
12
  llm?: boolean;
10
13
  llmKey?: string;
11
14
  url?: boolean;
12
15
  siwe?: boolean;
13
16
  privateKey?: string;
14
17
  partnerKey?: string;
18
+ allowedIps?: string;
19
+ allowedRecipients?: string;
15
20
  }): Promise<void>;
16
21
  //# sourceMappingURL=login.d.ts.map
@@ -1,5 +1,7 @@
1
1
  import { confirm, input, select } from "@inquirer/prompts";
2
+ import { isIP } from "node:net";
2
3
  import open from "open";
4
+ import { isAddress } from "viem";
3
5
  import { CLI_USER_AGENT, DEFAULT_API_URL, getApiUrl, getConfigPath, readConfig, writeConfig, } from "../lib/config.js";
4
6
  import * as output from "../lib/output.js";
5
7
  const DEFAULT_DASHBOARD_URL = "https://bankr.bot/api";
@@ -218,17 +220,21 @@ async function emailLoginFlow(apiUrl, opts) {
218
220
  if (wallet.solAddress) {
219
221
  output.dim(` SOL: ${wallet.solAddress}`);
220
222
  }
223
+ // Headless mode: when --code is provided, skip all interactive prompts
224
+ const headless = !!opts.code;
221
225
  // Step 3: Accept terms if needed
222
226
  if (!wallet.hasAcceptedTerms) {
223
227
  output.blank();
224
228
  output.dim(" Please review our Terms of Service: https://bankr.bot/terms");
225
229
  output.blank();
226
230
  const accepted = opts.acceptTerms ??
227
- (await confirm({
228
- message: "Do you accept the Terms of Service?",
229
- default: false,
230
- theme: output.bankrTheme,
231
- }));
231
+ (headless
232
+ ? false
233
+ : await confirm({
234
+ message: "Do you accept the Terms of Service?",
235
+ default: false,
236
+ theme: output.bankrTheme,
237
+ }));
232
238
  if (!accepted) {
233
239
  fatal("Terms must be accepted to continue.");
234
240
  }
@@ -246,37 +252,49 @@ async function emailLoginFlow(apiUrl, opts) {
246
252
  output.blank();
247
253
  const defaultKeyName = `CLI-${new Date().toISOString().slice(0, 10)}`;
248
254
  const keyName = opts.keyName ??
249
- (await input({
250
- message: "Key name:",
251
- default: defaultKeyName,
252
- theme: output.bankrTheme,
253
- validate: (v) => v.trim().length >= 3 ? true : "Name must be at least 3 characters.",
254
- }));
255
- const enableWrite = opts.readWrite ??
256
- (await confirm({
257
- message: "Enable write operations?",
258
- default: false,
259
- theme: output.bankrTheme,
260
- }));
261
- if (enableWrite) {
262
- output.dim(" Includes: transactions, transfers, automations, order management");
263
- }
264
- else {
265
- output.dim(" Read-only: portfolio, balances, prices, research");
266
- }
255
+ (headless
256
+ ? defaultKeyName
257
+ : await input({
258
+ message: "Key name:",
259
+ default: defaultKeyName,
260
+ theme: output.bankrTheme,
261
+ validate: (v) => v.trim().length >= 3 ? true : "Name must be at least 3 characters.",
262
+ }));
263
+ // Fine-grained API flags. walletApi and tokenLaunch default to enabled.
264
+ // --read-write only controls readOnly, not which APIs are enabled.
265
+ const enableWallet = opts.walletApi ?? true;
266
+ const enableAgent = opts.agentApi ??
267
+ (headless
268
+ ? false
269
+ : await confirm({
270
+ message: "Enable Agent API? (AI prompts)",
271
+ default: false,
272
+ theme: output.bankrTheme,
273
+ }));
274
+ const enableTokenLaunch = opts.tokenLaunch ?? true;
267
275
  const enableLlm = opts.llm ??
268
- (await confirm({
269
- message: "Enable LLM gateway?",
270
- default: false,
271
- theme: output.bankrTheme,
272
- }));
276
+ (headless
277
+ ? false
278
+ : await confirm({
279
+ message: "Enable LLM gateway?",
280
+ default: false,
281
+ theme: output.bankrTheme,
282
+ }));
283
+ if (opts.readWrite && !enableWallet && !enableAgent) {
284
+ output.warn("--read-write has no effect when both Wallet API and Agent API are disabled");
285
+ }
273
286
  const keySpin = output.spinner("Generating API key...");
274
287
  let apiKeyResult;
275
288
  try {
276
289
  apiKeyResult = await callGenerateApiKey(apiUrl, identityToken, {
277
290
  name: keyName.trim(),
278
- agentApiEnabled: { readOnly: !enableWrite },
291
+ walletApiEnabled: enableWallet,
292
+ agentApiEnabled: enableAgent,
293
+ readOnly: opts.readWrite ? false : undefined,
294
+ tokenLaunchApiEnabled: enableTokenLaunch,
279
295
  llmGatewayEnabled: enableLlm,
296
+ allowedIps: opts.allowedIps,
297
+ allowedRecipients: opts.allowedRecipients,
280
298
  });
281
299
  keySpin.stop();
282
300
  }
@@ -286,10 +304,25 @@ async function emailLoginFlow(apiUrl, opts) {
286
304
  }
287
305
  // Show result
288
306
  output.blank();
289
- const mode = enableWrite ? "read-write" : "read-only";
307
+ const features = [
308
+ enableWallet && "Wallet",
309
+ enableAgent && "Agent",
310
+ enableTokenLaunch && "Token Launch",
311
+ enableLlm && "LLM",
312
+ ].filter(Boolean);
290
313
  output.success(`API key "${apiKeyResult.name}" saved`);
291
- output.dim(` Mode: ${mode}`);
314
+ output.dim(` Features: ${features.length ? features.join(", ") : "none"}`);
292
315
  output.dim(` LLM: ${apiKeyResult.llmGatewayEnabled ? "enabled" : "disabled"}`);
316
+ if (opts.allowedIps) {
317
+ output.dim(` IPs: ${opts.allowedIps.join(", ")}`);
318
+ }
319
+ if (opts.allowedRecipients) {
320
+ const parts = [
321
+ ...(opts.allowedRecipients.evm ?? []),
322
+ ...(opts.allowedRecipients.solana ?? []),
323
+ ];
324
+ output.dim(` Recipients: ${parts.join(", ")}`);
325
+ }
293
326
  output.dim(" Manage keys at bankr.bot/api");
294
327
  return apiKeyResult.apiKey;
295
328
  }
@@ -340,6 +373,11 @@ async function siweLoginFlow(apiUrl, opts) {
340
373
  partnerApiKey: opts.partnerKey,
341
374
  keyName: opts.keyName ?? `SIWE-${new Date().toISOString().slice(0, 10)}`,
342
375
  readOnly: opts.readOnly ?? false,
376
+ walletApiEnabled: opts.walletApi ?? true,
377
+ agentApiEnabled: opts.agentApi ?? false,
378
+ tokenLaunchApiEnabled: opts.tokenLaunch ?? true,
379
+ allowedIps: opts.allowedIps,
380
+ allowedRecipients: opts.allowedRecipients,
343
381
  }),
344
382
  });
345
383
  if (!verifyRes.ok) {
@@ -397,6 +435,49 @@ async function validateApiKey(apiUrl, apiKey) {
397
435
  }
398
436
  }
399
437
  // ── Main login command ──────────────────────────────────────────────
438
+ function parseAndValidateIps(raw) {
439
+ const ips = raw
440
+ .split(",")
441
+ .map((s) => s.trim())
442
+ .filter(Boolean);
443
+ const invalid = ips.filter((ip) => isIP(ip) === 0);
444
+ if (invalid.length > 0) {
445
+ fatal(`Invalid IP address(es): ${invalid.join(", ")}`);
446
+ }
447
+ return ips;
448
+ }
449
+ function parseAndValidateRecipients(raw) {
450
+ const addrs = raw
451
+ .split(",")
452
+ .map((s) => s.trim())
453
+ .filter(Boolean);
454
+ const evm = [];
455
+ const solana = [];
456
+ const invalid = [];
457
+ for (const addr of addrs) {
458
+ if (addr.startsWith("0x")) {
459
+ if (isAddress(addr)) {
460
+ evm.push(addr);
461
+ }
462
+ else {
463
+ invalid.push(addr);
464
+ }
465
+ }
466
+ else {
467
+ // Solana: base58, 32-44 chars
468
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(addr)) {
469
+ solana.push(addr);
470
+ }
471
+ else {
472
+ invalid.push(addr);
473
+ }
474
+ }
475
+ }
476
+ if (invalid.length > 0) {
477
+ fatal(`Invalid address(es): ${invalid.join(", ")}`);
478
+ }
479
+ return { evm, solana };
480
+ }
400
481
  export async function loginCommand(opts) {
401
482
  // Fail fast if flags were passed with empty values
402
483
  if (opts.apiKey !== undefined && !opts.apiKey.trim()) {
@@ -444,6 +525,17 @@ export async function loginCommand(opts) {
444
525
  output.success(`Credentials saved to ${getConfigPath()}`);
445
526
  output.dim(`Bankr API URL: ${apiUrl}`);
446
527
  }
528
+ // Lazily parse allowedIps / allowedRecipients only in flows that use them
529
+ function getParsedRestrictions() {
530
+ return {
531
+ allowedIps: opts.allowedIps
532
+ ? parseAndValidateIps(opts.allowedIps)
533
+ : undefined,
534
+ allowedRecipients: opts.allowedRecipients
535
+ ? parseAndValidateRecipients(opts.allowedRecipients)
536
+ : undefined,
537
+ };
538
+ }
447
539
  // --siwe: SIWE login flow (headless agent onboarding)
448
540
  if (opts.siwe) {
449
541
  if (!opts.privateKey) {
@@ -454,6 +546,10 @@ export async function loginCommand(opts) {
454
546
  partnerKey: opts.partnerKey,
455
547
  keyName: opts.keyName,
456
548
  readOnly: !opts.readWrite,
549
+ walletApi: opts.walletApi,
550
+ agentApi: opts.agentApi,
551
+ tokenLaunch: opts.tokenLaunch,
552
+ ...getParsedRestrictions(),
457
553
  });
458
554
  config.apiKey = apiKey;
459
555
  if (opts.partnerKey) {
@@ -492,10 +588,20 @@ export async function loginCommand(opts) {
492
588
  const privyConfig = await fetchPrivyConfigOrFail(apiUrl);
493
589
  await sendOtpWithSpinner(privyConfig, opts.email);
494
590
  output.blank();
495
- output.info(`Complete login with: bankr login email ${opts.email} --code <code>`);
591
+ // Build the hint command, including any flags the user passed so they
592
+ // remember to re-supply them in step 2
593
+ let hint = `bankr login email ${opts.email} --code <code>`;
594
+ if (opts.allowedIps)
595
+ hint += ` --allowed-ips ${opts.allowedIps}`;
596
+ if (opts.allowedRecipients)
597
+ hint += ` --allowed-recipients ${opts.allowedRecipients}`;
598
+ output.info(`Complete login with: ${hint}`);
496
599
  return;
497
600
  }
498
- const apiKey = await emailLoginFlow(apiUrl, opts);
601
+ const apiKey = await emailLoginFlow(apiUrl, {
602
+ ...opts,
603
+ ...getParsedRestrictions(),
604
+ });
499
605
  output.blank();
500
606
  saveCredentials(apiKey, opts.llmKey);
501
607
  return;
@@ -0,0 +1,9 @@
1
+ export declare function portfolioCommand(opts: {
2
+ chain?: string;
3
+ json?: boolean;
4
+ lowValue?: boolean;
5
+ pnl?: boolean;
6
+ nfts?: boolean;
7
+ all?: boolean;
8
+ }): Promise<void>;
9
+ //# sourceMappingURL=portfolio.d.ts.map
@@ -0,0 +1,134 @@
1
+ import chalk from "chalk";
2
+ import { getPortfolio, } from "../lib/api.js";
3
+ import * as output from "../lib/output.js";
4
+ import { formatUsd, formatBalance } from "../lib/output.js";
5
+ import { CHAIN_LABELS, VALID_CHAINS } from "../lib/chains.js";
6
+ const BRAND = chalk.hex("#FF613D");
7
+ const DIM = chalk.dim;
8
+ const BOLD = chalk.bold;
9
+ const GREEN = chalk.greenBright;
10
+ const RED = chalk.redBright;
11
+ const WHITE = chalk.whiteBright;
12
+ function formatPnl(value) {
13
+ if (value === 0)
14
+ return DIM("$0.00");
15
+ const formatted = `$${Math.abs(value).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
16
+ return value > 0 ? GREEN(`+${formatted}`) : RED(`-${formatted}`);
17
+ }
18
+ function printChainBalances(chain, data, showPnl) {
19
+ const label = CHAIN_LABELS[chain] || chain;
20
+ const total = parseFloat(data.total);
21
+ const totalStr = total > 0 ? GREEN(formatUsd(total)) : DIM(formatUsd(total));
22
+ console.log();
23
+ console.log();
24
+ console.log(` ${BRAND.bold(label)} ${totalStr}`);
25
+ console.log(` ${DIM("─".repeat(showPnl ? 74 : 58))}`);
26
+ // Native balance
27
+ const nativeUsd = parseFloat(data.nativeUsd);
28
+ if (nativeUsd > 0 || parseFloat(data.nativeBalance) > 0) {
29
+ const nativeSymbol = chain === "solana" ? "SOL" : chain === "polygon" ? "POL" : "ETH";
30
+ console.log(` ${BOLD(WHITE(nativeSymbol.padEnd(12)))} ${formatBalance(data.nativeBalance).padStart(18)} ${DIM(formatUsd(nativeUsd))}`);
31
+ }
32
+ // Token balances sorted by USD value descending
33
+ const sorted = [...data.tokenBalances].sort((a, b) => b.token.balanceUSD - a.token.balanceUSD);
34
+ for (const tb of sorted) {
35
+ const symbol = tb.token.baseToken.symbol || "???";
36
+ const bal = formatBalance(tb.token.balance);
37
+ const usd = formatUsd(tb.token.balanceUSD);
38
+ let line = ` ${WHITE(symbol.padEnd(12))} ${bal.padStart(18)} ${DIM(usd)}`;
39
+ if (showPnl && tb.token.pnl) {
40
+ line += ` ${formatPnl(tb.token.pnl.totalPnl)}`;
41
+ }
42
+ console.log(line);
43
+ }
44
+ if (sorted.length === 0 && (nativeUsd === 0 || isNaN(nativeUsd))) {
45
+ console.log(DIM(" No token balances"));
46
+ }
47
+ }
48
+ export async function portfolioCommand(opts) {
49
+ // Validate chain option
50
+ const chains = opts.chain
51
+ ? opts.chain.split(",").map((c) => c.trim().toLowerCase())
52
+ : undefined;
53
+ if (chains) {
54
+ const invalid = chains.filter((c) => !VALID_CHAINS.has(c));
55
+ if (invalid.length > 0) {
56
+ output.error(`Invalid chain${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}. Valid chains: ${[...VALID_CHAINS].join(", ")}`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+ const includePnl = opts.pnl || opts.all;
61
+ const includeNfts = opts.nfts || opts.all;
62
+ const include = [];
63
+ if (includePnl)
64
+ include.push("pnl");
65
+ if (includeNfts)
66
+ include.push("nfts");
67
+ const spinText = include.length > 0
68
+ ? `Fetching portfolio (${include.join(", ")})...`
69
+ : "Fetching portfolio...";
70
+ const spin = output.spinner(spinText);
71
+ let data;
72
+ try {
73
+ data = await getPortfolio({
74
+ chains,
75
+ showLowValueTokens: opts.lowValue,
76
+ include: include.length > 0 ? include : undefined,
77
+ });
78
+ spin.succeed("Portfolio loaded");
79
+ }
80
+ catch (err) {
81
+ spin.fail(`Failed to fetch portfolio: ${err.message}`);
82
+ process.exit(1);
83
+ }
84
+ // JSON output mode
85
+ if (opts.json) {
86
+ console.log(JSON.stringify(data, null, 2));
87
+ return;
88
+ }
89
+ // Calculate grand total
90
+ let grandTotal = 0;
91
+ const chainEntries = Object.entries(data.balances);
92
+ for (const [, chainData] of chainEntries) {
93
+ grandTotal += parseFloat(chainData.total) || 0;
94
+ }
95
+ console.log();
96
+ output.label("Wallet", "");
97
+ console.log(` ${"EVM".padEnd(8)} ${data.evmAddress}`);
98
+ if (data.solAddress) {
99
+ console.log(` ${"SOLANA".padEnd(8)} ${data.solAddress}`);
100
+ }
101
+ console.log();
102
+ output.label("Total", GREEN(formatUsd(grandTotal)));
103
+ if (opts.lowValue) {
104
+ console.log();
105
+ output.info("Including low-value tokens");
106
+ }
107
+ // Print each chain
108
+ for (const [chain, chainData] of chainEntries) {
109
+ printChainBalances(chain, chainData, !!includePnl);
110
+ }
111
+ // Print NFTs
112
+ if (includeNfts && data.nfts && data.nfts.length > 0) {
113
+ console.log();
114
+ console.log();
115
+ console.log(` ${BRAND.bold("NFTs")} ${DIM(`${data.nfts.length} item(s)`)}`);
116
+ console.log(` ${DIM("─".repeat(58))}`);
117
+ for (const nft of data.nfts) {
118
+ const name = nft.name || DIM("Unnamed");
119
+ const collection = nft.collection?.name
120
+ ? DIM(` (${nft.collection.name})`)
121
+ : "";
122
+ const address = nft.collection?.address || DIM("unknown");
123
+ console.log(` ${WHITE(name)}${collection}`);
124
+ console.log(` ${DIM(address)} #${nft.tokenId} ${DIM(nft.chainName || nft.chain)}`);
125
+ console.log();
126
+ }
127
+ }
128
+ else if (includeNfts) {
129
+ console.log();
130
+ output.info("No NFTs found");
131
+ }
132
+ console.log();
133
+ }
134
+ //# sourceMappingURL=portfolio.js.map
@@ -0,0 +1,7 @@
1
+ export declare function tokensSearchCommand(query: string, opts: {
2
+ chain?: string;
3
+ }): Promise<void>;
4
+ export declare function tokensInfoCommand(address: string, opts: {
5
+ chain?: string;
6
+ }): Promise<void>;
7
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { searchTokens } from "../lib/api.js";
2
+ import * as output from "../lib/output.js";
3
+ export async function tokensSearchCommand(query, opts) {
4
+ const spin = output.spinner(`Searching tokens for "${query}"...`);
5
+ try {
6
+ const chainId = opts.chain ? Number(opts.chain) : undefined;
7
+ const res = await searchTokens(query, chainId);
8
+ spin.succeed(`Found ${res.tokens.length} result(s)`);
9
+ if (res.tokens.length === 0) {
10
+ output.info("No tokens found. Try a different search query.");
11
+ return;
12
+ }
13
+ console.log();
14
+ for (const token of res.tokens) {
15
+ const price = token.priceUsd
16
+ ? `$${token.priceUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}`
17
+ : output.fmt.dim("n/a");
18
+ console.log(` ${output.fmt.brandBold(`${token.symbol}`)} ${output.fmt.dim(token.name)}`);
19
+ console.log(` Address: ${token.address}`);
20
+ console.log(` Price: ${price}`);
21
+ console.log();
22
+ }
23
+ }
24
+ catch (err) {
25
+ spin.fail(`Search failed: ${err.message}`);
26
+ process.exit(1);
27
+ }
28
+ }
29
+ export async function tokensInfoCommand(address, opts) {
30
+ const spin = output.spinner(`Fetching token info for ${address}...`);
31
+ try {
32
+ const chainId = opts.chain ? Number(opts.chain) : undefined;
33
+ const res = await searchTokens(address, chainId);
34
+ spin.succeed("Token info loaded");
35
+ // Match by the requested address (search returns all tokens from matching pools)
36
+ const addressLower = address.toLowerCase();
37
+ const token = res.tokens.find((t) => t.address.toLowerCase() === addressLower) ??
38
+ res.tokens[0];
39
+ if (!token) {
40
+ output.error("Token not found. Check the address and try again.");
41
+ process.exit(1);
42
+ }
43
+ console.log();
44
+ output.label("Name", token.name);
45
+ output.label("Symbol", token.symbol);
46
+ output.label("Address", token.address);
47
+ output.label("Decimals", String(token.decimals));
48
+ if (token.priceUsd) {
49
+ output.label("Price", `$${token.priceUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}`);
50
+ }
51
+ if (token.logoURI) {
52
+ output.label("Logo", token.logoURI);
53
+ }
54
+ }
55
+ catch (err) {
56
+ spin.fail(`Failed to fetch token info: ${err.message}`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1,8 @@
1
+ export declare function transferCommand(opts: {
2
+ to: string;
3
+ amount: string;
4
+ token?: string;
5
+ native?: boolean;
6
+ chain?: string;
7
+ }): Promise<void>;
8
+ //# sourceMappingURL=transfer.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { transfer, searchTokens } from "../lib/api.js";
2
+ import * as output from "../lib/output.js";
3
+ import { NATIVE_SYMBOLS, CHAIN_IDS } from "../lib/chains.js";
4
+ import { isAddress } from "viem";
5
+ /**
6
+ * Resolve a token symbol or address to a contract address.
7
+ * If it's already a valid address, return as-is. Otherwise search by symbol.
8
+ */
9
+ async function resolveToken(tokenInput, chain) {
10
+ if (isAddress(tokenInput)) {
11
+ return { address: tokenInput, symbol: tokenInput };
12
+ }
13
+ const chainId = CHAIN_IDS[chain];
14
+ const res = await searchTokens(tokenInput, chainId);
15
+ // Exact symbol match (case-insensitive) to avoid resolving to wrong token
16
+ const inputUpper = tokenInput.toUpperCase();
17
+ const exactMatch = res.tokens.find((t) => t.symbol.toUpperCase() === inputUpper);
18
+ if (!exactMatch) {
19
+ const available = res.tokens
20
+ .slice(0, 5)
21
+ .map((t) => `${t.symbol} (${t.address})`)
22
+ .join(", ");
23
+ throw new Error(`No exact match for "${tokenInput}" on ${chain}. Did you mean: ${available || "no results"}`);
24
+ }
25
+ return { address: exactMatch.address, symbol: exactMatch.symbol };
26
+ }
27
+ export async function transferCommand(opts) {
28
+ const isNative = opts.native ?? false;
29
+ const chain = opts.chain ?? "base";
30
+ if (!isNative && !opts.token) {
31
+ output.error("Token required for ERC20 transfers. Use --token <symbol or address> or --native.");
32
+ process.exit(1);
33
+ }
34
+ let tokenAddress;
35
+ let tokenLabel;
36
+ if (isNative) {
37
+ tokenAddress = "0x0000000000000000000000000000000000000000";
38
+ tokenLabel = NATIVE_SYMBOLS[chain] ?? "ETH";
39
+ }
40
+ else {
41
+ const spin = output.spinner(`Resolving token "${opts.token}"...`);
42
+ try {
43
+ const resolved = await resolveToken(opts.token, chain);
44
+ tokenAddress = resolved.address;
45
+ tokenLabel = resolved.symbol;
46
+ if (tokenAddress !== opts.token) {
47
+ spin.succeed(`Resolved ${opts.token} → ${tokenLabel} (${tokenAddress})`);
48
+ }
49
+ else {
50
+ spin.stop();
51
+ }
52
+ }
53
+ catch (err) {
54
+ spin.fail(err.message);
55
+ process.exit(1);
56
+ }
57
+ }
58
+ const chainLabel = chain !== "base" ? ` on ${chain}` : "";
59
+ const spin = output.spinner(`Transferring ${opts.amount} ${tokenLabel} to ${opts.to}${chainLabel}...`);
60
+ try {
61
+ const result = await transfer({
62
+ tokenAddress,
63
+ recipientAddress: opts.to,
64
+ amount: opts.amount,
65
+ isNativeToken: isNative,
66
+ chain,
67
+ });
68
+ if (!result.success) {
69
+ spin.fail(`Transfer failed: ${result.error}`);
70
+ process.exit(1);
71
+ }
72
+ spin.succeed(`Transfer successful`);
73
+ output.label("Tx Hash", result.txHash ?? "unknown");
74
+ }
75
+ catch (err) {
76
+ spin.fail(`Transfer failed: ${err.message}`);
77
+ process.exit(1);
78
+ }
79
+ }
80
+ //# sourceMappingURL=transfer.js.map
@@ -0,0 +1,4 @@
1
+ export declare function updateCommand(opts: {
2
+ check?: boolean;
3
+ }): Promise<void>;
4
+ //# sourceMappingURL=update.d.ts.map