@bankr/cli 0.3.1 → 0.3.11
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/README.md +7 -7
- package/dist/cli.js +146 -83
- package/dist/commands/club.d.ts +1 -0
- package/dist/commands/club.js +47 -5
- package/dist/commands/fees.js +10 -2
- package/dist/commands/launch.js +16 -23
- package/dist/commands/llm.d.ts +3 -1
- package/dist/commands/llm.js +240 -63
- package/dist/commands/login.d.ts +0 -1
- package/dist/commands/login.js +28 -23
- package/dist/commands/profile.d.ts +5 -1
- package/dist/commands/profile.js +54 -10
- package/dist/commands/prompt.js +6 -0
- package/dist/commands/swap.d.ts +8 -0
- package/dist/commands/swap.js +107 -0
- package/dist/commands/transfer.js +38 -25
- package/dist/commands/webhooks.js +2 -7
- package/dist/commands/x402.js +57 -41
- package/dist/lib/api.d.ts +50 -3
- package/dist/lib/api.js +41 -16
- package/dist/lib/config.d.ts +0 -1
- package/dist/lib/output.d.ts +1 -0
- package/dist/lib/output.js +3 -0
- package/dist/lib/tokens.d.ts +21 -0
- package/dist/lib/tokens.js +40 -0
- package/package.json +6 -6
package/dist/commands/login.js
CHANGED
|
@@ -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-", "") +
|
|
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
|
|
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}/
|
|
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 (
|
|
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;
|
|
@@ -10,6 +10,7 @@ export declare function profileCreateCommand(opts: {
|
|
|
10
10
|
json?: boolean;
|
|
11
11
|
}): Promise<void>;
|
|
12
12
|
export declare function profileUpdateCommand(opts: {
|
|
13
|
+
slug?: string;
|
|
13
14
|
name?: string;
|
|
14
15
|
description?: string;
|
|
15
16
|
token?: string;
|
|
@@ -17,8 +18,11 @@ export declare function profileUpdateCommand(opts: {
|
|
|
17
18
|
website?: string;
|
|
18
19
|
json?: boolean;
|
|
19
20
|
}): Promise<void>;
|
|
20
|
-
export declare function profileDeleteCommand(
|
|
21
|
+
export declare function profileDeleteCommand(opts: {
|
|
22
|
+
slug?: string;
|
|
23
|
+
}): Promise<void>;
|
|
21
24
|
export declare function profileAddUpdateCommand(opts: {
|
|
25
|
+
slug?: string;
|
|
22
26
|
title?: string;
|
|
23
27
|
content?: string;
|
|
24
28
|
json?: boolean;
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { input, confirm } from "@inquirer/prompts";
|
|
3
|
-
import {
|
|
2
|
+
import { input, confirm, select } from "@inquirer/prompts";
|
|
3
|
+
import { getOwnProfiles, createProfile, updateProfile, deleteProfile, addProjectUpdate, } from "../lib/api.js";
|
|
4
4
|
import * as output from "../lib/output.js";
|
|
5
5
|
const BRAND = chalk.hex("#FF613D");
|
|
6
6
|
const DIM = chalk.dim;
|
|
@@ -42,15 +42,41 @@ function printProfile(profile, json) {
|
|
|
42
42
|
}
|
|
43
43
|
console.log();
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve which profile to operate on.
|
|
47
|
+
* If --slug is provided, use it. Otherwise, if there's exactly one profile, use that.
|
|
48
|
+
* If multiple profiles exist, prompt the user to pick.
|
|
49
|
+
*/
|
|
50
|
+
async function resolveSlug(profiles, slugOpt) {
|
|
51
|
+
if (slugOpt)
|
|
52
|
+
return slugOpt;
|
|
53
|
+
if (profiles.length === 1)
|
|
54
|
+
return profiles[0].slug;
|
|
55
|
+
const choice = await select({
|
|
56
|
+
message: "Which project?",
|
|
57
|
+
choices: profiles.map((p) => ({
|
|
58
|
+
name: `${p.projectName} (${p.slug})`,
|
|
59
|
+
value: p.slug,
|
|
60
|
+
})),
|
|
61
|
+
theme: output.bankrTheme,
|
|
62
|
+
});
|
|
63
|
+
return choice;
|
|
64
|
+
}
|
|
45
65
|
export async function profileViewCommand(opts) {
|
|
46
66
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
67
|
+
const profiles = await getOwnProfiles();
|
|
68
|
+
if (!profiles.length) {
|
|
69
|
+
output.info("No profiles found. Create one with: bankr profile create");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const profile of profiles) {
|
|
73
|
+
printProfile(profile, !!opts.json);
|
|
74
|
+
}
|
|
49
75
|
}
|
|
50
76
|
catch (err) {
|
|
51
77
|
const msg = err.message;
|
|
52
78
|
if (msg.includes("404")) {
|
|
53
|
-
output.info("No
|
|
79
|
+
output.info("No profiles found. Create one with: bankr profile create");
|
|
54
80
|
}
|
|
55
81
|
else {
|
|
56
82
|
output.error(msg);
|
|
@@ -126,7 +152,13 @@ export async function profileUpdateCommand(opts) {
|
|
|
126
152
|
output.error("No fields to update. Use flags: --name, --description, --token, --image, --website");
|
|
127
153
|
return;
|
|
128
154
|
}
|
|
129
|
-
const
|
|
155
|
+
const profiles = await getOwnProfiles();
|
|
156
|
+
if (!profiles.length) {
|
|
157
|
+
output.error("No profiles found. Create one first with: bankr profile create");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const slug = await resolveSlug(profiles, opts.slug);
|
|
161
|
+
const profile = await updateProfile(slug, data);
|
|
130
162
|
output.success("Profile updated!");
|
|
131
163
|
printProfile(profile, !!opts.json);
|
|
132
164
|
}
|
|
@@ -134,10 +166,16 @@ export async function profileUpdateCommand(opts) {
|
|
|
134
166
|
output.error(err.message);
|
|
135
167
|
}
|
|
136
168
|
}
|
|
137
|
-
export async function profileDeleteCommand() {
|
|
169
|
+
export async function profileDeleteCommand(opts) {
|
|
138
170
|
try {
|
|
171
|
+
const profiles = await getOwnProfiles();
|
|
172
|
+
if (!profiles.length) {
|
|
173
|
+
output.error("No profiles found.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const slug = await resolveSlug(profiles, opts.slug);
|
|
139
177
|
const yes = await confirm({
|
|
140
|
-
message:
|
|
178
|
+
message: `Are you sure you want to delete "${slug}"?`,
|
|
141
179
|
default: false,
|
|
142
180
|
theme: output.bankrTheme,
|
|
143
181
|
});
|
|
@@ -145,7 +183,7 @@ export async function profileDeleteCommand() {
|
|
|
145
183
|
output.info("Cancelled.");
|
|
146
184
|
return;
|
|
147
185
|
}
|
|
148
|
-
await deleteProfile();
|
|
186
|
+
await deleteProfile(slug);
|
|
149
187
|
output.success("Profile deleted.");
|
|
150
188
|
}
|
|
151
189
|
catch (err) {
|
|
@@ -170,7 +208,13 @@ export async function profileAddUpdateCommand(opts) {
|
|
|
170
208
|
required: true,
|
|
171
209
|
});
|
|
172
210
|
}
|
|
173
|
-
const
|
|
211
|
+
const profiles = await getOwnProfiles();
|
|
212
|
+
if (!profiles.length) {
|
|
213
|
+
output.error("No profiles found. Create one first with: bankr profile create");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const slug = await resolveSlug(profiles, opts.slug);
|
|
217
|
+
const profile = await addProjectUpdate(slug, { title, content });
|
|
174
218
|
output.success("Project update added!");
|
|
175
219
|
if (opts.json) {
|
|
176
220
|
console.log(JSON.stringify(profile, null, 2));
|
package/dist/commands/prompt.js
CHANGED
|
@@ -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,14 +29,17 @@ 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",
|
|
42
|
+
"glm-5.2",
|
|
37
43
|
]);
|
|
38
44
|
export async function promptCommand(text, options = {}) {
|
|
39
45
|
requireApiKey();
|
|
@@ -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
|
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
import { transfer,
|
|
1
|
+
import { transfer, resolveRecipient } from "../lib/api.js";
|
|
2
2
|
import * as output from "../lib/output.js";
|
|
3
|
-
import { NATIVE_SYMBOLS
|
|
3
|
+
import { NATIVE_SYMBOLS } from "../lib/chains.js";
|
|
4
4
|
import { isAddress } from "viem";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
import { ZERO_ADDRESS, resolveTokenSymbol } from "../lib/tokens.js";
|
|
6
|
+
function looksLikeEnsName(value) {
|
|
7
|
+
const v = value.trim().toLowerCase();
|
|
8
|
+
return v.endsWith(".eth") || v.endsWith(".cb.id");
|
|
9
|
+
}
|
|
10
|
+
async function resolveTransferRecipient(raw) {
|
|
11
|
+
const value = raw.trim();
|
|
12
|
+
if (isAddress(value)) {
|
|
13
|
+
return { address: value, label: value };
|
|
14
|
+
}
|
|
15
|
+
if (!looksLikeEnsName(value)) {
|
|
16
|
+
output.error(`Invalid recipient "${value}". Expected a 0x address or an ENS name (e.g. vitalik.eth, name.base.eth, name.cb.id).`);
|
|
17
|
+
process.exit(1);
|
|
12
18
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
|
|
19
|
+
const spin = output.spinner(`Resolving ENS name "${value}"...`);
|
|
20
|
+
try {
|
|
21
|
+
const resolved = await resolveRecipient(value, "ens");
|
|
22
|
+
if (!resolved.resolved || !resolved.address) {
|
|
23
|
+
spin.fail(`Could not resolve "${value}": ${resolved.error ?? "unknown error"}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
spin.succeed(`Resolved ${value} → ${resolved.address}`);
|
|
27
|
+
return {
|
|
28
|
+
address: resolved.address,
|
|
29
|
+
label: `${value} (${resolved.address})`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
spin.fail(`ENS resolution failed for "${value}": ${err.message}`);
|
|
34
|
+
process.exit(1);
|
|
24
35
|
}
|
|
25
|
-
return { address: exactMatch.address, symbol: exactMatch.symbol };
|
|
26
36
|
}
|
|
27
37
|
export async function transferCommand(opts) {
|
|
28
38
|
const isNative = opts.native ?? false;
|
|
@@ -31,16 +41,19 @@ export async function transferCommand(opts) {
|
|
|
31
41
|
output.error("Token required for ERC20 transfers. Use --token <symbol or address> or --native.");
|
|
32
42
|
process.exit(1);
|
|
33
43
|
}
|
|
44
|
+
const recipient = await resolveTransferRecipient(opts.to);
|
|
45
|
+
const recipientAddress = recipient.address;
|
|
46
|
+
const recipientLabel = recipient.label;
|
|
34
47
|
let tokenAddress;
|
|
35
48
|
let tokenLabel;
|
|
36
49
|
if (isNative) {
|
|
37
|
-
tokenAddress =
|
|
50
|
+
tokenAddress = ZERO_ADDRESS;
|
|
38
51
|
tokenLabel = NATIVE_SYMBOLS[chain] ?? "ETH";
|
|
39
52
|
}
|
|
40
53
|
else {
|
|
41
54
|
const spin = output.spinner(`Resolving token "${opts.token}"...`);
|
|
42
55
|
try {
|
|
43
|
-
const resolved = await
|
|
56
|
+
const resolved = await resolveTokenSymbol(opts.token, chain);
|
|
44
57
|
tokenAddress = resolved.address;
|
|
45
58
|
tokenLabel = resolved.symbol;
|
|
46
59
|
if (tokenAddress !== opts.token) {
|
|
@@ -56,11 +69,11 @@ export async function transferCommand(opts) {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
const chainLabel = chain !== "base" ? ` on ${chain}` : "";
|
|
59
|
-
const spin = output.spinner(`Transferring ${opts.amount} ${tokenLabel} to ${
|
|
72
|
+
const spin = output.spinner(`Transferring ${opts.amount} ${tokenLabel} to ${recipientLabel}${chainLabel}...`);
|
|
60
73
|
try {
|
|
61
74
|
const result = await transfer({
|
|
62
75
|
tokenAddress,
|
|
63
|
-
recipientAddress
|
|
76
|
+
recipientAddress,
|
|
64
77
|
amount: opts.amount,
|
|
65
78
|
isNativeToken: isNative,
|
|
66
79
|
chain,
|
|
@@ -17,7 +17,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { confirm } from "@inquirer/prompts";
|
|
19
19
|
import * as output from "../lib/output.js";
|
|
20
|
-
import { getApiUrl, requireApiKey,
|
|
20
|
+
import { getApiUrl, requireApiKey, CLI_USER_AGENT } from "../lib/config.js";
|
|
21
21
|
// ── Config file ─────────────────────────────────────────────────────────
|
|
22
22
|
const CONFIG_FILENAME = "bankr.webhooks.json";
|
|
23
23
|
const WEBHOOKS_DIR = "webhooks";
|
|
@@ -46,16 +46,11 @@ function saveConfigFile(projectRoot, config) {
|
|
|
46
46
|
writeFileSync(join(projectRoot, CONFIG_FILENAME), JSON.stringify(config, null, 2) + "\n");
|
|
47
47
|
}
|
|
48
48
|
function authHeaders() {
|
|
49
|
-
|
|
49
|
+
return {
|
|
50
50
|
"X-API-Key": requireApiKey(),
|
|
51
51
|
"Content-Type": "application/json",
|
|
52
52
|
"User-Agent": CLI_USER_AGENT,
|
|
53
53
|
};
|
|
54
|
-
const config = readConfig();
|
|
55
|
-
if (config.partnerKey) {
|
|
56
|
-
headers["X-Partner-Key"] = config.partnerKey;
|
|
57
|
-
}
|
|
58
|
-
return headers;
|
|
59
54
|
}
|
|
60
55
|
async function handleResponse(res) {
|
|
61
56
|
if (!res.ok) {
|