feat: add status and accounts commands#1
Conversation
ethsmith status — shows chain ID, block number, account count + balances ethsmith accounts — lists all accounts with ETH balances, supports --json Both use built-in http module (zero new deps), query JSON-RPC on configurable --port (default 8545), and exit 1 with helpful message when no node is running. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the status and accounts commands to the CLI to query node status and list accounts with their balances. The review feedback highlights three main issues: code duplication of the rpc helper function, incorrect JSON-RPC error handling that could lead to runtime crashes, and a lack of port validation. The reviewer provided a refactored implementation that extracts a shared createRpcClient helper to address these concerns.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // ── STATUS ──────────────────────────────────────────────────────────────── | ||
| program | ||
| .command('status') | ||
| .description('Show running node status: block number, chain ID, accounts, port') | ||
| .option('-p, --port <port>', 'RPC port to query', '8545') | ||
| .action(async (opts) => { | ||
| const http = require('http') | ||
| const port = parseInt(opts.port, 10) | ||
|
|
||
| function rpc (method, params = []) { | ||
| return new Promise((resolve, reject) => { | ||
| const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) | ||
| const req = http.request({ host: '127.0.0.1', port, path: '/', method: 'POST', | ||
| headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } | ||
| }, res => { | ||
| let data = '' | ||
| res.on('data', c => { data += c }) | ||
| res.on('end', () => { | ||
| try { resolve(JSON.parse(data).result) } catch { reject(new Error('bad response')) } | ||
| }) | ||
| }) | ||
| req.on('error', reject) | ||
| req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')) }) | ||
| req.write(body) | ||
| req.end() | ||
| }) | ||
| } | ||
|
|
||
| const ok = s => ` \x1b[32m✔\x1b[0m ${s}` | ||
| const err = s => ` \x1b[31m✖\x1b[0m ${s}` | ||
|
|
||
| console.log(`\nethsmith status — port ${port}\n`) | ||
|
|
||
| try { | ||
| const [blockHex, chainHex, accounts] = await Promise.all([ | ||
| rpc('eth_blockNumber'), | ||
| rpc('eth_chainId'), | ||
| rpc('eth_accounts'), | ||
| ]) | ||
| const block = parseInt(blockHex, 16) | ||
| const chain = parseInt(chainHex, 16) | ||
|
|
||
| console.log(ok(`Node running on http://127.0.0.1:${port}`)) | ||
| console.log(ok(`Chain ID ${chain}`)) | ||
| console.log(ok(`Block #${block}`)) | ||
| console.log(ok(`Accounts ${accounts.length}`)) | ||
| if (accounts.length) { | ||
| const bals = await Promise.all(accounts.slice(0, 5).map(a => rpc('eth_getBalance', [a, 'latest']))) | ||
| console.log() | ||
| accounts.slice(0, 5).forEach((a, i) => { | ||
| const eth = (BigInt(bals[i]) * 100n / 10n ** 18n) | ||
| const display = `${eth / 100n}.${String(eth % 100n).padStart(2, '0')}` | ||
| console.log(` ${a} ${display} ETH`) | ||
| }) | ||
| if (accounts.length > 5) console.log(` ... and ${accounts.length - 5} more`) | ||
| } | ||
| console.log() | ||
| } catch (e) { | ||
| console.log(err(`No node running on port ${port} (${e.message})`)) | ||
| console.log(`\n Start one with: \x1b[33methsmith\x1b[0m\n`) | ||
| process.exit(1) | ||
| } | ||
| }) | ||
|
|
||
| // ── ACCOUNTS ────────────────────────────────────────────────────────────── | ||
| program | ||
| .command('accounts') | ||
| .description('List accounts and ETH balances from the running node') | ||
| .option('-p, --port <port>', 'RPC port to query', '8545') | ||
| .option('--json', 'output as JSON') | ||
| .action(async (opts) => { | ||
| const http = require('http') | ||
| const port = parseInt(opts.port, 10) | ||
|
|
||
| function rpc (method, params = []) { | ||
| return new Promise((resolve, reject) => { | ||
| const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) | ||
| const req = http.request({ host: '127.0.0.1', port, path: '/', method: 'POST', | ||
| headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } | ||
| }, res => { | ||
| let data = '' | ||
| res.on('data', c => { data += c }) | ||
| res.on('end', () => { | ||
| try { resolve(JSON.parse(data).result) } catch { reject(new Error('bad response')) } | ||
| }) | ||
| }) | ||
| req.on('error', reject) | ||
| req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')) }) | ||
| req.write(body) | ||
| req.end() | ||
| }) | ||
| } | ||
|
|
||
| try { | ||
| const accounts = await rpc('eth_accounts') | ||
| const bals = await Promise.all(accounts.map(a => rpc('eth_getBalance', [a, 'latest']))) | ||
|
|
||
| const result = accounts.map((a, i) => { | ||
| const wei = BigInt(bals[i]) | ||
| const eth = Number(wei) / 1e18 | ||
| return { address: a, balance_wei: bals[i], balance_eth: eth.toFixed(4) } | ||
| }) | ||
|
|
||
| if (opts.json) { | ||
| console.log(JSON.stringify(result, null, 2)) | ||
| return | ||
| } | ||
|
|
||
| console.log(`\n Accounts (${result.length} total)\n`) | ||
| result.forEach((r, i) => { | ||
| console.log(` [${i}] ${r.address}`) | ||
| console.log(` ${r.balance_eth} ETH`) | ||
| }) | ||
| console.log() | ||
| } catch (e) { | ||
| console.error(` No node on port ${port}: ${e.message}`) | ||
| console.error(` Start one with: ethsmith`) | ||
| process.exit(1) | ||
| } | ||
| }) |
There was a problem hiding this comment.
There are three main issues in the newly added commands:
- Code Duplication: The
rpchelper function is duplicated verbatim in both thestatusandaccountscommands. - Incorrect JSON-RPC Error Handling: The
rpchelper resolves withundefinedwhen the node returns a JSON-RPC error (since it only checksJSON.parse(data).resultand doesn't check for theerrorfield). This causes downstreamTypeErrorcrashes (e.g., when trying to readaccounts.lengthor convertingbals[i]toBigInt). - Missing Port Validation: The
portoption is parsed usingparseIntbut never validated, which can lead toNaNand unexpected behavior if an invalid port is provided.
Extracting a shared createRpcClient helper inside buildCLI resolves the duplication, handles JSON-RPC errors correctly by rejecting on error, and allows for clean port validation in both commands.
// ── RPC HELPER ────────────────────────────────────────────────────────────
function createRpcClient(port) {
const http = require('http')
return function rpc(method, params = []) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
const req = http.request({
host: '127.0.0.1',
port,
path: '/',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body)
}
}, res => {
let data = ''
res.on('data', c => { data += c })
res.on('end', () => {
try {
const parsed = JSON.parse(data)
if (parsed.error) {
reject(new Error(parsed.error.message || 'RPC error'))
} else {
resolve(parsed.result)
}
} catch {
reject(new Error('bad response'))
}
})
})
req.on('error', reject)
req.setTimeout(3000, () => {
req.destroy()
reject(new Error('timeout'))
})
req.write(body)
req.end()
})
}
}
// ── STATUS ────────────────────────────────────────────────────────────────
program
.command('status')
.description('Show running node status: block number, chain ID, accounts, port')
.option('-p, --port <port>', 'RPC port to query', '8545')
.action(async (opts) => {
const port = parseInt(opts.port, 10)
if (isNaN(port)) {
console.error(' \\x1b[31m✖\\x1b[0m Invalid port number')
process.exit(1)
}
const rpc = createRpcClient(port)
const ok = s => ` \\x1b[32m✔\\x1b[0m ${s}`
const err = s => ` \\x1b[31m✖\\x1b[0m ${s}`
console.log(`\\nethsmith status — port ${port}\\n`)
try {
const [blockHex, chainHex, accounts] = await Promise.all([
rpc('eth_blockNumber'),
rpc('eth_chainId'),
rpc('eth_accounts'),
])
const block = parseInt(blockHex, 16)
const chain = parseInt(chainHex, 16)
console.log(ok(`Node running on http://127.0.0.1:${port}`))
console.log(ok(`Chain ID ${chain}`))
console.log(ok(`Block #${block}`))
console.log(ok(`Accounts ${accounts ? accounts.length : 0}`))
if (accounts && accounts.length) {
const bals = await Promise.all(accounts.slice(0, 5).map(a => rpc('eth_getBalance', [a, 'latest'])))
console.log()
accounts.slice(0, 5).forEach((a, i) => {
const eth = (BigInt(bals[i]) * 100n / 10n ** 18n)
const display = `${eth / 100n}.${String(eth % 100n).padStart(2, '0')}`
console.log(` ${a} ${display} ETH`)
})
if (accounts.length > 5) console.log(` ... and ${accounts.length - 5} more`)
}
console.log()
} catch (e) {
console.log(err(`No node running on port ${port} (${e.message})`))
console.log(`\\n Start one with: \\x1b[33methsmith\\x1b[0m\\n`)
process.exit(1)
}
})
// ── ACCOUNTS ──────────────────────────────────────────────────────────────
program
.command('accounts')
.description('List accounts and ETH balances from the running node')
.option('-p, --port <port>', 'RPC port to query', '8545')
.option('--json', 'output as JSON')
.action(async (opts) => {
const port = parseInt(opts.port, 10)
if (isNaN(port)) {
console.error(' Invalid port number')
process.exit(1)
}
const rpc = createRpcClient(port)
try {
const accounts = await rpc('eth_accounts')
if (!accounts || !Array.isArray(accounts)) {
throw new Error('No accounts returned from node')
}
const bals = await Promise.all(accounts.map(a => rpc('eth_getBalance', [a, 'latest'])))
const result = accounts.map((a, i) => {
const wei = BigInt(bals[i] || 0)
const eth = Number(wei) / 1e18
return { address: a, balance_wei: bals[i], balance_eth: eth.toFixed(4) }
})
if (opts.json) {
console.log(JSON.stringify(result, null, 2))
return
}
console.log(`\\n Accounts (${result.length} total)\\n`)
result.forEach((r, i) => {
console.log(` [${i}] ${r.address}`)
console.log(` ${r.balance_eth} ETH`)
})
console.log()
} catch (e) {
console.error(` No node on port ${port}: ${e.message}`)
console.error(` Start one with: ethsmith`)
process.exit(1)
}
})
Summary
ethsmith status— queries the running node (default port 8545), shows chain ID, current block number, account count, and top-5 balancesethsmith accounts— lists every account with ETH balance;--jsonflag for machine-readable outputhttpmodule only (zero new dependencies),--portflag to target any port, exit 1 with helpful message when no node is runningTest plan
ethsmithand runethsmith status— verify chain ID, block, accounts printethsmith accounts— verify all 10 Ganache accounts listedethsmith accounts --json— verify valid JSON arrayethsmith status --port 9999with no node — verify exit 1 message🤖 Generated with Claude Code