global + blk

This commit is contained in:
nym21
2026-05-07 14:02:53 +02:00
parent 9347b42c9a
commit cc9ebfaf42
30 changed files with 1044 additions and 140 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "blk"
description = "A command line tool to inspect Bitcoin Core blocks"
description = "A CLI to inspect Bitcoin Core blocks"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -8,6 +8,13 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
bitcoin = { workspace = true }
brk_error = { workspace = true }
brk_reader = { workspace = true }
brk_rpc = { workspace = true }
brk_types = { workspace = true }
owo-colors = { workspace = true }
serde_json = { workspace = true }
[[bin]]
name = "blk"

27
crates/blk/README.md Normal file
View File

@@ -0,0 +1,27 @@
# blk
A CLI to inspect Bitcoin Core blocks.
Reads `blk*.dat` files directly via [`brk_reader`](../brk_reader) and resolves
the chain tip / heights via the Bitcoin Core RPC. Output is shell-friendly:
bare values, NDJSON, pretty JSON, or TSV.
## Install
```sh
cargo install --path crates/blk
```
## Quick start
```sh
blk 800000 hash # bare hash
blk 800000 height hash time # one compact JSON line
blk 800000 tx.0.vout.0.value # coinbase output 0 sats
blk 0..2 hash tx.0.txid # 3 NDJSON lines
blk tip tx.0 # whole coinbase tx as JSON
```
## Reference
Run `blk --help` for the full field/selector/option reference.

131
crates/blk/src/args.rs Normal file
View File

@@ -0,0 +1,131 @@
use std::path::PathBuf;
use brk_error::{Error, Result};
use brk_rpc::{Auth, Client};
use crate::path::Path;
pub struct Args {
pub selector: String,
pub paths: Vec<Path>,
pub pretty: bool,
pub compact: bool,
bitcoindir: Option<PathBuf>,
blocksdir: Option<PathBuf>,
rpcconnect: Option<String>,
rpcport: Option<u16>,
rpccookiefile: Option<PathBuf>,
rpcuser: Option<String>,
rpcpassword: Option<String>,
}
impl Args {
pub fn parse(raw: Vec<String>) -> Result<Self> {
let mut pretty = false;
let mut compact = false;
let mut bitcoindir = None;
let mut blocksdir = None;
let mut rpcconnect = None;
let mut rpcport = None;
let mut rpccookiefile = None;
let mut rpcuser = None;
let mut rpcpassword = None;
let mut positional: Vec<String> = Vec::new();
let mut iter = raw.into_iter();
while let Some(a) = iter.next() {
if a == "-p" || a == "--pretty" {
pretty = true;
continue;
}
if a == "-c" || a == "--compact" {
compact = true;
continue;
}
if let Some(rest) = a.strip_prefix("--") {
let (key, value) = match rest.split_once('=') {
Some((k, v)) => (k.to_string(), v.to_string()),
None => (
rest.to_string(),
iter.next().ok_or_else(|| {
Error::Parse(format!("--{rest} requires a value"))
})?,
),
};
match key.as_str() {
"bitcoindir" => bitcoindir = Some(PathBuf::from(value)),
"blocksdir" => blocksdir = Some(PathBuf::from(value)),
"rpcconnect" => rpcconnect = Some(value),
"rpcport" => {
rpcport = Some(value.parse().map_err(|_| {
Error::Parse(format!("--rpcport: '{value}' is not a valid port"))
})?);
}
"rpccookiefile" => rpccookiefile = Some(PathBuf::from(value)),
"rpcuser" => rpcuser = Some(value),
"rpcpassword" => rpcpassword = Some(value),
other => return Err(Error::Parse(format!("unknown flag --{other}"))),
}
continue;
}
positional.push(a);
}
let mut iter = positional.into_iter();
let selector = iter
.next()
.ok_or_else(|| Error::Parse("missing selector".into()))?;
let paths: Vec<Path> = iter.map(|f| Path::parse(&f)).collect::<Result<_>>()?;
if paths.is_empty() {
return Err(Error::Parse(
"missing field. ask for at least one (e.g. `blk 0 hash`)".into(),
));
}
Ok(Self {
selector,
paths,
pretty,
compact,
bitcoindir,
blocksdir,
rpcconnect,
rpcport,
rpccookiefile,
rpcuser,
rpcpassword,
})
}
pub fn bitcoin_dir(&self) -> PathBuf {
self.bitcoindir
.clone()
.unwrap_or_else(Client::default_bitcoin_path)
}
pub fn blocks_dir(&self) -> PathBuf {
self.blocksdir
.clone()
.unwrap_or_else(|| self.bitcoin_dir().join("blocks"))
}
pub fn rpc(&self) -> Result<Client> {
let host = self.rpcconnect.as_deref().unwrap_or("localhost");
let port = self.rpcport.unwrap_or(8332);
let url = format!("http://{host}:{port}");
let cookie = self
.rpccookiefile
.clone()
.unwrap_or_else(|| self.bitcoin_dir().join(".cookie"));
let auth = if cookie.is_file() {
Auth::CookieFile(cookie)
} else if let (Some(u), Some(p)) =
(self.rpcuser.as_deref(), self.rpcpassword.as_deref())
{
Auth::UserPass(u.to_string(), p.to_string())
} else {
return Err(Error::Parse(
"no RPC auth: cookie file missing and --rpcuser/--rpcpassword not set".into(),
));
};
Client::new(&url, auth)
}
}

