@alchemy/cli 0.4.0 → 0.5.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.
@@ -1,1327 +0,0 @@
1
- #!/usr/bin/env node
2
- if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
3
- import {
4
- CLIError,
5
- ErrorCode,
6
- KEY_MAP,
7
- configDir,
8
- debug,
9
- dim,
10
- errAccessDenied,
11
- errAccessKeyRequired,
12
- errAdminAPI,
13
- errAuthRequired,
14
- errInvalidAPIKey,
15
- errInvalidAccessKey,
16
- errInvalidArgs,
17
- errNetwork,
18
- errNetworkNotEnabled,
19
- errNotFound,
20
- errRPC,
21
- errRateLimited,
22
- errWalletKeyRequired,
23
- exitWithError,
24
- get,
25
- green,
26
- isInteractiveAllowed,
27
- isJSONMode,
28
- load,
29
- maskIf,
30
- printHuman,
31
- printJSON,
32
- printKeyValueBox,
33
- promptAutocomplete,
34
- promptConfirm,
35
- promptMultiselect,
36
- promptSelect,
37
- promptText,
38
- redactSensitiveText,
39
- save,
40
- timeout,
41
- toMap,
42
- verbose,
43
- withSpinner,
44
- yellow
45
- } from "./chunk-TH75DFAY.js";
46
-
47
- // src/lib/client-utils.ts
48
- function isLocalhost(hostname) {
49
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
50
- }
51
- function parseBaseURLOverride(envVarName) {
52
- const raw = process.env[envVarName];
53
- if (!raw) return null;
54
- let parsed;
55
- try {
56
- parsed = new URL(raw);
57
- } catch {
58
- throw errInvalidArgs(`Invalid ${envVarName} value.`);
59
- }
60
- if (!isLocalhost(parsed.hostname)) {
61
- throw errInvalidArgs(
62
- `${envVarName} must target localhost or 127.0.0.1.`
63
- );
64
- }
65
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
66
- throw errInvalidArgs(
67
- `${envVarName} must use http:// or https://.`
68
- );
69
- }
70
- if (parsed.protocol === "http:" && !isLocalhost(parsed.hostname)) {
71
- throw errInvalidArgs(
72
- `${envVarName} can only use non-HTTPS for localhost targets.`
73
- );
74
- }
75
- return parsed;
76
- }
77
- var BREADCRUMB_HEADER = "alchemy-cli";
78
- async function fetchWithTimeout(url, init) {
79
- try {
80
- return await fetch(url, {
81
- ...init,
82
- headers: {
83
- ...init.headers,
84
- "x-alchemy-client-breadcrumb": BREADCRUMB_HEADER
85
- },
86
- ...timeout && { signal: AbortSignal.timeout(timeout) }
87
- });
88
- } catch (err) {
89
- if (err instanceof DOMException && err.name === "TimeoutError") {
90
- throw errNetwork(`Request timed out after ${timeout}ms`);
91
- }
92
- const message = err.message ?? String(err);
93
- const causeMessage = err.cause?.message ?? "";
94
- const causeCode = err.cause?.code ?? "";
95
- const fullErrorText = `${message} ${causeMessage} ${causeCode}`;
96
- if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(fullErrorText)) {
97
- try {
98
- const hostname = new URL(url).hostname;
99
- const networkSlug = hostname.replace(/\.g\.alchemy\.com$/, "");
100
- if (networkSlug !== hostname) {
101
- throw errInvalidArgs(
102
- `Unknown network '${networkSlug}'. Run 'alchemy network list' to see available networks.`
103
- );
104
- }
105
- } catch (innerErr) {
106
- if (innerErr instanceof CLIError) throw innerErr;
107
- }
108
- }
109
- throw errNetwork(message);
110
- }
111
- }
112
-
113
- // src/lib/admin-client.ts
114
- var AdminClient = class _AdminClient {
115
- static ADMIN_API_HOST = "admin-api.alchemy.com";
116
- // Test/debug only: used by mock E2E to route admin requests locally.
117
- static ADMIN_API_BASE_URL_ENV = "ALCHEMY_ADMIN_API_BASE_URL";
118
- accessKey;
119
- constructor(accessKey) {
120
- this.validateAccessKey(accessKey);
121
- this.accessKey = accessKey;
122
- }
123
- baseURL() {
124
- const override = this.baseURLOverride();
125
- if (override) return override.toString().replace(/\/$/, "");
126
- return "https://admin-api.alchemy.com";
127
- }
128
- allowedHosts() {
129
- const hosts = /* @__PURE__ */ new Set([_AdminClient.ADMIN_API_HOST]);
130
- const override = this.baseURLOverride();
131
- if (override) hosts.add(override.hostname);
132
- return hosts;
133
- }
134
- allowInsecureTransport(hostname) {
135
- return isLocalhost(hostname);
136
- }
137
- baseURLOverride() {
138
- return parseBaseURLOverride(_AdminClient.ADMIN_API_BASE_URL_ENV);
139
- }
140
- validateAccessKey(accessKey) {
141
- if (!accessKey.trim() || /\s/.test(accessKey)) {
142
- throw errInvalidAccessKey();
143
- }
144
- }
145
- assertSafeRequestTarget(url) {
146
- let parsed;
147
- try {
148
- parsed = new URL(url);
149
- } catch {
150
- throw errInvalidArgs("Invalid admin API URL.");
151
- }
152
- if (!this.allowedHosts().has(parsed.hostname)) {
153
- throw errInvalidArgs(`Refusing to send credentials to unexpected host: ${parsed.hostname}`);
154
- }
155
- if (parsed.protocol !== "https:" && !this.allowInsecureTransport(parsed.hostname)) {
156
- throw errInvalidArgs("Refusing to send credentials over non-HTTPS connection.");
157
- }
158
- }
159
- async request(method, path, body) {
160
- const url = `${this.baseURL()}${path}`;
161
- this.assertSafeRequestTarget(url);
162
- const resp = await fetchWithTimeout(url, {
163
- method,
164
- redirect: "error",
165
- headers: {
166
- Authorization: `Bearer ${this.accessKey}`,
167
- "Content-Type": "application/json",
168
- Accept: "application/json"
169
- },
170
- ...body !== void 0 && { body: JSON.stringify(body) }
171
- });
172
- if (resp.status === 401) throw errInvalidAccessKey();
173
- if (resp.status === 403) {
174
- const detail = await resp.text().catch(() => "");
175
- let reason;
176
- try {
177
- const parsed = JSON.parse(detail);
178
- reason = parsed?.message || parsed?.error?.message || parsed?.error || void 0;
179
- } catch {
180
- reason = detail || void 0;
181
- }
182
- throw errAccessDenied(typeof reason === "string" ? reason : void 0);
183
- }
184
- if (resp.status === 404) {
185
- const text = await resp.text().catch(() => "");
186
- throw errNotFound(text || path);
187
- }
188
- if (resp.status === 429) throw errRateLimited();
189
- if (!resp.ok) {
190
- const text = await resp.text().catch(() => "");
191
- throw errAdminAPI(resp.status, text);
192
- }
193
- return resp.json();
194
- }
195
- async listChains() {
196
- const result = await this.request("GET", "/v1/chains");
197
- const chains = (Array.isArray(result.data) ? result.data : void 0) ?? (!Array.isArray(result.data) ? result.data?.networks : void 0) ?? (!Array.isArray(result.data) ? result.data?.chains : void 0) ?? result.networks ?? result.chains;
198
- if (!Array.isArray(chains)) {
199
- throw errAdminAPI(200, "Unexpected response shape for /v1/chains.");
200
- }
201
- return chains;
202
- }
203
- async listApps(opts) {
204
- const params = new URLSearchParams();
205
- if (opts?.cursor) params.set("cursor", opts.cursor);
206
- if (opts?.limit) params.set("limit", String(opts.limit));
207
- const qs = params.toString();
208
- const resp = await this.request(
209
- "GET",
210
- `/v1/apps${qs ? `?${qs}` : ""}`
211
- );
212
- return resp.data;
213
- }
214
- async listAllApps(opts) {
215
- const apps = [];
216
- const seenCursors = /* @__PURE__ */ new Set();
217
- let cursor;
218
- let pages = 0;
219
- do {
220
- const page = await this.listApps({
221
- ...cursor && { cursor },
222
- ...opts?.limit !== void 0 && { limit: opts.limit }
223
- });
224
- pages += 1;
225
- apps.push(...page.apps);
226
- cursor = page.cursor;
227
- if (cursor && seenCursors.has(cursor)) break;
228
- if (cursor) seenCursors.add(cursor);
229
- } while (cursor);
230
- return { apps, pages };
231
- }
232
- async getApp(id) {
233
- const resp = await this.request("GET", `/v1/apps/${id}`);
234
- return resp.data;
235
- }
236
- async createApp(opts) {
237
- const resp = await this.request("POST", "/v1/apps", {
238
- name: opts.name,
239
- chainNetworks: opts.networks,
240
- ...opts.description && { description: opts.description },
241
- ...opts.products && { products: opts.products }
242
- });
243
- return resp.data;
244
- }
245
- async deleteApp(id) {
246
- await this.request("DELETE", `/v1/apps/${id}`);
247
- }
248
- async updateApp(id, opts) {
249
- const resp = await this.request("PATCH", `/v1/apps/${id}`, opts);
250
- return resp.data;
251
- }
252
- async updateNetworkAllowlist(id, networks) {
253
- const resp = await this.request("PUT", `/v1/apps/${id}/networks`, {
254
- chainNetworks: networks
255
- });
256
- return resp.data;
257
- }
258
- async updateAddressAllowlist(id, addresses) {
259
- const resp = await this.request("PUT", `/v1/apps/${id}/address-allowlist`, {
260
- addressAllowlist: addresses
261
- });
262
- return resp.data;
263
- }
264
- async updateOriginAllowlist(id, origins) {
265
- const resp = await this.request("PUT", `/v1/apps/${id}/origin-allowlist`, {
266
- originAllowlist: origins
267
- });
268
- return resp.data;
269
- }
270
- async updateIpAllowlist(id, ips) {
271
- const resp = await this.request("PUT", `/v1/apps/${id}/ip-allowlist`, {
272
- ipAllowlist: ips
273
- });
274
- return resp.data;
275
- }
276
- };
277
-
278
- // src/lib/ens.ts
279
- import { keccak_256 } from "@noble/hashes/sha3.js";
280
- var UNIVERSAL_RESOLVER = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe";
281
- var RESOLVE_SELECTOR = "9061b923";
282
- var ADDR_SELECTOR = "3b3b57de";
283
- function bytesToHex(bytes) {
284
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
285
- }
286
- function pad32(hex) {
287
- return hex.padStart(64, "0");
288
- }
289
- function namehash(name) {
290
- let node = new Uint8Array(32);
291
- if (!name) return node;
292
- const labels = name.split(".");
293
- for (let i = labels.length - 1; i >= 0; i--) {
294
- const labelHash = keccak_256(new TextEncoder().encode(labels[i]));
295
- const combined = new Uint8Array(64);
296
- combined.set(node, 0);
297
- combined.set(labelHash, 32);
298
- node = keccak_256(combined);
299
- }
300
- return node;
301
- }
302
- function dnsEncode(name) {
303
- const labels = name.split(".");
304
- const parts = [];
305
- for (const label of labels) {
306
- const encoded = new TextEncoder().encode(label);
307
- parts.push(encoded.length);
308
- parts.push(...encoded);
309
- }
310
- parts.push(0);
311
- return new Uint8Array(parts);
312
- }
313
- function buildResolveCalldata(name) {
314
- const dnsName = dnsEncode(name);
315
- const node = namehash(name);
316
- const innerHex = ADDR_SELECTOR + bytesToHex(node);
317
- const innerLen = 36;
318
- const dnsHex = bytesToHex(dnsName);
319
- const nameLen = dnsName.length;
320
- const namePad = Math.ceil(nameLen / 32) * 32;
321
- const innerPad = Math.ceil(innerLen / 32) * 32;
322
- const nameOffset = 64;
323
- const dataOffset = nameOffset + 32 + namePad;
324
- let hex = RESOLVE_SELECTOR;
325
- hex += pad32(nameOffset.toString(16));
326
- hex += pad32(dataOffset.toString(16));
327
- hex += pad32(nameLen.toString(16));
328
- hex += dnsHex.padEnd(namePad * 2, "0");
329
- hex += pad32(innerLen.toString(16));
330
- hex += innerHex.padEnd(innerPad * 2, "0");
331
- return "0x" + hex;
332
- }
333
- function isENSName(value) {
334
- return value.endsWith(".eth") && value.length > 4 && !value.startsWith("0x");
335
- }
336
- async function resolveENS(name, client) {
337
- if (!client.network.startsWith("eth-")) {
338
- throw errInvalidArgs(
339
- `ENS resolution is only supported on Ethereum networks. Current network: ${client.network}`
340
- );
341
- }
342
- const calldata = buildResolveCalldata(name.toLowerCase());
343
- const result = await client.call("eth_call", [
344
- { to: UNIVERSAL_RESOLVER, data: calldata },
345
- "latest"
346
- ]);
347
- if (!result || result === "0x" || result.length < 130) {
348
- throw errInvalidArgs(`ENS name "${name}" could not be resolved.`);
349
- }
350
- const raw = result.slice(2);
351
- const dataOffset = parseInt(raw.slice(0, 64), 16) * 2;
352
- const dataLen = parseInt(raw.slice(dataOffset, dataOffset + 64), 16);
353
- const dataHex = raw.slice(dataOffset + 64, dataOffset + 64 + dataLen * 2);
354
- if (dataHex.length < 64) {
355
- throw errInvalidArgs(`ENS name "${name}" could not be resolved.`);
356
- }
357
- const address = "0x" + dataHex.slice(24, 64);
358
- if (address === "0x0000000000000000000000000000000000000000") {
359
- throw errInvalidArgs(`ENS name "${name}" is not registered or has no address set.`);
360
- }
361
- return address;
362
- }
363
-
364
- // src/lib/validators.ts
365
- function splitCommaList(input) {
366
- return input.split(",").map((s) => s.trim()).filter(Boolean);
367
- }
368
- var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
369
- var TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
370
- async function readStdinArg(name) {
371
- if (process.stdin.isTTY) {
372
- throw errInvalidArgs(`Missing <${name}>. Provide it as an argument or pipe via stdin.`);
373
- }
374
- process.stdin.setEncoding("utf-8");
375
- let input = "";
376
- for await (const chunk of process.stdin) {
377
- input += chunk;
378
- }
379
- const data = input.trim().split("\n")[0]?.trim() ?? "";
380
- if (!data) {
381
- throw errInvalidArgs(`No <${name}> received on stdin.`);
382
- }
383
- return data;
384
- }
385
- async function readStdinLines(name) {
386
- if (process.stdin.isTTY) {
387
- throw errInvalidArgs(`Missing <${name}>. Provide it as an argument or pipe via stdin.`);
388
- }
389
- process.stdin.setEncoding("utf-8");
390
- let input = "";
391
- for await (const chunk of process.stdin) {
392
- input += chunk;
393
- }
394
- const lines = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
395
- if (lines.length === 0) {
396
- throw errInvalidArgs(`No <${name}> received on stdin.`);
397
- }
398
- return lines;
399
- }
400
- function validateAddress(address) {
401
- if (!ADDRESS_RE.test(address)) {
402
- throw errInvalidArgs(
403
- `Invalid address "${address}". Expected 0x-prefixed 40-hex-character address.`
404
- );
405
- }
406
- }
407
- async function resolveAddress(input, client) {
408
- if (isENSName(input)) {
409
- return resolveENS(input, client);
410
- }
411
- validateAddress(input);
412
- return input;
413
- }
414
- function validateTxHash(hash) {
415
- if (!TX_HASH_RE.test(hash)) {
416
- throw errInvalidArgs(
417
- `Invalid transaction hash "${hash}". Expected 0x-prefixed 64-hex-character hash.`
418
- );
419
- }
420
- }
421
-
422
- // src/commands/config.ts
423
- var RESET_KEY_MAP = { ...KEY_MAP, app: "app" };
424
- var APP_SEARCH_THRESHOLD = 15;
425
- async function saveAppWithPrompt(app) {
426
- const cfg = load();
427
- const updated = {
428
- ...cfg,
429
- api_key: app.apiKey,
430
- app: { id: app.id, name: app.name, apiKey: app.apiKey, webhookApiKey: app.webhookApiKey }
431
- };
432
- if (cfg.api_key) {
433
- const replace = await promptConfirm({
434
- message: "You already have an API key configured. Use the app's API key instead?",
435
- initialValue: true,
436
- cancelMessage: "Cancelled default app update."
437
- });
438
- if (replace === null) {
439
- return false;
440
- }
441
- if (!replace) {
442
- updated.api_key = cfg.api_key;
443
- }
444
- }
445
- save(updated);
446
- return true;
447
- }
448
- async function selectOrCreateApp(admin) {
449
- let apps;
450
- try {
451
- const result = await withSpinner(
452
- "Fetching apps\u2026",
453
- "Apps fetched",
454
- () => admin.listAllApps()
455
- );
456
- apps = result.apps;
457
- } catch {
458
- console.log(
459
- ` ${dim("Could not fetch apps. Skipping app selection.")}`
460
- );
461
- return;
462
- }
463
- if (apps.length > 0) {
464
- const CREATE_NEW = "__create_new__";
465
- const options = [
466
- ...apps.map((a) => ({
467
- label: `${a.name} (${a.id})`,
468
- value: a.id
469
- })),
470
- { label: "Create a new app", value: CREATE_NEW }
471
- ];
472
- const selected = apps.length > APP_SEARCH_THRESHOLD ? await promptAutocomplete({
473
- message: "Select default app",
474
- placeholder: "Type app name or id",
475
- options,
476
- cancelMessage: "Cancelled app selection.",
477
- commitLabel: null
478
- }) : await promptSelect({
479
- message: "Select default app",
480
- options,
481
- cancelMessage: "Cancelled app selection.",
482
- commitLabel: null
483
- });
484
- if (selected === null) {
485
- return;
486
- }
487
- if (selected !== CREATE_NEW) {
488
- const app = apps.find((a) => a.id === selected);
489
- const saved = await saveAppWithPrompt(app);
490
- if (saved) {
491
- console.log(`
492
- ${green("\u2713")} Default app set to ${app.name} (${app.id})`);
493
- } else {
494
- console.log(` ${dim("Skipped setting default app.")}`);
495
- }
496
- return;
497
- }
498
- } else {
499
- console.log(` ${dim("No apps found. Let's create one.")}`);
500
- }
501
- const name = await promptText({
502
- message: "App name",
503
- cancelMessage: "Cancelled app creation."
504
- });
505
- if (name === null) {
506
- return;
507
- }
508
- if (!name.trim()) {
509
- console.log(` ${dim("Skipped app creation.")}`);
510
- return;
511
- }
512
- let chainChoices = [];
513
- try {
514
- const chains = await withSpinner(
515
- "Fetching chains\u2026",
516
- "Chains fetched",
517
- () => admin.listChains()
518
- );
519
- chainChoices = chains.filter((c) => c.availability === "public" && !c.isTestnet).map((c) => ({ label: `${c.name} (${c.id})`, value: c.id }));
520
- } catch {
521
- }
522
- let networks;
523
- if (chainChoices.length > 0) {
524
- const selectedNetworks = await promptMultiselect({
525
- message: "Select networks",
526
- options: chainChoices,
527
- required: true,
528
- cancelMessage: "Cancelled network selection."
529
- });
530
- if (selectedNetworks === null) {
531
- return;
532
- }
533
- networks = selectedNetworks;
534
- } else {
535
- const raw = await promptText({
536
- message: "Network IDs (comma-separated)",
537
- cancelMessage: "Cancelled network selection."
538
- });
539
- if (raw === null) {
540
- return;
541
- }
542
- networks = splitCommaList(raw);
543
- }
544
- if (networks.length === 0) {
545
- console.log(` ${dim("No networks selected. Skipped app creation.")}`);
546
- return;
547
- }
548
- try {
549
- const app = await withSpinner(
550
- "Creating app\u2026",
551
- "App created",
552
- () => admin.createApp({ name: name.trim(), networks })
553
- );
554
- console.log(` ${green("\u2713")} Created app ${app.name} (${app.id})`);
555
- const setDefault = await promptConfirm({
556
- message: "Set as default app?",
557
- initialValue: true,
558
- cancelMessage: "Cancelled default app selection."
559
- });
560
- if (setDefault === null) {
561
- return;
562
- }
563
- if (setDefault) {
564
- const saved = await saveAppWithPrompt(app);
565
- if (saved) {
566
- console.log(`
567
- ${green("\u2713")} Default app set to ${app.name} (${app.id})`);
568
- } else {
569
- console.log(` ${dim("Skipped setting default app.")}`);
570
- }
571
- }
572
- } catch (err) {
573
- exitWithError(err);
574
- }
575
- }
576
- function registerConfig(program) {
577
- const cmd = program.command("config").description("Manage CLI configuration");
578
- const setCmd = cmd.command("set").description("Set a config value");
579
- setCmd.command("api-key <key>").description("Set the Alchemy API key for RPC requests").action((key) => {
580
- try {
581
- const cfg = load();
582
- save({ ...cfg, api_key: key });
583
- printHuman(`${green("\u2713")} Set api-key
584
- `, { key: "api-key", status: "set" });
585
- if (!isJSONMode() && cfg.app?.apiKey && cfg.app.apiKey !== key) {
586
- console.log(
587
- ` ${yellow("\u25C6")} ${dim("Warning: api-key differs from the selected app key. RPC commands use api-key; run 'alchemy config set app <app-id>' to resync.")}`
588
- );
589
- }
590
- } catch (err) {
591
- exitWithError(err);
592
- }
593
- });
594
- setCmd.command("access-key <key>").description("Set the Alchemy access key for Admin API operations").action(async (key) => {
595
- try {
596
- const cfg = load();
597
- save({ ...cfg, access_key: key });
598
- printHuman(`${green("\u2713")} Set access-key
599
- `, { key: "access-key", status: "set" });
600
- if (isInteractiveAllowed(program)) {
601
- await selectOrCreateApp(new AdminClient(key));
602
- }
603
- } catch (err) {
604
- exitWithError(err);
605
- }
606
- });
607
- setCmd.command("webhook-api-key <key>").description("Set the Alchemy webhook API key for Notify operations").action((key) => {
608
- try {
609
- const cfg = load();
610
- save({ ...cfg, webhook_api_key: key });
611
- printHuman(`${green("\u2713")} Set webhook-api-key
612
- `, { key: "webhook-api-key", status: "set" });
613
- } catch (err) {
614
- exitWithError(err);
615
- }
616
- });
617
- setCmd.command("app [app-id]").description("Select the default app (interactive) or set by ID").action(async (appId) => {
618
- try {
619
- const cfg = load();
620
- const accessKey = program.opts().accessKey || process.env.ALCHEMY_ACCESS_KEY || cfg.access_key;
621
- if (!accessKey) throw errAccessKeyRequired();
622
- if (appId) {
623
- const admin = new AdminClient(accessKey);
624
- const app = await withSpinner(
625
- "Fetching app\u2026",
626
- "App fetched",
627
- () => admin.getApp(appId)
628
- );
629
- const updated = {
630
- ...cfg,
631
- api_key: app.apiKey,
632
- app: { id: app.id, name: app.name, apiKey: app.apiKey, webhookApiKey: app.webhookApiKey }
633
- };
634
- save(updated);
635
- printHuman(
636
- `${green("\u2713")} Default app set to ${app.name} (${app.id})
637
- `,
638
- { app: { id: app.id, name: app.name }, status: "set" }
639
- );
640
- return;
641
- }
642
- if (!isInteractiveAllowed(program)) {
643
- exitWithError(
644
- new Error("Interactive app selection requires an interactive terminal. Use 'config set app <app-id>' or 'alchemy apps list' to find app IDs.")
645
- );
646
- }
647
- await selectOrCreateApp(new AdminClient(accessKey));
648
- } catch (err) {
649
- exitWithError(err);
650
- }
651
- });
652
- setCmd.command("network <network>").description("Set the default network (e.g. eth-mainnet, polygon-mainnet)").action((network) => {
653
- try {
654
- const cfg = load();
655
- save({ ...cfg, network });
656
- printHuman(`${green("\u2713")} Set network to ${network}
657
- `, { key: "network", value: network, status: "set" });
658
- } catch (err) {
659
- exitWithError(err);
660
- }
661
- });
662
- setCmd.command("verbose <enabled>").description("Set default verbose output (true|false)").action((enabled) => {
663
- try {
664
- const normalized = enabled.trim().toLowerCase();
665
- if (normalized !== "true" && normalized !== "false") {
666
- throw errInvalidArgs("verbose must be 'true' or 'false'");
667
- }
668
- const verbose2 = normalized === "true";
669
- const cfg = load();
670
- save({ ...cfg, verbose: verbose2 });
671
- printHuman(
672
- `${green("\u2713")} Set verbose default to ${verbose2}
673
- `,
674
- { key: "verbose", value: String(verbose2), status: "set" }
675
- );
676
- } catch (err) {
677
- exitWithError(err);
678
- }
679
- });
680
- setCmd.command("wallet-key-file <path>").description("Set the path to a wallet private key file for x402").action((path) => {
681
- try {
682
- const cfg = load();
683
- save({ ...cfg, wallet_key_file: path });
684
- printHuman(`${green("\u2713")} Set wallet-key-file
685
- `, { key: "wallet-key-file", status: "set" });
686
- } catch (err) {
687
- exitWithError(err);
688
- }
689
- });
690
- setCmd.command("x402 <enabled>").description("Enable or disable x402 wallet-based auth by default (true|false)").action((enabled) => {
691
- try {
692
- const normalized = enabled.trim().toLowerCase();
693
- if (normalized !== "true" && normalized !== "false") {
694
- throw errInvalidArgs("x402 must be 'true' or 'false'");
695
- }
696
- const x402 = normalized === "true";
697
- const cfg = load();
698
- save({ ...cfg, x402 });
699
- printHuman(
700
- `${green("\u2713")} Set x402 default to ${x402}
701
- `,
702
- { key: "x402", value: String(x402), status: "set" }
703
- );
704
- } catch (err) {
705
- exitWithError(err);
706
- }
707
- });
708
- cmd.command("get <key>").description("Get a config value (api-key, access-key, app, network, verbose, wallet-key-file, x402)").action((key) => {
709
- const cfg = load();
710
- let value = get(cfg, key);
711
- let isDefault = false;
712
- if (value === void 0) {
713
- const defaults = {
714
- network: "eth-mainnet",
715
- verbose: "false",
716
- x402: "false"
717
- };
718
- const normalizedKey = KEY_MAP[key] ?? key;
719
- const defaultValue = defaults[normalizedKey] ?? defaults[key];
720
- if (defaultValue !== void 0) {
721
- value = defaultValue;
722
- isDefault = true;
723
- }
724
- }
725
- if (value === void 0) {
726
- exitWithError(errNotFound(`config key '${key}'`));
727
- }
728
- const isSecret = key === "api-key" || key === "api_key" || key === "access-key" || key === "access_key";
729
- const display = isSecret ? maskIf(value) : value;
730
- const humanDisplay = isDefault ? `${display} ${dim("(default)")}` : display;
731
- printHuman(humanDisplay + "\n", { key, value: display, ...isDefault && { default: true } });
732
- });
733
- cmd.command("list").description("List all config values").action(() => {
734
- const cfg = load();
735
- const hasApiKeyMismatch = Boolean(
736
- cfg.api_key && cfg.app?.apiKey && cfg.api_key !== cfg.app.apiKey
737
- );
738
- if (isJSONMode()) {
739
- printJSON(toMap(cfg));
740
- return;
741
- }
742
- const pairs = [
743
- [
744
- "api-key",
745
- cfg.api_key ? `${hasApiKeyMismatch ? `${yellow("\u25C6")} ` : ""}${maskIf(cfg.api_key)}` : dim("(not set)")
746
- ],
747
- ["access-key", cfg.access_key ? maskIf(cfg.access_key) : dim("(not set)")],
748
- [
749
- "app",
750
- cfg.app ? `${cfg.app.name} ${dim(`(${cfg.app.id})`)}` : dim("(not set) \u2014 set automatically via 'config set access-key' or 'config set app'")
751
- ],
752
- ["network", cfg.network || dim("(not set, defaults to eth-mainnet)")],
753
- [
754
- "verbose",
755
- cfg.verbose !== void 0 ? String(cfg.verbose) : dim("(not set, defaults to false)")
756
- ],
757
- ["wallet-key-file", cfg.wallet_key_file || dim("(not set)")],
758
- ["wallet-address", cfg.wallet_address || dim("(not set)")],
759
- [
760
- "x402",
761
- cfg.x402 !== void 0 ? String(cfg.x402) : dim("(not set, defaults to false)")
762
- ]
763
- ];
764
- printKeyValueBox(pairs);
765
- if (hasApiKeyMismatch) {
766
- console.log("");
767
- console.log(
768
- ` ${yellow("\u25C6")} ${dim("Warning: api-key differs from the selected app key. RPC commands use api-key; run 'alchemy config set app <app-id>' to resync.")}`
769
- );
770
- }
771
- });
772
- cmd.command("reset [key]").description("Reset config values (all or a specific key)").option("-y, --yes", "Skip confirmation prompt for full reset").action(async (key, options) => {
773
- try {
774
- if (key) {
775
- const mapped = RESET_KEY_MAP[key];
776
- if (!mapped) {
777
- throw errInvalidArgs(
778
- `invalid reset key '${key}' (valid: api-key, access-key, app, network, verbose, wallet-key-file, x402)`
779
- );
780
- }
781
- const cfg = load();
782
- const updated = { ...cfg };
783
- delete updated[mapped];
784
- save(updated);
785
- printHuman(`${green("\u2713")} Reset ${key}
786
- `, {
787
- status: "reset",
788
- key
789
- });
790
- return;
791
- }
792
- if (!options.yes && isInteractiveAllowed(program)) {
793
- const proceed = await promptConfirm({
794
- message: "Reset all saved config values?",
795
- initialValue: false,
796
- cancelMessage: "Cancelled config reset."
797
- });
798
- if (proceed === null) {
799
- return;
800
- }
801
- if (!proceed) {
802
- console.log(` ${dim("Skipped config reset.")}`);
803
- return;
804
- }
805
- }
806
- save({});
807
- printHuman(`${green("\u2713")} Reset all config values
808
- `, {
809
- status: "reset",
810
- scope: "all"
811
- });
812
- } catch (err) {
813
- exitWithError(err);
814
- }
815
- });
816
- }
817
-
818
- // src/commands/wallet.ts
819
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
820
- import { join, dirname } from "path";
821
- import { randomUUID } from "crypto";
822
- import { generateWallet, getWalletAddress } from "@alchemy/x402";
823
-
824
- // src/lib/resolve.ts
825
- import { readFileSync } from "fs";
826
-
827
- // src/lib/client.ts
828
- var Client = class _Client {
829
- apiKey;
830
- network;
831
- // Test/debug only: used by mock E2E to route CLI requests locally.
832
- static RPC_BASE_URL_ENV = "ALCHEMY_RPC_BASE_URL";
833
- constructor(apiKey, network) {
834
- this.apiKey = apiKey;
835
- this.network = network;
836
- this.validateNetwork(network);
837
- }
838
- validateNetwork(network) {
839
- if (this.rpcBaseURLOverride()) {
840
- return;
841
- }
842
- const hostname = `${network}.g.alchemy.com`;
843
- let parsed;
844
- try {
845
- parsed = new URL(`https://${hostname}`);
846
- } catch {
847
- throw errInvalidArgs(
848
- `Unknown network '${network}'. Run 'alchemy network list' to see available networks.`
849
- );
850
- }
851
- if (!parsed.hostname.endsWith(".g.alchemy.com")) {
852
- throw errInvalidArgs(
853
- `Unknown network '${network}'. Run 'alchemy network list' to see available networks.`
854
- );
855
- }
856
- }
857
- rpcBaseURLOverride() {
858
- return parseBaseURLOverride(_Client.RPC_BASE_URL_ENV);
859
- }
860
- rpcBaseURL() {
861
- const override = this.rpcBaseURLOverride();
862
- if (override) return override;
863
- return new URL(`https://${this.network}.g.alchemy.com`);
864
- }
865
- rpcURL() {
866
- return new URL(`/v2/${this.apiKey}`, this.rpcBaseURL()).toString();
867
- }
868
- enhancedURL() {
869
- return new URL(`/nft/v3/${this.apiKey}`, this.rpcBaseURL()).toString();
870
- }
871
- parseNetworkNotEnabledError(detail) {
872
- const match = detail.match(
873
- /([A-Z0-9_]+)\s+is not enabled for this app\.\s+Visit this page to enable the network:\s+(https?:\/\/\S+)/i
874
- );
875
- if (!match) return null;
876
- return errNetworkNotEnabled(match[1], detail);
877
- }
878
- authErrorFromResponseBody(detail) {
879
- const networkNotEnabled = this.parseNetworkNotEnabledError(detail);
880
- if (networkNotEnabled) return networkNotEnabled;
881
- return errInvalidAPIKey(detail || void 0);
882
- }
883
- tryParseRPCError(text) {
884
- try {
885
- const parsed = JSON.parse(text);
886
- if (parsed?.error?.code !== void 0 && parsed?.error?.message !== void 0) {
887
- return errRPC(parsed.error.code, parsed.error.message);
888
- }
889
- } catch {
890
- }
891
- return null;
892
- }
893
- verboseLog(message) {
894
- if (verbose) {
895
- process.stderr.write(`[verbose] ${message}
896
- `);
897
- }
898
- }
899
- async doFetch(url, init) {
900
- return fetchWithTimeout(url, init);
901
- }
902
- async call(method, params = []) {
903
- const body = {
904
- jsonrpc: "2.0",
905
- method,
906
- params,
907
- id: 1
908
- };
909
- const redactedURL = redactSensitiveText(this.rpcURL());
910
- this.verboseLog(`\u2192 POST ${redactedURL}`);
911
- this.verboseLog(` method: ${method}`);
912
- const hasParams = Array.isArray(params) ? params.length > 0 : Object.keys(params).length > 0;
913
- if (hasParams) {
914
- this.verboseLog(` params: ${JSON.stringify(params)}`);
915
- }
916
- const startTime = Date.now();
917
- const resp = await this.doFetch(this.rpcURL(), {
918
- method: "POST",
919
- headers: {
920
- "Content-Type": "application/json",
921
- Accept: "application/json"
922
- },
923
- body: JSON.stringify(body)
924
- });
925
- this.verboseLog(`\u2190 ${resp.status} ${resp.statusText} (${Date.now() - startTime}ms)`);
926
- if (resp.status === 429) throw errRateLimited();
927
- if (resp.status === 401 || resp.status === 403) {
928
- const detail = await resp.text().catch(() => "");
929
- throw this.authErrorFromResponseBody(detail);
930
- }
931
- if (!resp.ok) {
932
- const text = await resp.text().catch(() => "");
933
- const rpcError = this.tryParseRPCError(text);
934
- if (rpcError) throw rpcError;
935
- throw errNetwork(`HTTP ${resp.status}: ${text}`);
936
- }
937
- const rpcResp = await resp.json();
938
- if (rpcResp.error) {
939
- throw errRPC(rpcResp.error.code, rpcResp.error.message);
940
- }
941
- return rpcResp.result;
942
- }
943
- async callEnhanced(path, params) {
944
- const url = new URL(`${this.enhancedURL()}/${path}`);
945
- for (const [k, v] of Object.entries(params)) {
946
- url.searchParams.set(k, v);
947
- }
948
- const redactedURL = redactSensitiveText(url.toString());
949
- this.verboseLog(`\u2192 GET ${redactedURL}`);
950
- const startTime = Date.now();
951
- const resp = await this.doFetch(url.toString(), {
952
- headers: { Accept: "application/json" }
953
- });
954
- this.verboseLog(`\u2190 ${resp.status} ${resp.statusText} (${Date.now() - startTime}ms)`);
955
- if (resp.status === 429) throw errRateLimited();
956
- if (resp.status === 401 || resp.status === 403) {
957
- const detail = await resp.text().catch(() => "");
958
- throw this.authErrorFromResponseBody(detail);
959
- }
960
- if (!resp.ok) {
961
- const text = await resp.text().catch(() => "");
962
- const rpcError = this.tryParseRPCError(text);
963
- if (rpcError) throw rpcError;
964
- throw errNetwork(`HTTP ${resp.status}: ${text}`);
965
- }
966
- return resp.json();
967
- }
968
- };
969
-
970
- // src/lib/x402-client.ts
971
- import { signSiwe, createPayment } from "@alchemy/x402";
972
- var X402Client = class _X402Client {
973
- network;
974
- privateKey;
975
- siweToken = null;
976
- static X402_BASE_URL_ENV = "ALCHEMY_X402_BASE_URL";
977
- static DEFAULT_BASE = "https://x402.alchemy.com";
978
- constructor(privateKey, network) {
979
- this.privateKey = privateKey;
980
- this.network = network;
981
- this.validateNetwork(network);
982
- }
983
- validateNetwork(network) {
984
- if (this.baseURLOverride()) return;
985
- if (!/^[A-Za-z0-9:_-]{1,128}$/.test(network)) {
986
- throw errInvalidArgs(`Invalid network: ${network}`);
987
- }
988
- }
989
- baseURLOverride() {
990
- return parseBaseURLOverride(_X402Client.X402_BASE_URL_ENV);
991
- }
992
- baseURL() {
993
- const override = this.baseURLOverride();
994
- if (override) return override;
995
- return new URL(_X402Client.DEFAULT_BASE);
996
- }
997
- rpcURL() {
998
- return new URL(`/${this.network}/v2`, this.baseURL()).toString();
999
- }
1000
- enhancedURL() {
1001
- return new URL(`/${this.network}/nft/v3`, this.baseURL()).toString();
1002
- }
1003
- async ensureSiweToken() {
1004
- if (this.siweToken) return this.siweToken;
1005
- this.siweToken = await signSiwe({
1006
- privateKey: this.privateKey,
1007
- expiresAfter: "1h"
1008
- });
1009
- return this.siweToken;
1010
- }
1011
- refreshSiweToken() {
1012
- this.siweToken = null;
1013
- }
1014
- async call(method, params = []) {
1015
- const body = { jsonrpc: "2.0", method, params, id: 1 };
1016
- const jsonBody = JSON.stringify(body);
1017
- const buildInit = (extra) => ({
1018
- method: "POST",
1019
- headers: {
1020
- "Content-Type": "application/json",
1021
- Accept: "application/json",
1022
- Authorization: `SIWE ${this.siweToken}`,
1023
- ...extra
1024
- },
1025
- body: jsonBody
1026
- });
1027
- await this.ensureSiweToken();
1028
- let resp = await this.doFetch(this.rpcURL(), buildInit());
1029
- resp = await this.handleAuthAndPayment(resp, {
1030
- authRetry: async () => {
1031
- this.refreshSiweToken();
1032
- await this.ensureSiweToken();
1033
- return this.doFetch(this.rpcURL(), buildInit());
1034
- },
1035
- paymentRetry: async (paymentSig) => this.doFetch(this.rpcURL(), buildInit({ "Payment-Signature": paymentSig }))
1036
- });
1037
- if (resp.status === 429) throw errRateLimited();
1038
- if (resp.status === 402) throw await this.parsePaymentError(resp);
1039
- if (!resp.ok) {
1040
- const text = await resp.text().catch(() => "");
1041
- throw errNetwork(`HTTP ${resp.status}: ${text}`);
1042
- }
1043
- const rpcResp = await resp.json();
1044
- if (rpcResp.error) {
1045
- throw errRPC(rpcResp.error.code, rpcResp.error.message);
1046
- }
1047
- return rpcResp.result;
1048
- }
1049
- async callEnhanced(path, params) {
1050
- const url = new URL(`${this.enhancedURL()}/${path}`);
1051
- for (const [k, v] of Object.entries(params)) {
1052
- url.searchParams.set(k, v);
1053
- }
1054
- const urlStr = url.toString();
1055
- const buildInit = (extra) => ({
1056
- headers: {
1057
- Accept: "application/json",
1058
- Authorization: `SIWE ${this.siweToken}`,
1059
- ...extra
1060
- }
1061
- });
1062
- await this.ensureSiweToken();
1063
- let resp = await this.doFetch(urlStr, buildInit());
1064
- resp = await this.handleAuthAndPayment(resp, {
1065
- authRetry: async () => {
1066
- this.refreshSiweToken();
1067
- await this.ensureSiweToken();
1068
- return this.doFetch(urlStr, buildInit());
1069
- },
1070
- paymentRetry: async (paymentSig) => this.doFetch(urlStr, buildInit({ "Payment-Signature": paymentSig }))
1071
- });
1072
- if (resp.status === 429) throw errRateLimited();
1073
- if (resp.status === 402) throw await this.parsePaymentError(resp);
1074
- if (!resp.ok) {
1075
- const text = await resp.text().catch(() => "");
1076
- throw errNetwork(`HTTP ${resp.status}: ${text}`);
1077
- }
1078
- return resp.json();
1079
- }
1080
- async doFetch(url, init) {
1081
- return fetchWithTimeout(url, init);
1082
- }
1083
- async parsePaymentError(resp) {
1084
- const text = await resp.text().catch(() => "");
1085
- try {
1086
- const body = JSON.parse(text);
1087
- const reason = body?.extensions?.paymentError?.info?.reason;
1088
- const message = body?.extensions?.paymentError?.info?.message;
1089
- const payer = body?.extensions?.paymentError?.info?.payer;
1090
- if (reason === "insufficient_funds") {
1091
- const network = body?.accepts?.[0]?.network;
1092
- const asset = body?.accepts?.[0]?.extra?.name ?? "USDC";
1093
- const networkLabel = network === "eip155:8453" ? "Base" : network ?? "the payment network";
1094
- return new CLIError(
1095
- ErrorCode.PAYMENT_REQUIRED,
1096
- `Insufficient ${asset} balance on ${networkLabel}. ${message ?? ""}`.trim(),
1097
- `Fund wallet ${payer ?? ""} with ${asset} on ${networkLabel} to use x402.`.trim()
1098
- );
1099
- }
1100
- return new CLIError(
1101
- ErrorCode.PAYMENT_REQUIRED,
1102
- `x402 payment failed: ${message || body?.error || text}`
1103
- );
1104
- } catch {
1105
- return new CLIError(
1106
- ErrorCode.PAYMENT_REQUIRED,
1107
- `x402 payment failed: ${text}`
1108
- );
1109
- }
1110
- }
1111
- async handleAuthAndPayment(resp, retries) {
1112
- if (resp.status === 401) {
1113
- const detail = await resp.text().catch(() => "");
1114
- if (detail.includes("MESSAGE_EXPIRED")) {
1115
- return retries.authRetry();
1116
- }
1117
- throw new CLIError(
1118
- ErrorCode.AUTH_REQUIRED,
1119
- `x402 authentication failed: ${detail || "unauthorized"}`,
1120
- "Check your wallet key and try again."
1121
- );
1122
- }
1123
- if (resp.status === 402) {
1124
- const paymentRequiredHeader = resp.headers.get("payment-required");
1125
- if (!paymentRequiredHeader) {
1126
- throw new CLIError(
1127
- ErrorCode.PAYMENT_REQUIRED,
1128
- "x402 payment required but no Payment-Required header received."
1129
- );
1130
- }
1131
- const paymentSignature = await createPayment({
1132
- privateKey: this.privateKey,
1133
- paymentRequiredHeader
1134
- });
1135
- return retries.paymentRetry(paymentSignature);
1136
- }
1137
- return resp;
1138
- }
1139
- };
1140
-
1141
- // src/lib/resolve.ts
1142
- function resolveAPIKey(program, cfg) {
1143
- const opts = program.opts();
1144
- if (opts.apiKey) return opts.apiKey;
1145
- if (process.env.ALCHEMY_API_KEY) return process.env.ALCHEMY_API_KEY;
1146
- const config = cfg ?? load();
1147
- if (config.api_key) return config.api_key;
1148
- if (config.app?.apiKey) return config.app.apiKey;
1149
- return void 0;
1150
- }
1151
- function resolveAccessKey(program, cfg) {
1152
- const opts = program.opts();
1153
- if (opts.accessKey) return opts.accessKey;
1154
- if (process.env.ALCHEMY_ACCESS_KEY) return process.env.ALCHEMY_ACCESS_KEY;
1155
- const config = cfg ?? load();
1156
- if (config.access_key) return config.access_key;
1157
- return void 0;
1158
- }
1159
- function resolveNetwork(program, cfg, defaultNetwork) {
1160
- const opts = program.opts();
1161
- if (opts.network) return opts.network;
1162
- if (process.env.ALCHEMY_NETWORK) return process.env.ALCHEMY_NETWORK;
1163
- const config = cfg ?? load();
1164
- if (config.network) return config.network;
1165
- return defaultNetwork ?? "eth-mainnet";
1166
- }
1167
- function resolveAppId(program, cfg) {
1168
- const opts = program.opts();
1169
- if (opts.appId) return opts.appId;
1170
- const config = cfg ?? load();
1171
- if (config.app?.id) return config.app.id;
1172
- return void 0;
1173
- }
1174
- function adminClientFromFlags(program) {
1175
- const accessKey = resolveAccessKey(program);
1176
- if (!accessKey) throw errAccessKeyRequired();
1177
- return new AdminClient(accessKey);
1178
- }
1179
- function resolveX402(program, cfg) {
1180
- const opts = program.opts();
1181
- if (opts.x402) return true;
1182
- const config = cfg ?? load();
1183
- return config.x402 === true;
1184
- }
1185
- function resolveWalletKey(program, cfg) {
1186
- const opts = program.opts();
1187
- if (opts.walletKeyFile) {
1188
- return readFileSync(opts.walletKeyFile, "utf-8").trim();
1189
- }
1190
- if (process.env.ALCHEMY_WALLET_KEY) {
1191
- return process.env.ALCHEMY_WALLET_KEY;
1192
- }
1193
- const config = cfg ?? load();
1194
- if (config.wallet_key_file) {
1195
- return readFileSync(config.wallet_key_file, "utf-8").trim();
1196
- }
1197
- return void 0;
1198
- }
1199
- function clientFromFlags(program, opts) {
1200
- const cfg = load();
1201
- const network = resolveNetwork(program, cfg, opts?.defaultNetwork);
1202
- debug(`using network=${network}`);
1203
- const programOpts = program.opts();
1204
- if (programOpts.accessKey) {
1205
- throw errInvalidArgs(
1206
- "--access-key is for admin commands (apps, chains, webhooks). Use --api-key for RPC commands."
1207
- );
1208
- }
1209
- if (resolveX402(program, cfg)) {
1210
- const walletKey = resolveWalletKey(program, cfg);
1211
- if (!walletKey) throw errWalletKeyRequired();
1212
- return new X402Client(walletKey, network);
1213
- }
1214
- const apiKey = resolveAPIKey(program, cfg);
1215
- if (!apiKey) throw errAuthRequired();
1216
- return new Client(apiKey, network);
1217
- }
1218
-
1219
- // src/commands/wallet.ts
1220
- var WALLET_KEYS_DIR = "wallet-keys";
1221
- var UUID_SLICE_LEN = 8;
1222
- var ADDRESS_SLICE_LEN = 12;
1223
- function walletKeysDirPath() {
1224
- return join(configDir(), WALLET_KEYS_DIR);
1225
- }
1226
- function walletKeyPath(address) {
1227
- const addr = address.trim().toLowerCase().replace(/^0x/, "").replace(/[^a-z0-9]/g, "").slice(0, ADDRESS_SLICE_LEN);
1228
- const addressTag = addr || "unknown";
1229
- const fileName = `wallet-key-${addressTag}-${Date.now()}-${randomUUID().slice(0, UUID_SLICE_LEN)}.txt`;
1230
- return join(walletKeysDirPath(), fileName);
1231
- }
1232
- function persistWalletKey(privateKey, address) {
1233
- const keyPath = walletKeyPath(address);
1234
- mkdirSync(dirname(keyPath), { recursive: true, mode: 493 });
1235
- writeFileSync(keyPath, privateKey + "\n", { mode: 384, flag: "wx" });
1236
- return keyPath;
1237
- }
1238
- function generateAndPersistWallet() {
1239
- const wallet = generateWallet();
1240
- const keyPath = persistWalletKey(wallet.privateKey, wallet.address);
1241
- const cfg = load();
1242
- save({ ...cfg, wallet_key_file: keyPath, wallet_address: wallet.address });
1243
- return { address: wallet.address, keyFile: keyPath };
1244
- }
1245
- function importAndPersistWallet(path) {
1246
- let key;
1247
- try {
1248
- key = readFileSync2(path, "utf-8").trim();
1249
- } catch {
1250
- throw errInvalidArgs(`Could not read key file: ${path}`);
1251
- }
1252
- const address = getWalletAddress(key);
1253
- const keyPath = persistWalletKey(key, address);
1254
- const cfg = load();
1255
- save({ ...cfg, wallet_key_file: keyPath, wallet_address: address });
1256
- return { address, keyFile: keyPath };
1257
- }
1258
- function registerWallet(program) {
1259
- const cmd = program.command("wallet").description("Manage x402 wallet");
1260
- cmd.command("generate").description("Generate a new wallet for x402 authentication").action(() => {
1261
- try {
1262
- const wallet = generateAndPersistWallet();
1263
- if (isJSONMode()) {
1264
- printJSON(wallet);
1265
- } else {
1266
- printKeyValueBox([
1267
- ["Address", green(wallet.address)],
1268
- ["Key file", wallet.keyFile]
1269
- ]);
1270
- console.log(` ${green("\u2713")} Wallet generated and saved to config`);
1271
- }
1272
- } catch (err) {
1273
- exitWithError(err);
1274
- }
1275
- });
1276
- cmd.command("import").argument("<path>", "Path to private key file").description("Import a wallet from a private key file").action((path) => {
1277
- try {
1278
- const wallet = importAndPersistWallet(path);
1279
- if (isJSONMode()) {
1280
- printJSON(wallet);
1281
- } else {
1282
- printKeyValueBox([
1283
- ["Address", green(wallet.address)],
1284
- ["Key file", wallet.keyFile]
1285
- ]);
1286
- console.log(` ${green("\u2713")} Wallet imported and saved to config`);
1287
- }
1288
- } catch (err) {
1289
- exitWithError(err);
1290
- }
1291
- });
1292
- cmd.command("address").description("Display the address of the configured wallet").action(() => {
1293
- try {
1294
- const key = resolveWalletKey(program);
1295
- if (!key) throw errWalletKeyRequired();
1296
- const address = getWalletAddress(key);
1297
- printHuman(
1298
- `${address}
1299
- `,
1300
- { address }
1301
- );
1302
- } catch (err) {
1303
- exitWithError(err);
1304
- }
1305
- });
1306
- }
1307
-
1308
- export {
1309
- fetchWithTimeout,
1310
- AdminClient,
1311
- splitCommaList,
1312
- readStdinArg,
1313
- readStdinLines,
1314
- validateAddress,
1315
- resolveAddress,
1316
- validateTxHash,
1317
- selectOrCreateApp,
1318
- registerConfig,
1319
- resolveAPIKey,
1320
- resolveNetwork,
1321
- resolveAppId,
1322
- adminClientFromFlags,
1323
- clientFromFlags,
1324
- generateAndPersistWallet,
1325
- importAndPersistWallet,
1326
- registerWallet
1327
- };