Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cli-usability-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@alchemy/cli": minor
---

Breaking: `tokens <address>` is now `tokens balances <address>`, `tx` no longer includes receipt data (use `receipt` separately), `network list --configured` moved to `apps configured-networks`, and `portfolio transactions` removed (use `transfers`).

New features: `tokens balances --metadata` resolves token symbols and decimals, `network list` supports `--mainnet-only`/`--testnet-only`/`--search`, `webhooks create/update/delete` support `--dry-run`, `agent-prompt --commands` filters JSON output, and `balance` accepts multiple addresses via stdin.
7 changes: 6 additions & 1 deletion src/commands/agent-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,13 @@ export function registerAgentPrompt(program: Command) {
program
.command("agent-prompt")
.description("Emit complete agent/automation usage instructions")
.action(() => {
.option("--commands <list>", "Filter to specific commands in JSON output (requires --json). Comma-separated (e.g. balance,tokens,gas)")
.action((opts: { commands?: string }) => {
const payload = buildAgentPrompt(program);
if (opts.commands) {
const filter = new Set(opts.commands.split(",").map((s) => s.trim().toLowerCase()));
payload.commands = payload.commands.filter((cmd) => filter.has(cmd.name.toLowerCase()));
}
printHuman(formatAsSystemPrompt(payload), payload);
});
}
49 changes: 47 additions & 2 deletions src/commands/apps.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command } from "commander";
import { adminClientFromFlags } from "../lib/resolve.js";
import { adminClientFromFlags, resolveAppId } from "../lib/resolve.js";
import type { App } from "../lib/admin-client.js";
import { errInvalidArgs, exitWithError } from "../lib/errors.js";
import { errInvalidArgs, errAppRequired, exitWithError } from "../lib/errors.js";
import { verbose, isJSONMode, printJSON } from "../lib/output.js";
import { promptSelect, promptConfirm } from "../lib/terminal-ui.js";
import {
Expand Down Expand Up @@ -526,6 +526,51 @@ export function registerApps(program: Command) {
}
});

// ── apps configured-networks ─────────────────────────────────────

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: { appId?: string }) => {
try {
const admin = adminClientFromFlags(program);
const appId = opts.appId || resolveAppId(program);
if (!appId) throw errAppRequired();

const app = await withSpinner("Fetching app…", "App fetched", () =>
admin.getApp(appId),
);

// Extract RPC network slugs from chain network URLs
const slugs = app.chainNetworks
.map((n) => {
const match = n.rpcUrl?.match(/^https:\/\/([^.]+)\.g\.alchemy\.com(?:\/|$)/);
return match ? match[1] : null;
})
.filter((s): s is string => Boolean(s));
const uniqueSlugs = Array.from(new Set(slugs)).sort();

if (isJSONMode()) {
printJSON({ appId: app.id, appName: app.name, networks: uniqueSlugs });
return;
}

if (uniqueSlugs.length === 0) {
emptyState(`No RPC networks configured for ${app.name}.`);
return;
}

const rows = uniqueSlugs.map((slug) => [slug]);
printTable(["Network ID"], rows);
console.log(`\n ${dim(`${uniqueSlugs.length} networks configured for ${app.name} (${app.id})`)}`);
} catch (err) {
exitWithError(err);
}
});

// ── apps chains ─────────────────────────────────────────────────

cmd
.command("chains")
.description("List Admin API chain identifiers for app configuration (e.g. ETH_MAINNET)")
Expand Down
113 changes: 70 additions & 43 deletions src/commands/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,65 @@ import { clientFromFlags, resolveNetwork } from "../lib/resolve.js";
import { verbose, isJSONMode, printJSON } from "../lib/output.js";
import { errInvalidArgs, exitWithError } from "../lib/errors.js";
import { green, withSpinner, weiToEth, printKeyValueBox } from "../lib/ui.js";
import { resolveAddress, readStdinArg } from "../lib/validators.js";
import { resolveAddress, readStdinLines } from "../lib/validators.js";
import { nativeTokenSymbol } from "../lib/networks.js";