309
crates/blk/src/fields.rs Normal file
View File

@@ -0,0 +1,309 @@
use std::cell::OnceCell;
use bitcoin::{
Address, Block, Network, ScriptBuf, Transaction, TxIn, TxOut, consensus::encode::serialize_hex,
hex::DisplayHex,
};
use brk_error::{Error, Result};
use brk_types::ReadBlock;
use serde_json::{Value, json};
use crate::path::{Path, Step};
pub struct Ctx<'a> {
block: &'a ReadBlock,
size_weight: OnceCell<(usize, usize)>,
}
impl<'a> Ctx<'a> {
pub fn new(block: &'a ReadBlock) -> Self {
Self {
block,
size_weight: OnceCell::new(),
}
}
pub fn resolve(&self, path: &Path) -> Result<Value> {
let (step, rest) = pop(&path.steps)?;
let b = self.block;
let raw: &Block = b;
let scalar = |v| scalar_leaf(v, step, rest);
match step.name.as_str() {
"height" => scalar(json!(*b.height())),
"hash" => scalar(json!(b.hash().to_string())),
"time" => scalar(json!(b.header.time)),
"version" => scalar(json!(b.header.version.to_consensus())),
"version_hex" => scalar(json!(format!(
"{:08x}",
b.header.version.to_consensus() as u32
))),
"bits" => scalar(json!(b.header.bits.to_consensus())),
"nonce" => scalar(json!(b.header.nonce)),
"prev" => scalar(json!(b.header.prev_blockhash.to_string())),
"merkle" => scalar(json!(b.header.merkle_root.to_string())),
"difficulty" => scalar(json!(b.header.difficulty_float())),
"txs" => scalar(json!(b.txdata.len())),
"n_inputs" => scalar(json!(
b.txdata.iter().map(|tx| tx.input.len()).sum::<usize>()
)),
"n_outputs" => scalar(json!(
b.txdata.iter().map(|tx| tx.output.len()).sum::<usize>()
)),
"witness_txs" => scalar(json!(
b.txdata.iter().filter(|tx| tx_has_witness(tx)).count()
)),
"size" => scalar(json!(self.size_and_weight().0)),
"weight" => scalar(json!(self.size_and_weight().1)),
"strippedsize" => {
let (size, weight) = self.size_and_weight();
scalar(json!((weight - size) / 3))
}
"subsidy" => scalar(json!(subsidy_sats(*b.height()))),
"header_hex" => scalar(json!(serialize_hex(&b.header))),
"hex" => scalar(json!(serialize_hex(raw))),
"coinbase" => scalar(json!(b.coinbase_tag().as_str())),
"tx" => pick(&b.txdata, step, rest, |i, tx| resolve_tx(tx, i == 0, rest)),
other => Err(unknown("block", other)),
}
}
pub fn resolve_str(&self, path: &Path) -> Result<String> {
Ok(match self.resolve(path)? {
Value::String(s) => s,
other => other.to_string(),
})
}
fn size_and_weight(&self) -> (usize, usize) {
*self
.size_weight
.get_or_init(|| self.block.total_size_and_weight())
}
}
fn resolve_tx(tx: &Transaction, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
if steps.is_empty() {
return Ok(tx_to_value(tx, is_coinbase));
}
let (step, rest) = pop(steps)?;
let scalar = |v| scalar_leaf(v, step, rest);
match step.name.as_str() {
"txid" => scalar(json!(tx.compute_txid().to_string())),
"wtxid" => scalar(json!(tx.compute_wtxid().to_string())),
"version" => scalar(json!(tx.version.0)),
"locktime" => scalar(json!(tx.lock_time.to_consensus_u32())),
"size" => scalar(json!(tx.total_size())),
"base_size" => scalar(json!(tx.base_size())),
"vsize" => scalar(json!(tx.vsize())),
"weight" => scalar(json!(tx.weight().to_wu())),
"inputs" => scalar(json!(tx.input.len())),
"outputs" => scalar(json!(tx.output.len())),
"is_coinbase" => scalar(json!(is_coinbase)),
"has_witness" => scalar(json!(tx_has_witness(tx))),
"is_rbf" => scalar(json!(tx_is_rbf(tx))),
"total_out" => scalar(json!(tx_total_out(tx))),
"hex" => scalar(json!(serialize_hex(tx))),
"vin" => pick(&tx.input, step, rest, |j, vin| {
resolve_vin(vin, is_coinbase && j == 0, rest)
}),
"vout" => pick(&tx.output, step, rest, |_, vout| resolve_vout(vout, rest)),
other => Err(unknown("tx", other)),
}
}
fn resolve_vin(vin: &TxIn, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
if steps.is_empty() {
return Ok(vin_to_value(vin, is_coinbase));
}
let (step, rest) = pop(steps)?;
let scalar = |v| scalar_leaf(v, step, rest);
match step.name.as_str() {
"prev_txid" => scalar(json!(vin.previous_output.txid.to_string())),
"prev_vout" => scalar(json!(vin.previous_output.vout)),
"sequence" => scalar(json!(vin.sequence.0)),
"script_sig" => scalar(json!(vin.script_sig.to_hex_string())),
"script_sig_asm" => scalar(json!(vin.script_sig.to_asm_string())),
"witness" => scalar(witness_to_value(vin)),
"has_witness" => scalar(json!(!vin.witness.is_empty())),
"is_rbf" => scalar(json!(vin.sequence.is_rbf())),
"coinbase" => scalar(json!(is_coinbase)),
other => Err(unknown("vin", other)),
}
}
fn resolve_vout(vout: &TxOut, steps: &[Step]) -> Result<Value> {
if steps.is_empty() {
return Ok(vout_to_value(vout));
}
let (step, rest) = pop(steps)?;
let scalar = |v| scalar_leaf(v, step, rest);
match step.name.as_str() {
"value" => scalar(json!(vout.value.to_sat())),
"script_pubkey" => scalar(json!(vout.script_pubkey.to_hex_string())),
"script_pubkey_asm" => scalar(json!(vout.script_pubkey.to_asm_string())),
"type" => scalar(json!(script_type(&vout.script_pubkey))),
"address" => scalar(address_value(&vout.script_pubkey)),
other => Err(unknown("vout", other)),
}
}
fn pick<T>(
items: &[T],
step: &Step,
_rest: &[Step],
mut resolve: impl FnMut(usize, &T) -> Result<Value>,
) -> Result<Value> {
match step.index {
Some(i) => {
let item = items
.get(i)
.ok_or_else(|| out_of_range(&step.name, i, items.len()))?;
resolve(i, item)
}
None => Ok(Value::Array(
items
.iter()
.enumerate()
.map(|(i, item)| resolve(i, item))
.collect::<Result<_>>()?,
)),
}
}
fn pop(steps: &[Step]) -> Result<(&Step, &[Step])> {
steps
.split_first()
.ok_or_else(|| Error::Parse("empty path segment".into()))
}
fn scalar_leaf(v: Value, step: &Step, rest: &[Step]) -> Result<Value> {
if step.index.is_some() {
return Err(Error::Parse(format!("'{}' is not an array", step.name)));
}
if !rest.is_empty() {
return Err(Error::Parse(format!(
"'{}' is a scalar; nothing to drill into",
step.name
)));
}
Ok(v)
}
fn out_of_range(name: &str, i: usize, len: usize) -> Error {
Error::Parse(format!("{name}.{i} out of range (len {len})"))
}
fn unknown(level: &str, name: &str) -> Error {
Error::Parse(format!(
"unknown {level} field '{name}' (run `blk --help` for the list)"
))
}
fn tx_to_value(tx: &Transaction, is_coinbase: bool) -> Value {
let vin: Vec<Value> = tx
.input
.iter()
.enumerate()
.map(|(j, v)| vin_to_value(v, is_coinbase && j == 0))
.collect();
let vout: Vec<Value> = tx.output.iter().map(vout_to_value).collect();
json!({
"txid": tx.compute_txid().to_string(),
"wtxid": tx.compute_wtxid().to_string(),
"version": tx.version.0,
"locktime": tx.lock_time.to_consensus_u32(),
"size": tx.total_size(),
"base_size": tx.base_size(),
"vsize": tx.vsize(),
"weight": tx.weight().to_wu(),
"inputs": tx.input.len(),
"outputs": tx.output.len(),
"is_coinbase": is_coinbase,
"has_witness": tx_has_witness(tx),
"is_rbf": tx_is_rbf(tx),
"total_out": tx_total_out(tx),
"hex": serialize_hex(tx),
"vin": vin,
"vout": vout,
})
}
fn vin_to_value(vin: &TxIn, is_coinbase: bool) -> Value {
json!({
"prev_txid": vin.previous_output.txid.to_string(),
"prev_vout": vin.previous_output.vout,
"sequence": vin.sequence.0,
"script_sig": vin.script_sig.to_hex_string(),
"script_sig_asm": vin.script_sig.to_asm_string(),
"witness": witness_to_value(vin),
"has_witness": !vin.witness.is_empty(),
"is_rbf": vin.sequence.is_rbf(),
"coinbase": is_coinbase,
})
}
fn vout_to_value(vout: &TxOut) -> Value {
json!({
"value": vout.value.to_sat(),
"script_pubkey": vout.script_pubkey.to_hex_string(),
"script_pubkey_asm": vout.script_pubkey.to_asm_string(),
"type": script_type(&vout.script_pubkey),
"address": address_value(&vout.script_pubkey),
})
}
fn tx_has_witness(tx: &Transaction) -> bool {
tx.input.iter().any(|i| !i.witness.is_empty())
}
fn tx_is_rbf(tx: &Transaction) -> bool {
tx.input.iter().any(|i| i.sequence.is_rbf())
}
fn tx_total_out(tx: &Transaction) -> u64 {
tx.output.iter().map(|o| o.value.to_sat()).sum()
}
fn subsidy_sats(height: u32) -> u64 {
let halvings = height / 210_000;
if halvings >= 64 {
0
} else {
(50 * 100_000_000u64) >> halvings
}
}
fn witness_to_value(vin: &TxIn) -> Value {
Value::Array(
vin.witness
.iter()
.map(|w| Value::String(w.to_lower_hex_string()))
.collect(),
)
}
fn script_type(s: &ScriptBuf) -> &'static str {
if s.is_p2pkh() {
"p2pkh"
} else if s.is_p2sh() {
"p2sh"
} else if s.is_p2wpkh() {
"p2wpkh"
} else if s.is_p2wsh() {
"p2wsh"
} else if s.is_p2tr() {
"p2tr"
} else if s.is_op_return() {
"op_return"
} else if s.is_p2pk() {
"p2pk"
} else {
"unknown"
}
}
fn address_value(s: &ScriptBuf) -> Value {
Address::from_script(s, Network::Bitcoin)
.map(|a| Value::String(a.to_string()))
.unwrap_or(Value::Null)
}

