@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.
- package/dist/cli.js +379 -133
- package/dist/commands/balances.d.ts +1 -0
- package/dist/commands/balances.js +7 -32
- package/dist/commands/llm.js +47 -3
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +139 -33
- package/dist/commands/portfolio.d.ts +9 -0
- package/dist/commands/portfolio.js +134 -0
- package/dist/commands/tokens.d.ts +7 -0
- package/dist/commands/tokens.js +60 -0
- package/dist/commands/transfer.d.ts +8 -0
- package/dist/commands/transfer.js +80 -0
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +116 -0
- package/dist/lib/api.d.ts +59 -2
- package/dist/lib/api.js +34 -8
- package/dist/lib/chains.d.ts +5 -0
- package/dist/lib/chains.js +35 -0
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +17 -0
- package/package.json +1 -1
package/dist/commands/llm.js
CHANGED
|
@@ -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
|
-
|
|
310
|
-
console.log(output.fmt.
|
|
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
|
-
|
|
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()}`);
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
-
(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
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(`
|
|
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
|
-
|
|
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,
|
|
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,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,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,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
|