@alchemy/cli 0.3.0 → 0.4.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.
@@ -10,7 +10,6 @@ import {
10
10
  errAccessDenied,
11
11
  errAccessKeyRequired,
12
12
  errAdminAPI,
13
- errAppRequired,
14
13
  errAuthRequired,
15
14
  errInvalidAPIKey,
16
15
  errInvalidAccessKey,
@@ -43,7 +42,7 @@ import {
43
42
  verbose,
44
43
  withSpinner,
45
44
  yellow
46
- } from "./chunk-MF6DXNO7.js";
45
+ } from "./chunk-TH75DFAY.js";
47
46
 
48
47
  // src/lib/client-utils.ts
49
48
  function isLocalhost(hostname) {
@@ -383,6 +382,21 @@ async function readStdinArg(name) {
383
382
  }
384
383
  return data;
385
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
+ }
386
400
  function validateAddress(address) {
387
401
  if (!ADDRESS_RE.test(address)) {
388
402
  throw errInvalidArgs(
@@ -1201,26 +1215,6 @@ function clientFromFlags(program, opts) {
1201
1215
  if (!apiKey) throw errAuthRequired();
1202
1216
  return new Client(apiKey, network);
1203
1217
  }
1204
- function appNetworkToSlug(rpcUrl) {
1205
- let parsed;
1206
- try {
1207
- parsed = new URL(rpcUrl);
1208
- } catch {
1209
- return null;
1210
- }
1211
- const suffix = ".g.alchemy.com";
1212
- if (!parsed.hostname.endsWith(suffix)) return null;
1213
- const slug = parsed.hostname.slice(0, -suffix.length);
1214
- return slug || null;
1215
- }
1216
- async function resolveConfiguredNetworkSlugs(program, appIdOverride) {
1217
- const appId = appIdOverride || resolveAppId(program);
1218
- if (!appId) throw errAppRequired();
1219
- const admin = adminClientFromFlags(program);
1220
- const app = await admin.getApp(appId);
1221
- const slugs = app.chainNetworks.map((network) => appNetworkToSlug(network.rpcUrl)).filter((slug) => Boolean(slug));
1222
- return Array.from(new Set(slugs)).sort((a, b) => a.localeCompare(b));
1223
- }
1224
1218
 
1225
1219
  // src/commands/wallet.ts
1226
1220
  var WALLET_KEYS_DIR = "wallet-keys";
@@ -1316,6 +1310,7 @@ export {
1316
1310
  AdminClient,
1317
1311
  splitCommaList,
1318
1312
  readStdinArg,
1313
+ readStdinLines,
1319
1314
  validateAddress,
1320
1315
  resolveAddress,
1321
1316
  validateTxHash,
@@ -1326,7 +1321,6 @@ export {
1326
1321
  resolveAppId,
1327
1322
  adminClientFromFlags,
1328
1323
  clientFromFlags,
1329
- resolveConfiguredNetworkSlugs,
1330
1324
  generateAndPersistWallet,
1331
1325
  importAndPersistWallet,
1332
1326
  registerWallet
@@ -4,10 +4,13 @@ if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
4
4
  // src/lib/colors.ts
5
5
  var forceColor = "FORCE_COLOR" in process.env && process.env.FORCE_COLOR !== "0";
6
6
  var noColor = !forceColor && ("NO_COLOR" in process.env || process.env.TERM === "dumb");
7
+ function setNoColor(value) {
8
+ noColor = value;
9
+ }
7
10
  var identity = (s) => s;
8
- var esc = (code) => noColor ? identity : (s) => `\x1B[${code}m${s}\x1B[0m`;
9
- var rgb = (r, g, b) => noColor ? identity : (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[39m`;
10
- var bgRgb = (r, g, b) => noColor ? identity : (s) => `\x1B[48;2;${r};${g};${b}m${s}\x1B[49m`;
11
+ var esc = (code) => (s) => noColor ? s : `\x1B[${code}m${s}\x1B[0m`;
12
+ var rgb = (r, g, b) => (s) => noColor ? s : `\x1B[38;2;${r};${g};${b}m${s}\x1B[39m`;
13
+ var bgRgb = (r, g, b) => (s) => noColor ? s : `\x1B[48;2;${r};${g};${b}m${s}\x1B[49m`;
11
14
 
12
15
  // src/lib/redact.ts
13
16
  var SENSITIVE_ERROR_CODES = /* @__PURE__ */ new Set([
@@ -195,7 +198,7 @@ function setFlags(opts) {
195
198
  timeout = opts.timeout;
196
199
  }
197
200
  function isRevealMode() {
198
- return reveal && Boolean(process.stdout.isTTY);
201
+ return reveal;
199
202
  }
200
203
  function isJSONMode() {
201
204
  if (forceJSON) return true;
@@ -475,7 +478,8 @@ function getHome() {
475
478
  }
476
479
  function configPath() {
477
480
  if (process.env.ALCHEMY_CONFIG) return process.env.ALCHEMY_CONFIG;
478
- return join(getHome(), ".config", "alchemy", "config.json");
481
+ const configHome = process.env.XDG_CONFIG_HOME || join(getHome(), ".config");
482
+ return join(configHome, "alchemy", "config.json");
479
483
  }
480
484
  function configDir() {
481
485
  return dirname(configPath());
@@ -1110,7 +1114,7 @@ function semverLT(a, b) {
1110
1114
  return false;
1111
1115
  }
1112
1116
  function currentVersion() {
1113
- return true ? "0.3.0" : "0.0.0";
1117
+ return true ? "0.4.0" : "0.0.0";
1114
1118
  }
1115
1119
  function toUpdateStatus(latestVersion, checkedAt) {
1116
1120
  const current = currentVersion();
@@ -1169,6 +1173,7 @@ ${getUpdateNoticeLines(latest).join("\n")}
1169
1173
 
1170
1174
  export {
1171
1175
  noColor,
1176
+ setNoColor,
1172
1177
  identity,
1173
1178
  esc,
1174
1179
  rgb,
@@ -2,7 +2,7 @@
2
2
  if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
3
3
  import {
4
4
  isInteractiveAllowed
5
- } from "./chunk-MF6DXNO7.js";
5
+ } from "./chunk-TH75DFAY.js";
6
6
 
7
7
  // src/lib/networks.ts
8
8
  var TESTNET_TOKEN_RE = /(testnet|sepolia|holesky|hoodi|devnet|minato|amoy|fuji|saigon|cardona|aeneid|curtis|chiado|cassiopeia|blaze|ropsten|signet|mocha|fam|bepolia)$/i;
package/dist/index.js CHANGED
@@ -5,24 +5,24 @@ import {
5
5
  clientFromFlags,
6
6
  fetchWithTimeout,
7
7
  readStdinArg,
8
+ readStdinLines,
8
9
  registerConfig,
9
10
  registerWallet,
10
11
  resolveAPIKey,
11
12
  resolveAddress,
12
13
  resolveAppId,
13
- resolveConfiguredNetworkSlugs,
14
14
  resolveNetwork,
15
15
  splitCommaList,
16
16
  validateAddress,
17
17
  validateTxHash
18
- } from "./chunk-UPQTWEPP.js";
18
+ } from "./chunk-PIWNNNMZ.js";
19
19
  import {
20
20
  getRPCNetworks,
21
21
  getSetupStatus,
22
22
  isSetupComplete,
23
23
  nativeTokenSymbol,
24
24
  shouldRunOnboarding
25
- } from "./chunk-VYQ5V2ZR.js";
25
+ } from "./chunk-Z3LXQFIY.js";
26
26
  import {
27
27
  EXIT_CODES,
28
28
  ErrorCode,
@@ -32,6 +32,7 @@ import {
32
32
  debug,
33
33
  dim,
34
34
  emptyState,
35
+ errAppRequired,
35
36
  errAuthRequired,
36
37
  errInvalidAPIKey,
37
38
  errInvalidArgs,
@@ -59,16 +60,18 @@ import {
59
60
  printSyntaxJSON,
60
61
  printTable,
61
62
  printUpdateNotice,
63
+ promptConfirm,
62
64
  promptSelect,
63
65
  quiet,
64
66
  red,
65
67
  setFlags,
68
+ setNoColor,
66
69
  successBadge,
67
70
  timeAgo,
68
71
  verbose,
69
72
  weiToEth,
70
73
  withSpinner
71
- } from "./chunk-MF6DXNO7.js";
74
+ } from "./chunk-TH75DFAY.js";
72
75
 
73
76
  // src/index.ts
74
77
  import { Command, Help } from "commander";
@@ -106,6 +109,54 @@ Examples:
106
109
  }
107
110
 
108
111
  // src/commands/balance.ts
112
+ async function fetchBalance(program2, addressInput, blockParam) {
113
+ const client = clientFromFlags(program2);
114
+ const address = await resolveAddress(addressInput, client);
115
+ const result = await withSpinner(
116
+ "Fetching balance\u2026",
117
+ "Balance fetched",
118
+ () => client.call("eth_getBalance", [address, blockParam])
119
+ );
120
+ const wei = BigInt(result);
121
+ const network = resolveNetwork(program2);
122
+ const symbol = nativeTokenSymbol(network);
123
+ if (isJSONMode()) {
124
+ printJSON({
125
+ address,
126
+ wei: wei.toString(),
127
+ balance: weiToEth(wei),
128
+ symbol,
129
+ network
130
+ });
131
+ } else {
132
+ printKeyValueBox([
133
+ ["Address", address],
134
+ ["Balance", green(`${weiToEth(wei)} ${symbol}`)],
135
+ ["Network", network]
136
+ ]);
137
+ if (verbose) {
138
+ console.log("");
139
+ printJSON({
140
+ rpcMethod: "eth_getBalance",
141
+ rpcParams: [address, blockParam],
142
+ rpcResult: result
143
+ });
144
+ }
145
+ }
146
+ }
147
+ function resolveBlockParam(block) {
148
+ let blockParam = block ?? "latest";
149
+ if (blockParam !== "latest" && blockParam !== "earliest" && blockParam !== "pending") {
150
+ if (!blockParam.startsWith("0x")) {
151
+ const num = parseInt(blockParam, 10);
152
+ if (isNaN(num) || num < 0) {
153
+ throw errInvalidArgs("Block must be a number, hex, or tag (latest, earliest, pending).");
154
+ }
155
+ blockParam = `0x${num.toString(16)}`;
156
+ }
157
+ }
158
+ return blockParam;
159
+ }
109
160
  function registerBalance(program2) {
110
161
  program2.command("balance").argument("[address]", "Wallet address (0x...) or ENS name, or pipe via stdin").alias("bal").description("Get the native token balance of an address").addHelpText(
111
162
  "after",
@@ -115,51 +166,21 @@ Examples:
115
166
  alchemy balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 -n polygon-mainnet
116
167
  echo 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 | alchemy balance
117
168
  alchemy balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --block 15537393
118
- alchemy balance vitalik.eth`
169
+ alchemy balance vitalik.eth
170
+ cat addresses.txt | alchemy balance`
119
171
  ).option("--block <block>", "Block number, hex, or tag (default: latest)").action(async (addressArg, opts) => {
120
172
  try {
121
- const addressInput = addressArg ?? await readStdinArg("address");
122
- const client = clientFromFlags(program2);
123
- const address = await resolveAddress(addressInput, client);
124
- let blockParam = opts?.block ?? "latest";
125
- if (blockParam !== "latest" && blockParam !== "earliest" && blockParam !== "pending") {
126
- if (!blockParam.startsWith("0x")) {
127
- const num = parseInt(blockParam, 10);
128
- if (isNaN(num) || num < 0) {
129
- throw errInvalidArgs("Block must be a number, hex, or tag (latest, earliest, pending).");
130
- }
131
- blockParam = `0x${num.toString(16)}`;
132
- }
173
+ const blockParam = resolveBlockParam(opts?.block);
174
+ if (addressArg) {
175
+ await fetchBalance(program2, addressArg, blockParam);
176
+ return;
133
177
  }
134
- const result = await withSpinner(
135
- "Fetching balance\u2026",
136
- "Balance fetched",
137
- () => client.call("eth_getBalance", [address, blockParam])
138
- );
139
- const wei = BigInt(result);
140
- const network = resolveNetwork(program2);
141
- const symbol = nativeTokenSymbol(network);
142
- if (isJSONMode()) {
143
- printJSON({
144
- address,
145
- wei: wei.toString(),
146
- balance: weiToEth(wei),
147
- symbol,
148
- network
149
- });
150
- } else {
151
- printKeyValueBox([
152
- ["Address", address],
153
- ["Balance", green(`${weiToEth(wei)} ${symbol}`)],
154
- ["Network", network]
155
- ]);
156
- if (verbose) {
157
- console.log("");
158
- printJSON({
159
- rpcMethod: "eth_getBalance",
160
- rpcParams: [address, "latest"],
161
- rpcResult: result
162
- });
178
+ const lines = await readStdinLines("address");
179
+ for (const line of lines) {
180
+ try {
181
+ await fetchBalance(program2, line, blockParam);
182
+ } catch (err) {
183
+ exitWithError(err);
163
184
  }
164
185
  }
165
186
  } catch (err) {
@@ -237,29 +258,28 @@ function formatGasSummary(gasUsed, gasLimit, options) {
237
258
 
238
259
  // src/commands/tx.ts
239
260
  function registerTx(program2) {
240
- program2.command("tx").argument("[hash]", "Transaction hash (0x...) or pipe via stdin").description("Get transaction details by hash").addHelpText(
261
+ program2.command("tx").argument("[hash]", "Transaction hash (0x...) or pipe via stdin").description("Get transaction details by hash (use 'receipt' for receipt data)").addHelpText(
241
262
  "after",
242
263
  `
243
264
  Examples:
244
265
  alchemy tx 0xabc123...
245
- echo 0xabc123... | alchemy tx`
266
+ echo 0xabc123... | alchemy tx
267
+
268
+ Tip: use 'alchemy receipt <hash>' to get the transaction receipt (status, gas used, logs).`
246
269
  ).action(async (hashArg) => {
247
270
  try {
248
271
  const hash = hashArg ?? await readStdinArg("hash");
249
272
  validateTxHash(hash);
250
273
  const client = clientFromFlags(program2);
251
- const [tx, receipt] = await withSpinner("Fetching transaction\u2026", "Transaction fetched", async () => {
274
+ const tx = await withSpinner("Fetching transaction\u2026", "Transaction fetched", async () => {
252
275
  const t = await client.call("eth_getTransactionByHash", [
253
276
  hash
254
277
  ]);
255
278
  if (!t) throw errNotFound(`transaction ${hash}`);
256
- const r = await client.call("eth_getTransactionReceipt", [
257
- hash
258
- ]);
259
- return [t, r];
279
+ return t;
260
280
  });
261
281
  if (isJSONMode()) {
262
- printJSON({ transaction: tx, receipt });
282
+ printJSON(tx);
263
283
  return;
264
284
  }
265
285
  const network = resolveNetwork(program2);
@@ -275,20 +295,13 @@ Examples:
275
295
  const formatted = formatHexWithRaw(tx.blockNumber);
276
296
  pairs.push(["Block", formatted ?? String(tx.blockNumber)]);
277
297
  }
278
- if (receipt) {
279
- if (receipt.status === "0x1") {
280
- pairs.push(["Status", `${successBadge()} Success`]);
281
- } else if (receipt.status) {
282
- pairs.push(["Status", `${failBadge()} Failed`]);
283
- }
284
- if (receipt.gasUsed) {
285
- const formatted = formatHexWithRaw(receipt.gasUsed);
286
- pairs.push(["Gas Used", formatted ?? String(receipt.gasUsed)]);
287
- }
288
- if (receipt.effectiveGasPrice) {
289
- const formatted = formatGweiWithRaw(receipt.effectiveGasPrice);
290
- pairs.push(["Gas Price", formatted ?? String(receipt.effectiveGasPrice)]);
291
- }
298
+ if (tx.nonce) {
299
+ const formatted = formatHexWithRaw(tx.nonce);
300
+ pairs.push(["Nonce", formatted ?? String(tx.nonce)]);
301
+ }
302
+ if (tx.gasPrice) {
303
+ const formatted = formatGweiWithRaw(tx.gasPrice);
304
+ pairs.push(["Gas Price", formatted ?? String(tx.gasPrice)]);
292
305
  }
293
306
  const explorerURL = etherscanTxURL(hash, network);
294
307
  if (explorerURL) {
@@ -297,7 +310,7 @@ Examples:
297
310
  printKeyValueBox(pairs);
298
311
  if (verbose) {
299
312
  console.log("");
300
- printJSON({ transaction: tx, receipt });
313
+ printJSON(tx);
301
314
  }
302
315
  } catch (err) {
303
316
  exitWithError(err);
@@ -312,7 +325,9 @@ function registerReceipt(program2) {
312
325
  `
313
326
  Examples:
314
327
  alchemy receipt 0xabc123...
315
- echo 0xabc123... | alchemy receipt`
328
+ echo 0xabc123... | alchemy receipt
329
+
330
+ Tip: use 'alchemy tx <hash>' for transaction details (value, block, nonce). Receipt provides execution results (status, gas used, logs).`
316
331
  ).action(async (hashArg) => {
317
332
  try {
318
333
  const hash = hashArg ?? await readStdinArg("hash");
@@ -596,11 +611,13 @@ async function promptTokensPagination() {
596
611
  if (action === null) return "stop";
597
612
  return action;
598
613
  }
599
- function formatTokenRows(balances) {
600
- const nonZero = balances.filter(
614
+ function filterNonZero(balances) {
615
+ return balances.filter(
601
616
  (tb) => tb.tokenBalance !== "0x0" && tb.tokenBalance !== "0x0000000000000000000000000000000000000000000000000000000000000000"
602
617
  );
603
- return nonZero.map((tb) => {
618
+ }
619
+ function formatTokenRows(balances) {
620
+ return filterNonZero(balances).map((tb) => {
604
621
  let decimalBalance = dim("unparseable");
605
622
  try {
606
623
  decimalBalance = BigInt(tb.tokenBalance).toString();
@@ -609,15 +626,72 @@ function formatTokenRows(balances) {
609
626
  return [tb.contractAddress, decimalBalance, tb.tokenBalance];
610
627
  });
611
628
  }
629
+ function formatWithDecimals(rawBalance, decimals) {
630
+ if (decimals === null || decimals === 0) {
631
+ try {
632
+ return BigInt(rawBalance).toString();
633
+ } catch {
634
+ return rawBalance;
635
+ }
636
+ }
637
+ try {
638
+ const raw = BigInt(rawBalance);
639
+ const divisor = 10n ** BigInt(decimals);
640
+ const whole = raw / divisor;
641
+ const remainder = raw % divisor;
642
+ if (remainder === 0n) return whole.toString();
643
+ const fracStr = remainder.toString().padStart(decimals, "0").replace(/0+$/, "");
644
+ return `${whole}.${fracStr}`;
645
+ } catch {
646
+ return rawBalance;
647
+ }
648
+ }
649
+ async function resolveMetadata(client, balances) {
650
+ const nonZero = filterNonZero(balances);
651
+ const results = await Promise.all(
652
+ nonZero.map(async (tb) => {
653
+ try {
654
+ const meta = await client.call("alchemy_getTokenMetadata", [tb.contractAddress]);
655
+ return [tb.contractAddress, meta];
656
+ } catch {
657
+ return [tb.contractAddress, { name: null, symbol: null, decimals: null, logo: null }];
658
+ }
659
+ })
660
+ );
661
+ return new Map(results);
662
+ }
663
+ function formatResolvedRows(balances, metadata) {
664
+ return filterNonZero(balances).map((tb) => {
665
+ const meta = metadata.get(tb.contractAddress);
666
+ const symbol = meta?.symbol ?? "???";
667
+ const formatted = formatWithDecimals(tb.tokenBalance, meta?.decimals ?? null);
668
+ return [tb.contractAddress, symbol, `${formatted} ${symbol}`];
669
+ });
670
+ }
671
+ function formatResolvedJSON(balances, metadata) {
672
+ return filterNonZero(balances).map((tb) => {
673
+ const meta = metadata.get(tb.contractAddress);
674
+ return {
675
+ contractAddress: tb.contractAddress,
676
+ tokenBalance: tb.tokenBalance,
677
+ ...meta?.symbol && { symbol: meta.symbol },
678
+ ...meta?.name && { name: meta.name },
679
+ ...meta?.decimals !== null && meta?.decimals !== void 0 && { decimals: meta.decimals },
680
+ ...meta?.decimals !== null && meta?.decimals !== void 0 && {
681
+ formattedBalance: formatWithDecimals(tb.tokenBalance, meta.decimals)
682
+ }
683
+ };
684
+ });
685
+ }
612
686
  function registerTokens(program2) {
613
- const cmd = program2.command("tokens").description("Token API wrappers").argument("[address]", "Wallet address or ENS name (default action: list balances)").option("--page-key <key>", "Pagination key from a previous response").addHelpText(
687
+ const cmd = program2.command("tokens").description("Token API wrappers");
688
+ cmd.command("balances").argument("[address]", "Wallet address or ENS name, or pipe via stdin").description("Get ERC-20 token balances for an address").option("--page-key <key>", "Pagination key from a previous response").option("--metadata", "Fetch token metadata (symbol, decimals) and show formatted balances").addHelpText(
614
689
  "after",
615
690
  `
616
691
  Examples:
617
- alchemy tokens 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
618
- alchemy tokens metadata 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eB48
619
- alchemy tokens allowance --owner 0x... --spender 0x... --contract 0x...
620
- echo 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 | alchemy tokens`
692
+ alchemy tokens balances 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
693
+ alchemy tokens balances 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --metadata
694
+ echo 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 | alchemy tokens balances`
621
695
  ).action(async (addressArg, opts) => {
622
696
  try {
623
697
  const addressInput = addressArg ?? await readStdinArg("address");
@@ -632,24 +706,50 @@ Examples:
632
706
  "Token balances fetched",
633
707
  () => client.call("alchemy_getTokenBalances", params)
634
708
  );
635
- if (isJSONMode()) {
636
- printJSON(result);
709
+ const nonZero = filterNonZero(result.tokenBalances);
710
+ if (nonZero.length === 0) {
711
+ if (isJSONMode()) {
712
+ printJSON(result);
713
+ } else {
714
+ emptyState("No token balances found.");
715
+ }
637
716
  return;
638
717
  }
639
- const rows = formatTokenRows(result.tokenBalances);
640
- if (rows.length === 0) {
641
- emptyState("No token balances found.");
718
+ const metadata = opts.metadata ? await withSpinner(
719
+ `Resolving metadata for ${nonZero.length} tokens\u2026`,
720
+ "Metadata resolved",
721
+ () => resolveMetadata(client, result.tokenBalances)
722
+ ) : null;
723
+ if (isJSONMode()) {
724
+ if (metadata) {
725
+ printJSON({
726
+ address: result.address,
727
+ tokenBalances: formatResolvedJSON(result.tokenBalances, metadata),
728
+ ...result.pageKey && { pageKey: result.pageKey }
729
+ });
730
+ } else {
731
+ printJSON(result);
732
+ }
642
733
  return;
643
734
  }
644
- let totalShown = rows.length;
735
+ let totalShown = nonZero.length;
645
736
  printKeyValueBox([
646
737
  ["Address", address],
647
738
  ["Network", client.network],
648
739
  ["Tokens", String(totalShown)]
649
740
  ]);
650
- printTable(["Contract", "Balance (base units)", "Raw (hex)"], rows);
741
+ if (metadata) {
742
+ const rows = formatResolvedRows(result.tokenBalances, metadata);
743
+ printTable(["Contract", "Symbol", "Balance"], rows);
744
+ } else {
745
+ const rows = formatTokenRows(result.tokenBalances);
746
+ printTable(["Contract", "Balance (base units)", "Raw (hex)"], rows);
747
+ }
651
748
  console.log(`
652
749
  ${dim(`${totalShown} tokens (zero balances hidden).`)}`);
750
+ if (!metadata) {
751
+ console.log(` ${dim("Tip: use --metadata to fetch token symbols, decimals, and show formatted balances.")}`);
752
+ }
653
753
  if (verbose) {
654
754
  console.log("");
655
755
  printJSON(result);
@@ -672,10 +772,21 @@ Examples:
672
772
  printJSON(nextResult);
673
773
  return;
674
774
  }
675
- const nextRows = formatTokenRows(nextResult.tokenBalances);
676
- totalShown += nextRows.length;
677
- if (nextRows.length > 0) {
678
- printTable(["Contract", "Balance (base units)", "Raw (hex)"], nextRows);
775
+ const nextNonZero = filterNonZero(nextResult.tokenBalances);
776
+ totalShown += nextNonZero.length;
777
+ if (nextNonZero.length > 0) {
778
+ if (metadata) {
779
+ const nextMeta = await withSpinner(
780
+ `Resolving metadata for ${nextNonZero.length} tokens\u2026`,
781
+ "Metadata resolved",
782
+ () => resolveMetadata(client, nextResult.tokenBalances)
783
+ );
784
+ const rows = formatResolvedRows(nextResult.tokenBalances, nextMeta);
785
+ printTable(["Contract", "Symbol", "Balance"], rows);
786
+ } else {
787
+ const rows = formatTokenRows(nextResult.tokenBalances);
788
+ printTable(["Contract", "Balance (base units)", "Raw (hex)"], rows);
789
+ }
679
790
  }
680
791
  console.log(`
681
792
  ${dim(`${totalShown} tokens total (zero balances hidden).`)}`);
@@ -689,7 +800,12 @@ Examples:
689
800
  exitWithError(err);
690
801
  }
691
802
  });
692
- cmd.command("metadata <contract>").description("Get ERC-20 token metadata").action(async (contract) => {
803
+ cmd.command("metadata <contract>").description("Get ERC-20 token metadata (name, symbol, decimals, logo)").addHelpText(
804
+ "after",
805
+ `
806
+ Examples:
807
+ alchemy tokens metadata 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eB48`
808
+ ).action(async (contract) => {
693
809
  try {
694
810
  validateAddress(contract);
695
811
  const client = clientFromFlags(program2);
@@ -730,34 +846,22 @@ Examples:
730
846
  // src/commands/network.ts
731
847
  function registerNetwork(program2) {
732
848
  const cmd = program2.command("network").description("Manage networks");
733
- cmd.command("list").description("List RPC network IDs for use with --network (e.g. eth-mainnet)").option(
734
- "--configured",
735
- "List only configured app RPC networks (requires access key and app context)"
736
- ).option(
737
- "--app-id <id>",
738
- "App ID for configured network lookups (overrides saved app)"
739
- ).action(async (opts) => {
849
+ cmd.command("list").description("List RPC network IDs for use with --network (e.g. eth-mainnet)").option("--mainnet-only", "Show only mainnet networks").option("--testnet-only", "Show only testnet networks").option("--search <term>", "Filter networks by name or ID").action(async (opts) => {
740
850
  try {
741
- const supported = getRPCNetworks();
851
+ let display = getRPCNetworks();
742
852
  const current = resolveNetwork(program2);
743
- const configured = opts.configured ? await withSpinner(
744
- "Fetching configured networks\u2026",
745
- "Configured networks fetched",
746
- () => resolveConfiguredNetworkSlugs(program2, opts.appId)
747
- ) : null;
748
- const configuredSet = new Set(configured ?? []);
749
- const appId = opts.configured ? opts.appId || resolveAppId(program2) : void 0;
750
- const display = configured ? supported.filter((network) => configuredSet.has(network.id)) : supported;
853
+ if (opts.mainnetOnly) {
854
+ display = display.filter((n) => !n.isTestnet);
855
+ } else if (opts.testnetOnly) {
856
+ display = display.filter((n) => n.isTestnet);
857
+ }
858
+ if (opts.search) {
859
+ const term = opts.search.toLowerCase();
860
+ display = display.filter(
861
+ (n) => n.id.toLowerCase().includes(term) || n.name.toLowerCase().includes(term) || n.family.toLowerCase().includes(term)
862
+ );
863
+ }
751
864
  if (isJSONMode()) {
752
- if (configured) {
753
- printJSON({
754
- mode: "configured",
755
- appId,
756
- configuredNetworkIds: configured,
757
- networks: display
758
- });
759
- return;
760
- }
761
865
  printJSON(display);
762
866
  return;
763
867
  }
@@ -769,23 +873,18 @@ function registerNetwork(program2) {
769
873
  return [idCell, nameCell, network.family, testnetCell];
770
874
  });
771
875
  printTable(["Network ID", "Name", "Family", "Testnet"], rows);
772
- if (configured) {
773
- console.log(
774
- `
775
- ${dim(`Configured networks for app ${appId}: ${display.length}`)}`
776
- );
777
- }
778
876
  console.log(`
779
877
  Current: ${green(current)}`);
780
878
  console.log(
781
879
  ` ${dim("Need Admin API chain identifiers (e.g. ETH_MAINNET)? See: apps chains")}`
782
880
  );
881
+ console.log(
882
+ ` ${dim("Need configured networks for an app? See: apps networks")}`
883
+ );
783
884
  if (verbose) {
784
885
  console.log("");
785
886
  printJSON({
786
- mode: configured ? "configured" : "all",
787
- appId: appId ?? null,
788
- configuredNetworkIds: configured ?? null,
887
+ mode: "all",
789
888
  networks: display,
790
889
  currentNetwork: current
791
890
  });
@@ -1037,9 +1136,21 @@ function registerApps(program2) {
1037
1136
  exitWithError(err);
1038
1137
  }
1039
1138
  });
1040
- cmd.command("delete <id>").description("Delete an app").option("--dry-run", "Preview without executing").action(async (id, opts) => {
1139
+ cmd.command("delete <id>").description("Delete an app").option("--dry-run", "Preview without executing").option("-y, --yes", "Skip confirmation prompt").action(async (id, opts) => {
1041
1140
  try {
1042
1141
  if (handleDryRun(opts, "delete", { id }, `Would delete app ${id}`)) return;
1142
+ if (!opts.yes && isInteractiveAllowed(program2)) {
1143
+ const proceed = await promptConfirm({
1144
+ message: `Delete app ${id}?`,
1145
+ initialValue: false,
1146
+ cancelMessage: "Cancelled app deletion."
1147
+ });
1148
+ if (proceed === null) return;
1149
+ if (!proceed) {
1150
+ console.log(` ${dim("Skipped app deletion.")}`);
1151
+ return;
1152
+ }
1153
+ }
1043
1154
  const admin = adminClientFromFlags(program2);
1044
1155
  await withSpinner(
1045
1156
  "Deleting app\u2026",
@@ -1168,6 +1279,37 @@ function registerApps(program2) {
1168
1279
  exitWithError(err);
1169
1280
  }
1170
1281
  });
1282
+ cmd.command("configured-networks").description("List RPC network slugs configured for an app").option("--app-id <id>", "App ID (overrides saved app)").action(async (opts) => {
1283
+ try {
1284
+ const admin = adminClientFromFlags(program2);
1285
+ const appId = opts.appId || resolveAppId(program2);
1286
+ if (!appId) throw errAppRequired();
1287
+ const app = await withSpinner(
1288
+ "Fetching app\u2026",
1289
+ "App fetched",
1290
+ () => admin.getApp(appId)
1291
+ );
1292
+ const slugs = app.chainNetworks.map((n) => {
1293
+ const match = n.rpcUrl?.match(/^https:\/\/([^.]+)\.g\.alchemy\.com(?:\/|$)/);
1294
+ return match ? match[1] : null;
1295
+ }).filter((s) => Boolean(s));
1296
+ const uniqueSlugs = Array.from(new Set(slugs)).sort();
1297
+ if (isJSONMode()) {
1298
+ printJSON({ appId: app.id, appName: app.name, networks: uniqueSlugs });
1299
+ return;
1300
+ }
1301
+ if (uniqueSlugs.length === 0) {
1302
+ emptyState(`No RPC networks configured for ${app.name}.`);
1303
+ return;
1304
+ }
1305
+ const rows = uniqueSlugs.map((slug) => [slug]);
1306
+ printTable(["Network ID"], rows);
1307
+ console.log(`
1308
+ ${dim(`${uniqueSlugs.length} networks configured for ${app.name} (${app.id})`)}`);
1309
+ } catch (err) {
1310
+ exitWithError(err);
1311
+ }
1312
+ });
1171
1313
  cmd.command("chains").description("List Admin API chain identifiers for app configuration (e.g. ETH_MAINNET)").action(async () => {
1172
1314
  try {
1173
1315
  const admin = adminClientFromFlags(program2);
@@ -1520,7 +1662,13 @@ function registerPrices(program2) {
1520
1662
  exitWithError(err);
1521
1663
  }
1522
1664
  });
1523
- cmd.command("historical").description("Get historical prices").requiredOption("--body <json>", "JSON request payload").action(async (opts) => {
1665
+ cmd.command("historical").description("Get historical prices").requiredOption("--body <json>", "JSON request payload").addHelpText(
1666
+ "after",
1667
+ `
1668
+ Examples:
1669
+ alchemy prices historical --body '{"symbol":"ETH","startTime":"2024-01-01T00:00:00Z","endTime":"2024-01-02T00:00:00Z","interval":"1h"}'
1670
+ alchemy prices historical --body '{"address":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","network":"eth-mainnet","startTime":"2024-06-01","endTime":"2024-06-07","interval":"1d"}'`
1671
+ ).action(async (opts) => {
1524
1672
  try {
1525
1673
  const apiKey = resolveAPIKey(program2);
1526
1674
  const payload = JSON.parse(opts.body);
@@ -1607,21 +1755,6 @@ function registerPortfolio(program2) {
1607
1755
  exitWithError(err);
1608
1756
  }
1609
1757
  });
1610
- cmd.command("transactions").description("Get transaction history by address/network pairs").requiredOption("--body <json>", "JSON body for /transactions/history/by-address").action(async (opts) => {
1611
- try {
1612
- const apiKey = resolveAPIKey(program2);
1613
- const result = await runDataCall(
1614
- apiKey,
1615
- "transaction history",
1616
- "/transactions/history/by-address",
1617
- JSON.parse(opts.body)
1618
- );
1619
- if (isJSONMode()) printJSON(result);
1620
- else printSyntaxJSON(result);
1621
- } catch (err) {
1622
- exitWithError(err);
1623
- }
1624
- });
1625
1758
  }
1626
1759
 
1627
1760
  // src/commands/simulate.ts
@@ -1700,15 +1833,24 @@ function registerWebhooks(program2) {
1700
1833
  exitWithError(err);
1701
1834
  }
1702
1835
  });
1703
- cmd.command("create").description("Create webhook").requiredOption("--body <json>", "Create webhook JSON payload").action(async (opts) => {
1836
+ cmd.command("create").description("Create webhook").requiredOption("--body <json>", "Create webhook JSON payload").option("--dry-run", "Preview without executing").action(async (opts) => {
1704
1837
  try {
1838
+ const payload = parseRequiredJSON(opts.body, "--body");
1839
+ if (opts.dryRun) {
1840
+ if (isJSONMode()) printJSON({ dryRun: true, action: "create-webhook", payload });
1841
+ else {
1842
+ console.log(` ${dim("Dry run:")} Would create webhook`);
1843
+ printSyntaxJSON(payload);
1844
+ }
1845
+ return;
1846
+ }
1705
1847
  const token = resolveWebhookApiKey(cmd.opts());
1706
1848
  const result = await withSpinner(
1707
1849
  "Creating webhook\u2026",
1708
1850
  "Webhook created",
1709
1851
  () => callNotify(token, "/create-webhook", {
1710
1852
  method: "POST",
1711
- body: parseRequiredJSON(opts.body, "--body")
1853
+ body: payload
1712
1854
  })
1713
1855
  );
1714
1856
  if (isJSONMode()) printJSON(result);
@@ -1717,15 +1859,24 @@ function registerWebhooks(program2) {
1717
1859
  exitWithError(err);
1718
1860
  }
1719
1861
  });
1720
- cmd.command("update").description("Update webhook").requiredOption("--body <json>", "Update webhook JSON payload").action(async (opts) => {
1862
+ cmd.command("update").description("Update webhook").requiredOption("--body <json>", "Update webhook JSON payload").option("--dry-run", "Preview without executing").action(async (opts) => {
1721
1863
  try {
1864
+ const payload = parseRequiredJSON(opts.body, "--body");
1865
+ if (opts.dryRun) {
1866
+ if (isJSONMode()) printJSON({ dryRun: true, action: "update-webhook", payload });
1867
+ else {
1868
+ console.log(` ${dim("Dry run:")} Would update webhook`);
1869
+ printSyntaxJSON(payload);
1870
+ }
1871
+ return;
1872
+ }
1722
1873
  const token = resolveWebhookApiKey(cmd.opts());
1723
1874
  const result = await withSpinner(
1724
1875
  "Updating webhook\u2026",
1725
1876
  "Webhook updated",
1726
1877
  () => callNotify(token, "/update-webhook", {
1727
1878
  method: "PUT",
1728
- body: parseRequiredJSON(opts.body, "--body")
1879
+ body: payload
1729
1880
  })
1730
1881
  );
1731
1882
  if (isJSONMode()) printJSON(result);
@@ -1734,8 +1885,25 @@ function registerWebhooks(program2) {
1734
1885
  exitWithError(err);
1735
1886
  }
1736
1887
  });
1737
- cmd.command("delete <webhookId>").description("Delete webhook").action(async (webhookId) => {
1888
+ cmd.command("delete <webhookId>").description("Delete webhook").option("--dry-run", "Preview without executing").option("-y, --yes", "Skip confirmation prompt").action(async (webhookId, opts) => {
1738
1889
  try {
1890
+ if (opts.dryRun) {
1891
+ if (isJSONMode()) printJSON({ dryRun: true, action: "delete-webhook", payload: { webhookId } });
1892
+ else console.log(` ${dim("Dry run:")} Would delete webhook ${webhookId}`);
1893
+ return;
1894
+ }
1895
+ if (!opts.yes && isInteractiveAllowed(program2)) {
1896
+ const proceed = await promptConfirm({
1897
+ message: `Delete webhook ${webhookId}?`,
1898
+ initialValue: false,
1899
+ cancelMessage: "Cancelled webhook deletion."
1900
+ });
1901
+ if (proceed === null) return;
1902
+ if (!proceed) {
1903
+ console.log(` ${dim("Skipped webhook deletion.")}`);
1904
+ return;
1905
+ }
1906
+ }
1739
1907
  const token = resolveWebhookApiKey(cmd.opts());
1740
1908
  const result = await withSpinner(
1741
1909
  "Deleting webhook\u2026",
@@ -2387,8 +2555,12 @@ function formatAsSystemPrompt(payload) {
2387
2555
  return lines.join("\n");
2388
2556
  }
2389
2557
  function registerAgentPrompt(program2) {
2390
- program2.command("agent-prompt").description("Emit complete agent/automation usage instructions").action(() => {
2558
+ program2.command("agent-prompt").description("Emit complete agent/automation usage instructions").option("--commands <list>", "Filter to specific commands in JSON output (requires --json). Comma-separated (e.g. balance,tokens,gas)").action((opts) => {
2391
2559
  const payload = buildAgentPrompt(program2);
2560
+ if (opts.commands) {
2561
+ const filter = new Set(opts.commands.split(",").map((s) => s.trim().toLowerCase()));
2562
+ payload.commands = payload.commands.filter((cmd) => filter.has(cmd.name.toLowerCase()));
2563
+ }
2392
2564
  printHuman(formatAsSystemPrompt(payload), payload);
2393
2565
  });
2394
2566
  }
@@ -2500,10 +2672,10 @@ function resetUpdateNoticeState() {
2500
2672
  }
2501
2673
  program.name("alchemy").description(
2502
2674
  "The Alchemy CLI lets you query blockchain data, call JSON-RPC methods, and manage your Alchemy configuration."
2503
- ).version("0.3.0", "-v, --version", "display CLI version").option("--api-key <key>", "Alchemy API key (env: ALCHEMY_API_KEY)").option("--access-key <key>", "Alchemy access key (env: ALCHEMY_ACCESS_KEY)").option(
2675
+ ).version("0.4.0", "-v, --version", "display CLI version").option("--api-key <key>", "Alchemy API key (env: ALCHEMY_API_KEY)").option("--access-key <key>", "Alchemy access key (env: ALCHEMY_ACCESS_KEY)").option(
2504
2676
  "-n, --network <network>",
2505
2677
  "Target network (default: eth-mainnet) (env: ALCHEMY_NETWORK)"
2506
- ).option("--x402", "Use x402 wallet-based gateway auth").option("--wallet-key-file <path>", "Path to wallet private key file for x402").option("--json", "Force JSON output (auto-enabled when piped)").option("-q, --quiet", "Suppress non-essential output").option("--verbose", "Enable verbose output").option("--no-color", "Disable color output").option("--reveal", "Show secrets in plain text (TTY only)").option("--timeout <ms>", "Request timeout in milliseconds", parseInt).option("--debug", "Enable debug diagnostics").option("--no-interactive", "Disable REPL and prompt-driven interactions").addHelpCommand(false).allowExcessArguments(true).exitOverride((err) => {
2678
+ ).option("--x402", "Use x402 wallet-based gateway auth").option("--wallet-key-file <path>", "Path to wallet private key file for x402").option("--json", "Force JSON output (auto-enabled when piped)").option("-q, --quiet", "Suppress non-essential output").option("--verbose", "Enable verbose output").option("--no-color", "Disable color output").option("--reveal", "Show secrets in plain text").option("--timeout <ms>", "Request timeout in milliseconds (default: none)", parseInt).option("--debug", "Enable debug diagnostics").option("--no-interactive", "Disable REPL and prompt-driven interactions").addHelpCommand(false).allowExcessArguments(true).exitOverride((err) => {
2507
2679
  if (err.code === "commander.help" || err.code === "commander.helpDisplayed" || err.code === "commander.version") {
2508
2680
  process.exit(0);
2509
2681
  }
@@ -2645,6 +2817,7 @@ ${styledLine}`;
2645
2817
  ].join("\n");
2646
2818
  }).hook("preAction", () => {
2647
2819
  const opts = program.opts();
2820
+ if (opts.color === false) setNoColor(true);
2648
2821
  const cfg = load();
2649
2822
  setFlags({
2650
2823
  json: opts.json,
@@ -2679,7 +2852,7 @@ ${styledLine}`;
2679
2852
  if (isInteractiveAllowed(program)) {
2680
2853
  let latestForInteractiveStartup = null;
2681
2854
  if (shouldRunOnboarding(program, cfg)) {
2682
- const { runOnboarding } = await import("./onboarding-MUJF5QIE.js");
2855
+ const { runOnboarding } = await import("./onboarding-WQ2TWDM3.js");
2683
2856
  const latest = getAvailableUpdateOnce();
2684
2857
  const completed = await runOnboarding(program, latest);
2685
2858
  updateShownDuringInteractiveStartup = Boolean(latest);
@@ -2691,7 +2864,7 @@ ${styledLine}`;
2691
2864
  latestForInteractiveStartup = getAvailableUpdateOnce();
2692
2865
  updateShownDuringInteractiveStartup = Boolean(latestForInteractiveStartup);
2693
2866
  }
2694
- const { startREPL } = await import("./interactive-BFAXB5SN.js");
2867
+ const { startREPL } = await import("./interactive-NASSNJHQ.js");
2695
2868
  program.exitOverride();
2696
2869
  program.configureOutput({
2697
2870
  writeErr: () => {
@@ -3,7 +3,7 @@ if(process.argv.includes("--no-color"))process.env.NO_COLOR="1";
3
3
  import {
4
4
  getRPCNetworkIds,
5
5
  getSetupMethod
6
- } from "./chunk-VYQ5V2ZR.js";
6
+ } from "./chunk-Z3LXQFIY.js";
7
7
  import {
8
8
  bgRgb,
9
9
  bold,
@@ -19,7 +19,7 @@ import {
19
19
  rgb,
20
20
  setBrandedHelpSuppressed,
21
21
  setReplMode
22
- } from "./chunk-MF6DXNO7.js";
22
+ } from "./chunk-TH75DFAY.js";
23
23
 
24
24
  // src/commands/interactive.ts
25
25
  import * as readline from "readline";
@@ -5,7 +5,7 @@ import {
5
5
  generateAndPersistWallet,
6
6
  importAndPersistWallet,
7
7
  selectOrCreateApp
8
- } from "./chunk-UPQTWEPP.js";
8
+ } from "./chunk-PIWNNNMZ.js";
9
9
  import {
10
10
  bold,
11
11
  brand,
@@ -19,7 +19,7 @@ import {
19
19
  promptSelect,
20
20
  promptText,
21
21
  save
22
- } from "./chunk-MF6DXNO7.js";
22
+ } from "./chunk-TH75DFAY.js";
23
23
 
24
24
  // src/commands/onboarding.ts
25
25
  function printNextSteps(method) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alchemy/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Alchemy CLI — interact with blockchain data",
5
5
  "type": "module",
6
6
  "bin": {