View File

@@ -0,0 +1,66 @@
use brk_error::Result;
use serde_json::{Map, Value};
use crate::{fields::Ctx, mode::Mode, path::Path};
pub struct Formatter {
mode: Mode,
fields: Vec<Path>,
}
impl Formatter {
pub fn new(mode: Mode, fields: Vec<Path>) -> Self {
Self { mode, fields }
}
pub fn format(&self, ctx: &Ctx) -> Result<String> {
match self.mode {
Mode::Bare => self.bare(ctx),
Mode::Tsv => self.tsv(ctx),
Mode::Json => Ok(serde_json::to_string(&self.object(ctx)?)?),
Mode::Pretty => Ok(serde_json::to_string_pretty(&self.object(ctx)?)?),
}
}
fn bare(&self, ctx: &Ctx) -> Result<String> {
let mut out = String::new();
flatten(&ctx.resolve(&self.fields[0])?, &mut out);
Ok(out)
}
fn tsv(&self, ctx: &Ctx) -> Result<String> {
let mut row = String::new();
for (i, path) in self.fields.iter().enumerate() {
if i > 0 {
row.push('\t');
}
for c in ctx.resolve_str(path)?.chars() {
row.push(if matches!(c, '\t' | '\n' | '\r') { ' ' } else { c });
}
}
Ok(row)
}
fn object(&self, ctx: &Ctx) -> Result<Value> {
let mut obj = Map::with_capacity(self.fields.len());
for path in &self.fields {
obj.insert(path.raw.clone(), ctx.resolve(path)?);
}
Ok(Value::Object(obj))
}
}
fn flatten(v: &Value, out: &mut String) {
match v {
Value::Array(arr) => arr.iter().for_each(|item| flatten(item, out)),
Value::String(s) => push_line(out, s),
other => push_line(out, &other.to_string()),
}
}
fn push_line(out: &mut String, s: &str) {
if !out.is_empty() {
out.push('\n');
}
out.push_str(s);
}