async function fetchBalance(
program: Command,
addressInput: string,
blockParam: string,
): Promise<void> {
const client = clientFromFlags(program);
const address = await resolveAddress(addressInput, client);

const result = await withSpinner("Fetching balance…", "Balance fetched", () =>
client.call("eth_getBalance", [address, blockParam]),
) as string;

const wei = BigInt(result);
const network = resolveNetwork(program);
const symbol = nativeTokenSymbol(network);

if (isJSONMode()) {
printJSON({
address,
wei: wei.toString(),
balance: weiToEth(wei),
symbol,
network,
});
} else {
printKeyValueBox([
["Address", address],
["Balance", green(`${weiToEth(wei)} ${symbol}`)],
["Network", network],
]);

if (verbose) {
console.log("");
printJSON({
rpcMethod: "eth_getBalance",
rpcParams: [address, blockParam],
rpcResult: result,
});
}
}
}

function resolveBlockParam(block?: string): string {
let blockParam = block ?? "latest";
if (blockParam !== "latest" && blockParam !== "earliest" && blockParam !== "pending") {
if (!blockParam.startsWith("0x")) {
const num = parseInt(blockParam, 10);
if (isNaN(num) || num < 0) {
throw errInvalidArgs("Block must be a number, hex, or tag (latest, earliest, pending).");
}
blockParam = `0x${num.toString(16)}`;
}
}
return blockParam;
}

