@alchemy/cli 0.4.0 → 0.5.1
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/auth-7E33EMAI.js +13 -0
- package/dist/auth-E26YCAJV.js +23 -0
- package/dist/chunk-44OGGLN4.js +681 -0
- package/dist/chunk-56ZVYB4G.js +536 -0
- package/dist/{chunk-Z3LXQFIY.js → chunk-5X6YRTPU.js} +15 -5
- package/dist/chunk-IGD4NIK7.js +300 -0
- package/dist/chunk-LYUW7O6X.js +231 -0
- package/dist/chunk-T2XSNZE3.js +1398 -0
- package/dist/chunk-Z7J64GJJ.js +118 -0
- package/dist/index.js +127 -60
- package/dist/{interactive-NASSNJHQ.js → interactive-G4ON47AR.js} +12 -7
- package/dist/{onboarding-WQ2TWDM3.js → onboarding-CWCVWSUG.js} +57 -15
- package/package.json +1 -1
- package/scripts/postinstall.cjs +17 -1
- package/dist/chunk-PIWNNNMZ.js +0 -1327
- package/dist/chunk-TH75DFAY.js +0 -1246
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
|
|
3
|
+
import {
|
|
4
|
+
AdminClient,
|
|
5
|
+
KEY_MAP,
|
|
6
|
+
configDir,
|
|
7
|
+
dim,
|
|
8
|
+
get,
|
|
9
|
+
green,
|
|
10
|
+
isInteractiveAllowed,
|
|
11
|
+
load,
|
|
12
|
+
maskIf,
|
|
13
|
+
printKeyValueBox,
|
|
14
|
+
promptAutocomplete,
|
|
15
|
+
promptConfirm,
|
|
16
|
+
promptMultiselect,
|
|
17
|
+
promptSelect,
|
|
18
|
+
promptText,
|
|
19
|
+
resolveWalletKey,
|
|
20
|
+
save,
|
|
21
|
+
toMap,
|
|
22
|
+
withSpinner,
|
|
23
|
+
yellow
|
|
24
|
+
} from "./chunk-T2XSNZE3.js";
|
|
25
|
+
import {
|
|
26
|
+
errAccessKeyRequired,
|
|
27
|
+
errInvalidArgs,
|
|
28
|
+
errNotFound,
|
|
29
|
+
errWalletKeyRequired,
|
|
30
|
+
exitWithError,
|
|
31
|
+
isJSONMode,
|
|
32
|
+
printHuman,
|
|
33
|
+
printJSON
|
|
34
|
+
} from "./chunk-56ZVYB4G.js";
|
|
35
|
+
|
|
36
|
+
// src/lib/ens.ts
|
|
37
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
38
|
+
var UNIVERSAL_RESOLVER = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe";
|
|
39
|
+
var RESOLVE_SELECTOR = "9061b923";
|
|
40
|
+
var ADDR_SELECTOR = "3b3b57de";
|
|
41
|
+
function bytesToHex(bytes) {
|
|
42
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
43
|
+
}
|
|
44
|
+
function pad32(hex) {
|
|
45
|
+
return hex.padStart(64, "0");
|
|
46
|
+
}
|
|
47
|
+
function namehash(name) {
|
|
48
|
+
let node = new Uint8Array(32);
|
|
49
|
+
if (!name) return node;
|
|
50
|
+
const labels = name.split(".");
|
|
51
|
+
for (let i = labels.length - 1; i >= 0; i--) {
|
|
52
|
+
const labelHash = keccak_256(new TextEncoder().encode(labels[i]));
|
|
53
|
+
const combined = new Uint8Array(64);
|
|
54
|
+
combined.set(node, 0);
|
|
55
|
+
combined.set(labelHash, 32);
|
|
56
|
+
node = keccak_256(combined);
|
|
57
|
+
}
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
function dnsEncode(name) {
|
|
61
|
+
const labels = name.split(".");
|
|
62
|
+
const parts = [];
|
|
63
|
+
for (const label of labels) {
|
|
64
|
+
const encoded = new TextEncoder().encode(label);
|
|
65
|
+
parts.push(encoded.length);
|
|
66
|
+
parts.push(...encoded);
|
|
67
|
+
}
|
|
68
|
+
parts.push(0);
|
|
69
|
+
return new Uint8Array(parts);
|
|
70
|
+
}
|
|
71
|
+
function buildResolveCalldata(name) {
|
|
72
|
+
const dnsName = dnsEncode(name);
|
|
73
|
+
const node = namehash(name);
|
|
74
|
+
const innerHex = ADDR_SELECTOR + bytesToHex(node);
|
|
75
|
+
const innerLen = 36;
|
|
76
|
+
const dnsHex = bytesToHex(dnsName);
|
|
77
|
+
const nameLen = dnsName.length;
|
|
78
|
+
const namePad = Math.ceil(nameLen / 32) * 32;
|
|
79
|
+
const innerPad = Math.ceil(innerLen / 32) * 32;
|
|
80
|
+
const nameOffset = 64;
|
|
81
|
+
const dataOffset = nameOffset + 32 + namePad;
|
|
82
|
+
let hex = RESOLVE_SELECTOR;
|
|
83
|
+
hex += pad32(nameOffset.toString(16));
|
|
84
|
+
hex += pad32(dataOffset.toString(16));
|
|
85
|
+
hex += pad32(nameLen.toString(16));
|
|
86
|
+
hex += dnsHex.padEnd(namePad * 2, "0");
|
|
87
|
+
hex += pad32(innerLen.toString(16));
|
|
88
|
+
hex += innerHex.padEnd(innerPad * 2, "0");
|
|
89
|
+
return "0x" + hex;
|
|
90
|
+
}
|
|
91
|
+
function isENSName(value) {
|
|
92
|
+
return value.endsWith(".eth") && value.length > 4 && !value.startsWith("0x");
|
|
93
|
+
}
|
|
94
|
+
async function resolveENS(name, client) {
|
|
95
|
+
if (!client.network.startsWith("eth-")) {
|
|
96
|
+
throw errInvalidArgs(
|
|
97
|
+
`ENS resolution is only supported on Ethereum networks. Current network: ${client.network}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const calldata = buildResolveCalldata(name.toLowerCase());
|
|
101
|
+
const result = await client.call("eth_call", [
|
|
102
|
+
{ to: UNIVERSAL_RESOLVER, data: calldata },
|
|
103
|
+
"latest"
|
|
104
|
+
]);
|
|
105
|
+
if (!result || result === "0x" || result.length < 130) {
|
|
106
|
+
throw errInvalidArgs(`ENS name "${name}" could not be resolved.`);
|
|
107
|
+
}
|
|
108
|
+
const raw = result.slice(2);
|
|
109
|
+
const dataOffset = parseInt(raw.slice(0, 64), 16) * 2;
|
|
110
|
+
const dataLen = parseInt(raw.slice(dataOffset, dataOffset + 64), 16);
|
|
111
|
+
const dataHex = raw.slice(dataOffset + 64, dataOffset + 64 + dataLen * 2);
|
|
112
|
+
if (dataHex.length < 64) {
|
|
113
|
+
throw errInvalidArgs(`ENS name "${name}" could not be resolved.`);
|
|
114
|
+
}
|
|
115
|
+
const address = "0x" + dataHex.slice(24, 64);
|
|
116
|
+
if (address === "0x0000000000000000000000000000000000000000") {
|
|
117
|
+
throw errInvalidArgs(`ENS name "${name}" is not registered or has no address set.`);
|
|
118
|
+
}
|
|
119
|
+
return address;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/lib/validators.ts
|
|
123
|
+
function splitCommaList(input) {
|
|
124
|
+
return input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
125
|
+
}
|
|
126
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
127
|
+
var TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
128
|
+
async function readStdinArg(name) {
|
|
129
|
+
if (process.stdin.isTTY) {
|
|
130
|
+
throw errInvalidArgs(`Missing <${name}>. Provide it as an argument or pipe via stdin.`);
|
|
131
|
+
}
|
|
132
|
+
process.stdin.setEncoding("utf-8");
|
|
133
|
+
let input = "";
|
|
134
|
+
for await (const chunk of process.stdin) {
|
|
135
|
+
input += chunk;
|
|
136
|
+
}
|
|
137
|
+
const data = input.trim().split("\n")[0]?.trim() ?? "";
|
|
138
|
+
if (!data) {
|
|
139
|
+
throw errInvalidArgs(`No <${name}> received on stdin.`);
|
|
140
|
+
}
|
|
141
|
+
return data;
|
|
142
|
+
}
|
|
143
|
+
async function readStdinLines(name) {
|
|
144
|
+
if (process.stdin.isTTY) {
|
|
145
|
+
throw errInvalidArgs(`Missing <${name}>. Provide it as an argument or pipe via stdin.`);
|
|
146
|
+
}
|
|
147
|
+
process.stdin.setEncoding("utf-8");
|
|
148
|
+
let input = "";
|
|
149
|
+
for await (const chunk of process.stdin) {
|
|
150
|
+
input += chunk;
|
|
151
|
+
}
|
|
152
|
+
const lines = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
153
|
+
if (lines.length === 0) {
|
|
154
|
+
throw errInvalidArgs(`No <${name}> received on stdin.`);
|
|
155
|
+
}
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
function validateAddress(address) {
|
|
159
|
+
if (!ADDRESS_RE.test(address)) {
|
|
160
|
+
throw errInvalidArgs(
|
|
161
|
+
`Invalid address "${address}". Expected 0x-prefixed 40-hex-character address.`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function resolveAddress(input, client) {
|
|
166
|
+
if (isENSName(input)) {
|
|
167
|
+
return resolveENS(input, client);
|
|
168
|
+
}
|
|
169
|
+
validateAddress(input);
|
|
170
|
+
return input;
|
|
171
|
+
}
|
|
172
|
+
function validateTxHash(hash) {
|
|
173
|
+
if (!TX_HASH_RE.test(hash)) {
|
|
174
|
+
throw errInvalidArgs(
|
|
175
|
+
`Invalid transaction hash "${hash}". Expected 0x-prefixed 64-hex-character hash.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/commands/config.ts
|
|
181
|
+
var RESET_KEY_MAP = { ...KEY_MAP, app: "app" };
|
|
182
|
+
var APP_SEARCH_THRESHOLD = 15;
|
|
183
|
+
async function saveAppWithPrompt(app) {
|
|
184
|
+
const cfg = load();
|
|
185
|
+
const updated = {
|
|
186
|
+
...cfg,
|
|
187
|
+
api_key: app.apiKey,
|
|
188
|
+
app: { id: app.id, name: app.name, apiKey: app.apiKey, webhookApiKey: app.webhookApiKey }
|
|
189
|
+
};
|
|
190
|
+
if (cfg.api_key) {
|
|
191
|
+
const replace = await promptConfirm({
|
|
192
|
+
message: "You already have an API key configured. Use the app's API key instead?",
|
|
193
|
+
initialValue: true,
|
|
194
|
+
cancelMessage: "Cancelled default app update."
|
|
195
|
+
});
|
|
196
|
+
if (replace === null) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (!replace) {
|
|
200
|
+
updated.api_key = cfg.api_key;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
save(updated);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
async function selectOrCreateApp(admin) {
|
|
207
|
+
let apps;
|
|
208
|
+
try {
|
|
209
|
+
const result = await withSpinner(
|
|
210
|
+
"Fetching apps\u2026",
|
|
211
|
+
"Apps fetched",
|
|
212
|
+
() => admin.listAllApps()
|
|
213
|
+
);
|
|
214
|
+
apps = result.apps;
|
|
215
|
+
} catch {
|
|
216
|
+
console.log(
|
|
217
|
+
` ${dim("Could not fetch apps. Skipping app selection.")}`
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (apps.length > 0) {
|
|
222
|
+
const CREATE_NEW = "__create_new__";
|
|
223
|
+
const options = [
|
|
224
|
+
...apps.map((a) => ({
|
|
225
|
+
label: `${a.name} (${a.id})`,
|
|
226
|
+
value: a.id
|
|
227
|
+
})),
|
|
228
|
+
{ label: "Create a new app", value: CREATE_NEW }
|
|
229
|
+
];
|
|
230
|
+
const selected = apps.length > APP_SEARCH_THRESHOLD ? await promptAutocomplete({
|
|
231
|
+
message: "Select default app",
|
|
232
|
+
placeholder: "Type app name or id",
|
|
233
|
+
options,
|
|
234
|
+
cancelMessage: "Cancelled app selection.",
|
|
235
|
+
commitLabel: null
|
|
236
|
+
}) : await promptSelect({
|
|
237
|
+
message: "Select default app",
|
|
238
|
+
options,
|
|
239
|
+
cancelMessage: "Cancelled app selection.",
|
|
240
|
+
commitLabel: null
|
|
241
|
+
});
|
|
242
|
+
if (selected === null) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (selected !== CREATE_NEW) {
|
|
246
|
+
const app = apps.find((a) => a.id === selected);
|
|
247
|
+
const saved = await saveAppWithPrompt(app);
|
|
248
|
+
if (saved) {
|
|
249
|
+
console.log(`
|
|
250
|
+
${green("\u2713")} Default app set to ${app.name} (${app.id})`);
|
|
251
|
+
} else {
|
|
252
|
+
console.log(` ${dim("Skipped setting default app.")}`);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
console.log(` ${dim("No apps found. Let's create one.")}`);
|
|
258
|
+
}
|
|
259
|
+
const name = await promptText({
|
|
260
|
+
message: "App name",
|
|
261
|
+
cancelMessage: "Cancelled app creation."
|
|
262
|
+
});
|
|
263
|
+
if (name === null) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!name.trim()) {
|
|
267
|
+
console.log(` ${dim("Skipped app creation.")}`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let chainChoices = [];
|
|
271
|
+
try {
|
|
272
|
+
const chains = await withSpinner(
|
|
273
|
+
"Fetching chains\u2026",
|
|
274
|
+
"Chains fetched",
|
|
275
|
+
() => admin.listChains()
|
|
276
|
+
);
|
|
277
|
+
chainChoices = chains.filter((c) => c.availability === "public" && !c.isTestnet).map((c) => ({ label: `${c.name} (${c.id})`, value: c.id }));
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
let networks;
|
|
281
|
+
if (chainChoices.length > 0) {
|
|
282
|
+
const selectedNetworks = await promptMultiselect({
|
|
283
|
+
message: "Select networks",
|
|
284
|
+
options: chainChoices,
|
|
285
|
+
required: true,
|
|
286
|
+
cancelMessage: "Cancelled network selection."
|
|
287
|
+
});
|
|
288
|
+
if (selectedNetworks === null) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
networks = selectedNetworks;
|
|
292
|
+
} else {
|
|
293
|
+
const raw = await promptText({
|
|
294
|
+
message: "Network IDs (comma-separated)",
|
|
295
|
+
cancelMessage: "Cancelled network selection."
|
|
296
|
+
});
|
|
297
|
+
if (raw === null) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
networks = splitCommaList(raw);
|
|
301
|
+
}
|
|
302
|
+
if (networks.length === 0) {
|
|
303
|
+
console.log(` ${dim("No networks selected. Skipped app creation.")}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const app = await withSpinner(
|
|
308
|
+
"Creating app\u2026",
|
|
309
|
+
"App created",
|
|
310
|
+
() => admin.createApp({ name: name.trim(), networks })
|
|
311
|
+
);
|
|
312
|
+
console.log(` ${green("\u2713")} Created app ${app.name} (${app.id})`);
|
|
313
|
+
const setDefault = await promptConfirm({
|
|
314
|
+
message: "Set as default app?",
|
|
315
|
+
initialValue: true,
|
|
316
|
+
cancelMessage: "Cancelled default app selection."
|
|
317
|
+
});
|
|
318
|
+
if (setDefault === null) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (setDefault) {
|
|
322
|
+
const saved = await saveAppWithPrompt(app);
|
|
323
|
+
if (saved) {
|
|
324
|
+
console.log(`
|
|
325
|
+
${green("\u2713")} Default app set to ${app.name} (${app.id})`);
|
|
326
|
+
} else {
|
|
327
|
+
console.log(` ${dim("Skipped setting default app.")}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
exitWithError(err);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function registerConfig(program) {
|
|
335
|
+
const cmd = program.command("config").description("Manage CLI configuration");
|
|
336
|
+
const setCmd = cmd.command("set").description("Set a config value");
|
|
337
|
+
setCmd.command("api-key <key>").description("Set the Alchemy API key for RPC requests").action((key) => {
|
|
338
|
+
try {
|
|
339
|
+
const cfg = load();
|
|
340
|
+
save({ ...cfg, api_key: key });
|
|
341
|
+
printHuman(`${green("\u2713")} Set api-key
|
|
342
|
+
`, { key: "api-key", status: "set" });
|
|
343
|
+
if (!isJSONMode() && cfg.app?.apiKey && cfg.app.apiKey !== key) {
|
|
344
|
+
console.log(
|
|
345
|
+
` ${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.")}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
exitWithError(err);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
setCmd.command("access-key <key>").description("Set the Alchemy access key for Admin API operations").action(async (key) => {
|
|
353
|
+
try {
|
|
354
|
+
const cfg = load();
|
|
355
|
+
save({ ...cfg, access_key: key });
|
|
356
|
+
printHuman(`${green("\u2713")} Set access-key
|
|
357
|
+
`, { key: "access-key", status: "set" });
|
|
358
|
+
if (isInteractiveAllowed(program)) {
|
|
359
|
+
await selectOrCreateApp(new AdminClient(key));
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
exitWithError(err);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
setCmd.command("webhook-api-key <key>").description("Set the Alchemy webhook API key for Notify operations").action((key) => {
|
|
366
|
+
try {
|
|
367
|
+
const cfg = load();
|
|
368
|
+
save({ ...cfg, webhook_api_key: key });
|
|
369
|
+
printHuman(`${green("\u2713")} Set webhook-api-key
|
|
370
|
+
`, { key: "webhook-api-key", status: "set" });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
exitWithError(err);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
setCmd.command("app [app-id]").description("Select the default app (interactive) or set by ID").action(async (appId) => {
|
|
376
|
+
try {
|
|
377
|
+
const cfg = load();
|
|
378
|
+
const accessKey = program.opts().accessKey || process.env.ALCHEMY_ACCESS_KEY || cfg.access_key;
|
|
379
|
+
if (!accessKey) throw errAccessKeyRequired();
|
|
380
|
+
if (appId) {
|
|
381
|
+
const admin = new AdminClient(accessKey);
|
|
382
|
+
const app = await withSpinner(
|
|
383
|
+
"Fetching app\u2026",
|
|
384
|
+
"App fetched",
|
|
385
|
+
() => admin.getApp(appId)
|
|
386
|
+
);
|
|
387
|
+
const updated = {
|
|
388
|
+
...cfg,
|
|
389
|
+
api_key: app.apiKey,
|
|
390
|
+
app: { id: app.id, name: app.name, apiKey: app.apiKey, webhookApiKey: app.webhookApiKey }
|
|
391
|
+
};
|
|
392
|
+
save(updated);
|
|
393
|
+
printHuman(
|
|
394
|
+
`${green("\u2713")} Default app set to ${app.name} (${app.id})
|
|
395
|
+
`,
|
|
396
|
+
{ app: { id: app.id, name: app.name }, status: "set" }
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!isInteractiveAllowed(program)) {
|
|
401
|
+
exitWithError(
|
|
402
|
+
new Error("Interactive app selection requires an interactive terminal. Use 'config set app <app-id>' or 'alchemy apps list' to find app IDs.")
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
await selectOrCreateApp(new AdminClient(accessKey));
|
|
406
|
+
} catch (err) {
|
|
407
|
+
exitWithError(err);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
setCmd.command("network <network>").description("Set the default network (e.g. eth-mainnet, polygon-mainnet)").action((network) => {
|
|
411
|
+
try {
|
|
412
|
+
const cfg = load();
|
|
413
|
+
save({ ...cfg, network });
|
|
414
|
+
printHuman(`${green("\u2713")} Set network to ${network}
|
|
415
|
+
`, { key: "network", value: network, status: "set" });
|
|
416
|
+
} catch (err) {
|
|
417
|
+
exitWithError(err);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
setCmd.command("verbose <enabled>").description("Set default verbose output (true|false)").action((enabled) => {
|
|
421
|
+
try {
|
|
422
|
+
const normalized = enabled.trim().toLowerCase();
|
|
423
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
424
|
+
throw errInvalidArgs("verbose must be 'true' or 'false'");
|
|
425
|
+
}
|
|
426
|
+
const verbose = normalized === "true";
|
|
427
|
+
const cfg = load();
|
|
428
|
+
save({ ...cfg, verbose });
|
|
429
|
+
printHuman(
|
|
430
|
+
`${green("\u2713")} Set verbose default to ${verbose}
|
|
431
|
+
`,
|
|
432
|
+
{ key: "verbose", value: String(verbose), status: "set" }
|
|
433
|
+
);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
exitWithError(err);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
setCmd.command("wallet-key-file <path>").description("Set the path to a wallet private key file for x402").action((path) => {
|
|
439
|
+
try {
|
|
440
|
+
const cfg = load();
|
|
441
|
+
save({ ...cfg, wallet_key_file: path });
|
|
442
|
+
printHuman(`${green("\u2713")} Set wallet-key-file
|
|
443
|
+
`, { key: "wallet-key-file", status: "set" });
|
|
444
|
+
} catch (err) {
|
|
445
|
+
exitWithError(err);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
setCmd.command("x402 <enabled>").description("Enable or disable x402 wallet-based auth by default (true|false)").action((enabled) => {
|
|
449
|
+
try {
|
|
450
|
+
const normalized = enabled.trim().toLowerCase();
|
|
451
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
452
|
+
throw errInvalidArgs("x402 must be 'true' or 'false'");
|
|
453
|
+
}
|
|
454
|
+
const x402 = normalized === "true";
|
|
455
|
+
const cfg = load();
|
|
456
|
+
save({ ...cfg, x402 });
|
|
457
|
+
printHuman(
|
|
458
|
+
`${green("\u2713")} Set x402 default to ${x402}
|
|
459
|
+
`,
|
|
460
|
+
{ key: "x402", value: String(x402), status: "set" }
|
|
461
|
+
);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
exitWithError(err);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
cmd.command("get <key>").description("Get a config value (api-key, access-key, app, network, verbose, wallet-key-file, x402)").action((key) => {
|
|
467
|
+
const cfg = load();
|
|
468
|
+
let value = get(cfg, key);
|
|
469
|
+
let isDefault = false;
|
|
470
|
+
if (value === void 0) {
|
|
471
|
+
const defaults = {
|
|
472
|
+
network: "eth-mainnet",
|
|
473
|
+
verbose: "false",
|
|
474
|
+
x402: "false"
|
|
475
|
+
};
|
|
476
|
+
const normalizedKey = KEY_MAP[key] ?? key;
|
|
477
|
+
const defaultValue = defaults[normalizedKey] ?? defaults[key];
|
|
478
|
+
if (defaultValue !== void 0) {
|
|
479
|
+
value = defaultValue;
|
|
480
|
+
isDefault = true;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (value === void 0) {
|
|
484
|
+
exitWithError(errNotFound(`config key '${key}'`));
|
|
485
|
+
}
|
|
486
|
+
const isSecret = key === "api-key" || key === "api_key" || key === "access-key" || key === "access_key";
|
|
487
|
+
const display = isSecret ? maskIf(value) : value;
|
|
488
|
+
const humanDisplay = isDefault ? `${display} ${dim("(default)")}` : display;
|
|
489
|
+
printHuman(humanDisplay + "\n", { key, value: display, ...isDefault && { default: true } });
|
|
490
|
+
});
|
|
491
|
+
cmd.command("list").description("List all config values").action(() => {
|
|
492
|
+
const cfg = load();
|
|
493
|
+
const hasApiKeyMismatch = Boolean(
|
|
494
|
+
cfg.api_key && cfg.app?.apiKey && cfg.api_key !== cfg.app.apiKey
|
|
495
|
+
);
|
|
496
|
+
if (isJSONMode()) {
|
|
497
|
+
printJSON(toMap(cfg));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const pairs = [
|
|
501
|
+
[
|
|
502
|
+
"api-key",
|
|
503
|
+
cfg.api_key ? `${hasApiKeyMismatch ? `${yellow("\u25C6")} ` : ""}${maskIf(cfg.api_key)}` : dim("(not set)")
|
|
504
|
+
],
|
|
505
|
+
["access-key", cfg.access_key ? maskIf(cfg.access_key) : dim("(not set)")],
|
|
506
|
+
[
|
|
507
|
+
"app",
|
|
508
|
+
cfg.app ? `${cfg.app.name} ${dim(`(${cfg.app.id})`)}` : dim("(not set) \u2014 set automatically via 'config set access-key' or 'config set app'")
|
|
509
|
+
],
|
|
510
|
+
["network", cfg.network || dim("(not set, defaults to eth-mainnet)")],
|
|
511
|
+
[
|
|
512
|
+
"verbose",
|
|
513
|
+
cfg.verbose !== void 0 ? String(cfg.verbose) : dim("(not set, defaults to false)")
|
|
514
|
+
],
|
|
515
|
+
["wallet-key-file", cfg.wallet_key_file || dim("(not set)")],
|
|
516
|
+
["wallet-address", cfg.wallet_address || dim("(not set)")],
|
|
517
|
+
[
|
|
518
|
+
"x402",
|
|
519
|
+
cfg.x402 !== void 0 ? String(cfg.x402) : dim("(not set, defaults to false)")
|
|
520
|
+
]
|
|
521
|
+
];
|
|
522
|
+
printKeyValueBox(pairs);
|
|
523
|
+
if (hasApiKeyMismatch) {
|
|
524
|
+
console.log("");
|
|
525
|
+
console.log(
|
|
526
|
+
` ${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.")}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
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) => {
|
|
531
|
+
try {
|
|
532
|
+
if (key) {
|
|
533
|
+
const mapped = RESET_KEY_MAP[key];
|
|
534
|
+
if (!mapped) {
|
|
535
|
+
throw errInvalidArgs(
|
|
536
|
+
`invalid reset key '${key}' (valid: api-key, access-key, app, network, verbose, wallet-key-file, x402)`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const cfg = load();
|
|
540
|
+
const updated = { ...cfg };
|
|
541
|
+
delete updated[mapped];
|
|
542
|
+
save(updated);
|
|
543
|
+
printHuman(`${green("\u2713")} Reset ${key}
|
|
544
|
+
`, {
|
|
545
|
+
status: "reset",
|
|
546
|
+
key
|
|
547
|
+
});
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!options.yes && isInteractiveAllowed(program)) {
|
|
551
|
+
const proceed = await promptConfirm({
|
|
552
|
+
message: "Reset all saved config values?",
|
|
553
|
+
initialValue: false,
|
|
554
|
+
cancelMessage: "Cancelled config reset."
|
|
555
|
+
});
|
|
556
|
+
if (proceed === null) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (!proceed) {
|
|
560
|
+
console.log(` ${dim("Skipped config reset.")}`);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
save({});
|
|
565
|
+
printHuman(`${green("\u2713")} Reset all config values
|
|
566
|
+
`, {
|
|
567
|
+
status: "reset",
|
|
568
|
+
scope: "all"
|
|
569
|
+
});
|
|
570
|
+
} catch (err) {
|
|
571
|
+
exitWithError(err);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/commands/wallet.ts
|
|
577
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
578
|
+
import { join, dirname } from "path";
|
|
579
|
+
import { randomUUID } from "crypto";
|
|
580
|
+
import { generateWallet, getWalletAddress } from "@alchemy/x402";
|
|
581
|
+
var WALLET_KEYS_DIR = "wallet-keys";
|
|
582
|
+
var UUID_SLICE_LEN = 8;
|
|
583
|
+
var ADDRESS_SLICE_LEN = 12;
|
|
584
|
+
function walletKeysDirPath() {
|
|
585
|
+
return join(configDir(), WALLET_KEYS_DIR);
|
|
586
|
+
}
|
|
587
|
+
function walletKeyPath(address) {
|
|
588
|
+
const addr = address.trim().toLowerCase().replace(/^0x/, "").replace(/[^a-z0-9]/g, "").slice(0, ADDRESS_SLICE_LEN);
|
|
589
|
+
const addressTag = addr || "unknown";
|
|
590
|
+
const fileName = `wallet-key-${addressTag}-${Date.now()}-${randomUUID().slice(0, UUID_SLICE_LEN)}.txt`;
|
|
591
|
+
return join(walletKeysDirPath(), fileName);
|
|
592
|
+
}
|
|
593
|
+
function persistWalletKey(privateKey, address) {
|
|
594
|
+
const keyPath = walletKeyPath(address);
|
|
595
|
+
mkdirSync(dirname(keyPath), { recursive: true, mode: 493 });
|
|
596
|
+
writeFileSync(keyPath, privateKey + "\n", { mode: 384, flag: "wx" });
|
|
597
|
+
return keyPath;
|
|
598
|
+
}
|
|
599
|
+
function generateAndPersistWallet() {
|
|
600
|
+
const wallet = generateWallet();
|
|
601
|
+
const keyPath = persistWalletKey(wallet.privateKey, wallet.address);
|
|
602
|
+
const cfg = load();
|
|
603
|
+
save({ ...cfg, wallet_key_file: keyPath, wallet_address: wallet.address });
|
|
604
|
+
return { address: wallet.address, keyFile: keyPath };
|
|
605
|
+
}
|
|
606
|
+
function importAndPersistWallet(path) {
|
|
607
|
+
let key;
|
|
608
|
+
try {
|
|
609
|
+
key = readFileSync(path, "utf-8").trim();
|
|
610
|
+
} catch {
|
|
611
|
+
throw errInvalidArgs(`Could not read key file: ${path}`);
|
|
612
|
+
}
|
|
613
|
+
const address = getWalletAddress(key);
|
|
614
|
+
const keyPath = persistWalletKey(key, address);
|
|
615
|
+
const cfg = load();
|
|
616
|
+
save({ ...cfg, wallet_key_file: keyPath, wallet_address: address });
|
|
617
|
+
return { address, keyFile: keyPath };
|
|
618
|
+
}
|
|
619
|
+
function registerWallet(program) {
|
|
620
|
+
const cmd = program.command("wallet").description("Manage x402 wallet");
|
|
621
|
+
cmd.command("generate").description("Generate a new wallet for x402 authentication").action(() => {
|
|
622
|
+
try {
|
|
623
|
+
const wallet = generateAndPersistWallet();
|
|
624
|
+
if (isJSONMode()) {
|
|
625
|
+
printJSON(wallet);
|
|
626
|
+
} else {
|
|
627
|
+
printKeyValueBox([
|
|
628
|
+
["Address", green(wallet.address)],
|
|
629
|
+
["Key file", wallet.keyFile]
|
|
630
|
+
]);
|
|
631
|
+
console.log(` ${green("\u2713")} Wallet generated and saved to config`);
|
|
632
|
+
}
|
|
633
|
+
} catch (err) {
|
|
634
|
+
exitWithError(err);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
cmd.command("import").argument("<path>", "Path to private key file").description("Import a wallet from a private key file").action((path) => {
|
|
638
|
+
try {
|
|
639
|
+
const wallet = importAndPersistWallet(path);
|
|
640
|
+
if (isJSONMode()) {
|
|
641
|
+
printJSON(wallet);
|
|
642
|
+
} else {
|
|
643
|
+
printKeyValueBox([
|
|
644
|
+
["Address", green(wallet.address)],
|
|
645
|
+
["Key file", wallet.keyFile]
|
|
646
|
+
]);
|
|
647
|
+
console.log(` ${green("\u2713")} Wallet imported and saved to config`);
|
|
648
|
+
}
|
|
649
|
+
} catch (err) {
|
|
650
|
+
exitWithError(err);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
cmd.command("address").description("Display the address of the configured wallet").action(() => {
|
|
654
|
+
try {
|
|
655
|
+
const key = resolveWalletKey(program);
|
|
656
|
+
if (!key) throw errWalletKeyRequired();
|
|
657
|
+
const address = getWalletAddress(key);
|
|
658
|
+
printHuman(
|
|
659
|
+
`${address}
|
|
660
|
+
`,
|
|
661
|
+
{ address }
|
|
662
|
+
);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
exitWithError(err);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export {
|
|
670
|
+
splitCommaList,
|
|
671
|
+
readStdinArg,
|
|
672
|
+
readStdinLines,
|
|
673
|
+
validateAddress,
|
|
674
|
+
resolveAddress,
|
|
675
|
+
validateTxHash,
|
|
676
|
+
selectOrCreateApp,
|
|
677
|
+
registerConfig,
|
|
678
|
+
generateAndPersistWallet,
|
|
679
|
+
importAndPersistWallet,
|
|
680
|
+
registerWallet
|
|
681
|
+
};
|