View File

@@ -1 +1,52 @@
fn main() {}
mod args;
mod fields;
mod formatter;
mod mode;
mod path;
mod selector;
mod usage;
use std::process::ExitCode;
use brk_error::Result;
use brk_reader::Reader;
use args::Args;
use fields::Ctx;
use formatter::Formatter;
use mode::Mode;
use selector::Selector;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("blk: {e}");
ExitCode::from(1)
}
}
}
fn run() -> Result<()> {
let raw: Vec<String> = std::env::args().skip(1).collect();
if raw.is_empty() || raw.iter().any(|a| matches!(a.as_str(), "-h" | "--help")) {
usage::print();
return Ok(());
}
let args = Args::parse(raw)?;
let client = args.rpc()?;
let (start, end) = Selector::parse(&args.selector, &client)?;
let mode = Mode::pick(args.pretty, args.compact, args.paths.len());
let reader = Reader::new(args.blocks_dir(), &client);
let formatter = Formatter::new(mode, args.paths);
for block in reader.range(start, end)?.iter() {
let block = block?;
let line = formatter.format(&Ctx::new(&block))?;
if !line.is_empty() {
println!("{line}");
}
}
Ok(())
}

21
crates/blk/src/mode.rs Normal file
View File

@@ -0,0 +1,21 @@
#[derive(Clone, Copy)]
pub enum Mode {
Bare,
Tsv,
Json,
Pretty,
}
impl Mode {
pub fn pick(pretty: bool, compact: bool, n_fields: usize) -> Self {
if pretty {
Self::Pretty
} else if n_fields == 1 {
Self::Bare
} else if compact {
Self::Tsv
} else {
Self::Json
}
}
}