export function registerBalance(program: Command) {
program
.command("balance")
Expand All @@ -20,55 +76,26 @@ Examples:
alchemy balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 -n polygon-mainnet
echo 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 | alchemy balance
alchemy balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --block 15537393
alchemy balance vitalik.eth`,
alchemy balance vitalik.eth
cat addresses.txt | alchemy balance`,
)
.option("--block <block>", "Block number, hex, or tag (default: latest)")
.action(async (addressArg?: string, opts?: { block?: string }) => {
try {
const addressInput = addressArg ?? (await readStdinArg("address"));
const client = clientFromFlags(program);
const address = await resolveAddress(addressInput, client);
const blockParam = resolveBlockParam(opts?.block);

let blockParam = opts?.block ?? "latest";
if (blockParam !== "latest" && blockParam !== "earliest" && blockParam !== "pending") {
if (!blockParam.startsWith("0x")) {
const num = parseInt(blockParam, 10);
if (isNaN(num) || num < 0) {
throw errInvalidArgs("Block must be a number, hex, or tag (latest, earliest, pending).");
}
blockParam = `0x${num.toString(16)}`;
}
if (addressArg) {
await fetchBalance(program, addressArg, blockParam);
return;
}
const result = await withSpinner("Fetching balance…", "Balance fetched", () =>
client.call("eth_getBalance", [address, blockParam]),
) as string;

const wei = BigInt(result);
const network = resolveNetwork(program);
const symbol = nativeTokenSymbol(network);

if (isJSONMode()) {
printJSON({
address,
wei: wei.toString(),
balance: weiToEth(wei),
symbol,
network,
});
} else {
printKeyValueBox([
["Address", address],
["Balance", green(`${weiToEth(wei)} ${symbol}`)],
["Network", network],
]);

if (verbose) {
console.log("");
printJSON({
rpcMethod: "eth_getBalance",
rpcParams: [address, "latest"],
rpcResult: result,
});
// No positional arg — read from stdin (supports multiple lines)
const lines = await readStdinLines("address");
for (const line of lines) {
try {
await fetchBalance(program, line, blockParam);
} catch (err) {
exitWithError(err);
}
}
} catch (err) {
Expand Down
70 changes: 25 additions & 45 deletions src/commands/network.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Command } from "commander";
import {
resolveAppId,
resolveConfiguredNetworkSlugs,
resolveNetwork,
} from "../lib/resolve.js";
import { verbose, isJSONMode, printJSON } from "../lib/output.js";
import { dim, green, printTable, withSpinner } from "../lib/ui.js";
import { dim, green, printTable } from "../lib/ui.js";
import { getRPCNetworks } from "../lib/networks.js";
import { exitWithError } from "../lib/errors.js";

Expand All @@ -15,45 +13,31 @@ export function registerNetwork(program: Command) {
cmd
.command("list")
.description("List RPC network IDs for use with --network (e.g. eth-mainnet)")
.option(
"--configured",
"List only configured app RPC networks (requires access key and app context)",
)
.option(
"--app-id <id>",
"App ID for configured network lookups (overrides saved app)",
)
.action(async (opts: { configured?: boolean; appId?: string }) => {
.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: { mainnetOnly?: boolean; testnetOnly?: boolean; search?: string }) => {
try {
const supported = getRPCNetworks();
let display = getRPCNetworks();
const current = resolveNetwork(program);
const configured = opts.configured
? await withSpinner(
"Fetching configured networks…",
"Configured networks fetched",
() => resolveConfiguredNetworkSlugs(program, opts.appId),
)
: null;
const configuredSet = new Set(configured ?? []);
const appId = opts.configured
? opts.appId || resolveAppId(program)
: undefined;

const display = configured
? supported.filter((network) => configuredSet.has(network.id))
: supported;
if (opts.mainnetOnly) {
display = display.filter((n) => !n.isTestnet);
} else if (opts.testnetOnly) {
display = display.filter((n) => n.isTestnet);
}

if (isJSONMode()) {
if (configured) {
printJSON({
mode: "configured",
appId,
configuredNetworkIds: configured,
networks: display,
});
return;
}
if (opts.search) {
const term = opts.search.toLowerCase();
display = display.filter(
(n) =>
n.id.toLowerCase().includes(term) ||
n.name.toLowerCase().includes(term) ||
n.family.toLowerCase().includes(term),
);
}

if (isJSONMode()) {
printJSON(display);
return;
}
Expand All @@ -68,22 +52,18 @@ export function registerNetwork(program: Command) {

printTable(["Network ID", "Name", "Family", "Testnet"], rows);

if (configured) {
console.log(
`\n ${dim(`Configured networks for app ${appId}: ${display.length}`)}`,
);
}
console.log(`\n Current: ${green(current)}`);
console.log(
` ${dim("Need Admin API chain identifiers (e.g. ETH_MAINNET)? See: apps chains")}`,
);
console.log(
` ${dim("Need configured networks for an app? See: apps networks")}`,
);

if (verbose) {
console.log("");
printJSON({
mode: configured ? "configured" : "all",
appId: appId ?? null,
configuredNetworkIds: configured ?? null,
mode: "all",
networks: display,
currentNetwork: current,
});
Expand Down
19 changes: 0 additions & 19 deletions src/commands/portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,4 @@ export function registerPortfolio(program: Command) {
}
});

cmd
.command("transactions")
.description("Get transaction history by address/network pairs")
.requiredOption("--body <json>", "JSON body for /transactions/history/by-address")
.action(async (opts: { body: string }) => {
try {
const apiKey = resolveAPIKey(program);
const result = await runDataCall(
apiKey,
"transaction history",
"/transactions/history/by-address",
JSON.parse(opts.body),
);
if (isJSONMode()) printJSON(result);
else printSyntaxJSON(result);
} catch (err) {
exitWithError(err);
}
});
}
7 changes: 7 additions & 0 deletions src/commands/prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export function registerPrices(program: Command) {
.command("historical")
.description("Get historical prices")
.requiredOption("--body <json>", "JSON request payload")
.addHelpText(
"after",
`
Examples:
alchemy prices historical --body '{"symbol":"ETH","startTime":"2024-01-01T00:00:00Z","endTime":"2024-01-02T00:00:00Z","interval":"1h"}'
alchemy prices historical --body '{"address":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","network":"eth-mainnet","startTime":"2024-06-01","endTime":"2024-06-07","interval":"1d"}'`,
)
.action(async (opts: { body: string }) => {
try {
const apiKey = resolveAPIKey(program);
Expand Down
4 changes: 3 additions & 1 deletion src/commands/receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export function registerReceipt(program: Command) {
`
Examples:
alchemy receipt 0xabc123...
echo 0xabc123... | alchemy receipt`,
echo 0xabc123... | alchemy receipt

Tip: use 'alchemy tx <hash>' for transaction details (value, block, nonce). Receipt provides execution results (status, gas used, logs).`,
)
.action(async (hashArg?: string) => {
try {
Expand Down
Loading
Loading