40
crates/blk/src/path.rs Normal file
View File

@@ -0,0 +1,40 @@
use brk_error::{Error, Result};
pub struct Step {
pub name: String,
pub index: Option<usize>,
}
pub struct Path {
pub raw: String,
pub steps: Vec<Step>,
}
impl Path {
pub fn parse(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.split('.').collect();
let mut steps = Vec::new();
let mut i = 0;
while i < parts.len() {
let name = parts[i];
if name.is_empty() {
return Err(Error::Parse(format!("bad path '{s}': empty segment")));
}
if name.parse::<usize>().is_ok() {
return Err(Error::Parse(format!(
"bad path '{s}': '{name}' must follow a field name"
)));
}
let index = parts.get(i + 1).and_then(|p| p.parse::<usize>().ok());
steps.push(Step {
name: name.to_string(),
index,
});
i += if index.is_some() { 2 } else { 1 };
}
Ok(Self {
raw: s.to_string(),
steps,
})
}
}

View File

@@ -0,0 +1,40 @@
use brk_error::{Error, Result};
use brk_rpc::Client;
use brk_types::{CheckedSub, Height};
pub struct Selector;
impl Selector {
pub fn parse(s: &str, client: &Client) -> Result<(Height, Height)> {
let (start, end) = match s.split_once("..") {
Some((a, b)) => (Self::endpoint(a, client)?, Self::endpoint(b, client)?),
None => {
let h = Self::endpoint(s, client)?;
(h, h)
}
};
if end < start {
return Err(Error::Parse(format!("range end {end} before start {start}")));
}
Ok((start, end))
}
fn endpoint(s: &str, client: &Client) -> Result<Height> {
if s == "tip" {
return client.get_last_height();
}
if let Some(rest) = s.strip_prefix("tip-") {
let n: u32 = rest
.parse()
.map_err(|_| Error::Parse(format!("bad tip offset: {s}")))?;
let tip = client.get_last_height()?;
return tip
.checked_sub(n)
.ok_or_else(|| Error::Parse(format!("tip-{n} underflows genesis")));
}
let n: u32 = s
.parse()
.map_err(|_| Error::Parse(format!("bad height: {s}")))?;
Ok(Height::new(n))
}
}

155
crates/blk/src/usage.rs Normal file
View File

@@ -0,0 +1,155 @@
use owo_colors::OwoColorize;
const SEL_W: usize = 5; // longest selector token: "tip-N"
const LABEL_W: usize = 28; // longest label across OUTPUT/OPTIONS/EXAMPLES (= example cmd "blk 800000 tx.0.vout.0.value")
const FLAG_W: usize = 15; // longest flag: "--rpccookiefile"
const PH_W: usize = LABEL_W - FLAG_W - 1; // placeholder column width so flag+ph total = LABEL_W
const GAP: usize = 4;
pub fn print() {
println!("{} - inspect a Bitcoin Core block", "blk".bold());
println!();
section("USAGE");
println!(
" blk {} {} [field ...] [OPTIONS]",
"<selector>".bright_black(),
"<field>".bright_black()
);
println!();
section("SELECTOR");
sel("<n>", "single height (e.g. 800000)");
sel("tip", "current chain tip");
sel("tip-N", "tip minus N");
sel("a..b", "inclusive range, endpoints can be height/tip/tip-N");
println!();
section("FIELDS");
println!(
" {}",
"dotted paths drill into nested data; omit an index for arrays"
.bright_black()
);
println!();
group("block");
fields(&[
"height, hash, time, version, version_hex, bits, nonce,",
"prev, merkle, difficulty, txs, n_inputs, n_outputs,",
"witness_txs, size, strippedsize, weight, subsidy, coinbase,",
"header_hex, hex",
]);
println!();
group_note("tx.i", "omit i for all txs");
fields(&[
"txid, wtxid, version, locktime, size, base_size, vsize,",
"weight, inputs, outputs, is_coinbase, has_witness, is_rbf,",
"total_out, hex",
]);
println!();
group_note("tx.i.vin.j", "omit j for all inputs");
fields(&[
"prev_txid, prev_vout, sequence, script_sig, script_sig_asm,",
"witness, has_witness, is_rbf, coinbase",
]);
println!();
group_note("tx.i.vout.j", "omit j for all outputs");
fields(&["value, script_pubkey, script_pubkey_asm, type, address"]);
println!();
println!(
" {}",
"Naked tx / tx.i / vin / vout returns the whole sub-object as JSON."
.bright_black()
);
println!();
section("OUTPUT");
out("1 field", "bare value, one per line");
out("2+ fields", "compact JSON object, one per line (NDJSON)");
out("-p, --pretty", "pretty JSON object instead");
out("-c, --compact", "tab-separated values, no field names (TSV)");
println!();
section("OPTIONS");
opt("--bitcoindir", "<PATH>", "Bitcoin directory", Some("[OS default]"));
opt("--blocksdir", "<PATH>", "Blocks directory", Some("[<bitcoindir>/blocks]"));
opt("--rpcconnect", "<IP>", "RPC host", Some("[localhost]"));
opt("--rpcport", "<PORT>", "RPC port", Some("[8332]"));
opt("--rpccookiefile", "<PATH>", "RPC cookie file", Some("[<bitcoindir>/.cookie]"));
opt("--rpcuser", "<USERNAME>", "RPC username", None);
opt("--rpcpassword", "<PASSWORD>", "RPC password", None);
println!();
section("EXAMPLES");
ex("blk 800000 hash", "bare hash");
ex("blk 800000 height hash time", "one compact JSON line");
ex("blk 800000 tx.0.txid", "coinbase txid");
ex("blk 800000 tx.txid", "all txids in block (array)");
ex("blk 800000 tx.0.vout.0.value", "coinbase output 0 sats");
ex("blk 800000 tx.0.vout.value", "all output sats for tx 0");
ex("blk 800000 tx.vout.value", "array of arrays (per tx)");
ex("blk 0..2 hash tx.0.txid", "3 NDJSON lines");
ex("blk tip tx.0", "whole coinbase tx as JSON");
}
fn section(name: &str) {
println!("{}", format!("{name}:").bold());
}
fn group(name: &str) {
println!(" {}", format!("{name}:").bold());
}
fn group_note(name: &str, note: &str) {
println!(
" {} {}",
format!("{name}:").bold(),
format!("({note})").bright_black()
);
}
fn fields(lines: &[&str]) {
for line in lines {
println!(" {line}");
}
}
fn pad(s: &str, width: usize) -> String {
" ".repeat(width.saturating_sub(s.len()))
}
fn sel(token: &str, desc: &str) {
println!(
" {}{}{}{desc}",
token.bright_black(),
pad(token, SEL_W),
" ".repeat(GAP),
);
}
fn out(label: &str, desc: &str) {
println!(" {label}{}{}{desc}", pad(label, LABEL_W), " ".repeat(GAP));
}
fn opt(flag: &str, ph: &str, desc: &str, default: Option<&str>) {
let head = format!(
" {flag}{} {}{}{}",
pad(flag, FLAG_W),
ph.bright_black(),
pad(ph, PH_W),
" ".repeat(GAP),
);
match default {
Some(d) => println!("{head}{desc} {}", d.bright_black()),
None => println!("{head}{desc}"),
}
}
fn ex(cmd: &str, note: &str) {
println!(
" {cmd}{}{}{}",
pad(cmd, LABEL_W),
" ".repeat(GAP),
format!("# {note}").bright_black()
);
}