mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 14:24:47 -07:00
mmpl: new, mempool + rpc: fixes
This commit is contained in:
@@ -13,7 +13,7 @@ brk_error = { workspace = true }
|
||||
brk_reader = { workspace = true }
|
||||
brk_rpc = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
owo-colors = { workspace = true, features = ["supports-colors"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_rpc::{Auth, Client};
|
||||
@@ -66,6 +66,9 @@ impl Args {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if a.starts_with('-') {
|
||||
return Err(Error::Parse(format!("unknown flag {a}")));
|
||||
}
|
||||
positional.push(a);
|
||||
}
|
||||
|
||||
@@ -74,6 +77,12 @@ impl Args {
|
||||
.next()
|
||||
.ok_or_else(|| Error::Parse("missing selector".into()))?;
|
||||
let paths: Vec<Path> = iter.map(|f| Path::parse(&f)).collect::<Result<_>>()?;
|
||||
let mut seen = HashSet::with_capacity(paths.len());
|
||||
for p in &paths {
|
||||
if !seen.insert(p.raw.as_str()) {
|
||||
return Err(Error::Parse(format!("duplicate field '{}'", p.raw)));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
selector,
|
||||
paths,
|
||||
|
||||
@@ -6,29 +6,126 @@ use bitcoin::{
|
||||
};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::ReadBlock;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::path::{Path, Step};
|
||||
|
||||
// `hex` is intentionally absent: matches `bitcoin-cli getblock <hash> 2`
|
||||
// and keeps NDJSON dumps tractable. Still reachable explicitly via `blk N hex`.
|
||||
const BLOCK_FIELDS: &[&str] = &[
|
||||
"height",
|
||||
"hash",
|
||||
"version",
|
||||
"version_hex",
|
||||
"merkle",
|
||||
"time",
|
||||
"nonce",
|
||||
"bits",
|
||||
"difficulty",
|
||||
"prev",
|
||||
"txs",
|
||||
"n_inputs",
|
||||
"n_outputs",
|
||||
"witness_txs",
|
||||
"size",
|
||||
"strippedsize",
|
||||
"weight",
|
||||
"subsidy",
|
||||
"coinbase",
|
||||
"coinbase_hex",
|
||||
"header_hex",
|
||||
"tx",
|
||||
];
|
||||
|
||||
const TX_FIELDS: &[&str] = &[
|
||||
"txid",
|
||||
"wtxid",
|
||||
"version",
|
||||
"locktime",
|
||||
"size",
|
||||
"base_size",
|
||||
"vsize",
|
||||
"weight",
|
||||
"inputs",
|
||||
"outputs",
|
||||
"is_coinbase",
|
||||
"has_witness",
|
||||
"is_rbf",
|
||||
"total_out",
|
||||
"hex",
|
||||
"vin",
|
||||
"vout",
|
||||
];
|
||||
|
||||
const VIN_FIELDS: &[&str] = &[
|
||||
"prev_txid",
|
||||
"prev_vout",
|
||||
"sequence",
|
||||
"script_sig",
|
||||
"script_sig_asm",
|
||||
"witness",
|
||||
"has_witness",
|
||||
"is_rbf",
|
||||
"coinbase",
|
||||
];
|
||||
|
||||
const VOUT_FIELDS: &[&str] = &[
|
||||
"value",
|
||||
"script_pubkey",
|
||||
"script_pubkey_asm",
|
||||
"type",
|
||||
"address",
|
||||
];
|
||||
|
||||
pub struct Ctx<'a> {
|
||||
block: &'a ReadBlock,
|
||||
network: Network,
|
||||
size_weight: OnceCell<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
pub fn new(block: &'a ReadBlock) -> Self {
|
||||
pub fn new(block: &'a ReadBlock, network: Network) -> Self {
|
||||
Self {
|
||||
block,
|
||||
network,
|
||||
size_weight: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&self, path: &Path) -> Result<Value> {
|
||||
let (step, rest) = pop(&path.steps)?;
|
||||
self.block_field(&step.name, step.index, rest)
|
||||
}
|
||||
|
||||
pub fn resolve_str(&self, path: &Path) -> Result<String> {
|
||||
Ok(match self.resolve(path)? {
|
||||
Value::String(s) => s,
|
||||
other => other.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn full(&self) -> Value {
|
||||
let mut obj = Map::with_capacity(BLOCK_FIELDS.len());
|
||||
for &name in BLOCK_FIELDS {
|
||||
obj.insert(
|
||||
name.into(),
|
||||
self.block_field(name, None, &[]).expect("known block field"),
|
||||
);
|
||||
}
|
||||
Value::Object(obj)
|
||||
}
|
||||
|
||||
fn size_and_weight(&self) -> (usize, usize) {
|
||||
*self
|
||||
.size_weight
|
||||
.get_or_init(|| self.block.total_size_and_weight())
|
||||
}
|
||||
|
||||
fn block_field(&self, name: &str, index: Option<usize>, rest: &[Step]) -> Result<Value> {
|
||||
let b = self.block;
|
||||
let raw: &Block = b;
|
||||
let scalar = |v| scalar_leaf(v, step, rest);
|
||||
match step.name.as_str() {
|
||||
let scalar = |v| scalar_leaf(v, name, index, rest);
|
||||
match name {
|
||||
"height" => scalar(json!(*b.height())),
|
||||
"hash" => scalar(json!(b.hash().to_string())),
|
||||
"time" => scalar(json!(b.header.time)),
|
||||
@@ -37,7 +134,7 @@ impl<'a> Ctx<'a> {
|
||||
"{:08x}",
|
||||
b.header.version.to_consensus() as u32
|
||||
))),
|
||||
"bits" => scalar(json!(b.header.bits.to_consensus())),
|
||||
"bits" => scalar(json!(format!("{:08x}", 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())),
|
||||
@@ -62,102 +159,154 @@ impl<'a> Ctx<'a> {
|
||||
"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)),
|
||||
"coinbase_hex" => {
|
||||
debug_assert!(
|
||||
!b.txdata.is_empty() && !b.txdata[0].input.is_empty(),
|
||||
"consensus-valid block has a coinbase tx with at least one input"
|
||||
);
|
||||
scalar(json!(b.txdata[0].input[0].script_sig.to_hex_string()))
|
||||
}
|
||||
"tx" => pick(&b.txdata, name, index, |i, tx| {
|
||||
self.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 resolve_tx(&self, tx: &Transaction, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
|
||||
if steps.is_empty() {
|
||||
let mut obj = Map::with_capacity(TX_FIELDS.len());
|
||||
for &name in TX_FIELDS {
|
||||
obj.insert(
|
||||
name.into(),
|
||||
self.tx_field(tx, is_coinbase, name, None, &[])
|
||||
.expect("known tx field"),
|
||||
);
|
||||
}
|
||||
return Ok(Value::Object(obj));
|
||||
}
|
||||
let (step, rest) = pop(steps)?;
|
||||
self.tx_field(tx, is_coinbase, &step.name, step.index, rest)
|
||||
}
|
||||
|
||||
pub fn full(&self) -> Value {
|
||||
let b = self.block;
|
||||
let (size, weight) = self.size_and_weight();
|
||||
let tx: Vec<Value> = b
|
||||
.txdata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, tx)| tx_to_value(tx, i == 0))
|
||||
.collect();
|
||||
json!({
|
||||
"height": *b.height(),
|
||||
"hash": b.hash().to_string(),
|
||||
"version": b.header.version.to_consensus(),
|
||||
"version_hex": format!("{:08x}", b.header.version.to_consensus() as u32),
|
||||
"merkle": b.header.merkle_root.to_string(),
|
||||
"time": b.header.time,
|
||||
"nonce": b.header.nonce,
|
||||
"bits": b.header.bits.to_consensus(),
|
||||
"difficulty": b.header.difficulty_float(),
|
||||
"prev": b.header.prev_blockhash.to_string(),
|
||||
"txs": b.txdata.len(),
|
||||
"n_inputs": b.txdata.iter().map(|t| t.input.len()).sum::<usize>(),
|
||||
"n_outputs": b.txdata.iter().map(|t| t.output.len()).sum::<usize>(),
|
||||
"witness_txs": b.txdata.iter().filter(|t| tx_has_witness(t)).count(),
|
||||
"size": size,
|
||||
"strippedsize": (weight - size) / 3,
|
||||
"weight": weight,
|
||||
"subsidy": subsidy_sats(*b.height()),
|
||||
"coinbase": b.coinbase_tag().as_str(),
|
||||
"header_hex": serialize_hex(&b.header),
|
||||
"tx": tx,
|
||||
})
|
||||
fn tx_field(
|
||||
&self,
|
||||
tx: &Transaction,
|
||||
is_coinbase: bool,
|
||||
name: &str,
|
||||
index: Option<usize>,
|
||||
rest: &[Step],
|
||||
) -> Result<Value> {
|
||||
let scalar = |v| scalar_leaf(v, name, index, rest);
|
||||
match name {
|
||||
"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, name, index, |j, vin| {
|
||||
resolve_vin(vin, is_coinbase && j == 0, rest)
|
||||
}),
|
||||
"vout" => pick(&tx.output, name, index, |_, vout| {
|
||||
self.resolve_vout(vout, rest)
|
||||
}),
|
||||
other => Err(unknown("tx", other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn size_and_weight(&self) -> (usize, usize) {
|
||||
*self
|
||||
.size_weight
|
||||
.get_or_init(|| self.block.total_size_and_weight())
|
||||
fn resolve_vout(&self, vout: &TxOut, steps: &[Step]) -> Result<Value> {
|
||||
if steps.is_empty() {
|
||||
let mut obj = Map::with_capacity(VOUT_FIELDS.len());
|
||||
for &name in VOUT_FIELDS {
|
||||
obj.insert(
|
||||
name.into(),
|
||||
self.vout_field(vout, name, None, &[])
|
||||
.expect("known vout field"),
|
||||
);
|
||||
}
|
||||
return Ok(Value::Object(obj));
|
||||
}
|
||||
let (step, rest) = pop(steps)?;
|
||||
self.vout_field(vout, &step.name, step.index, rest)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_tx(tx: &Transaction, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
|
||||
if steps.is_empty() {
|
||||
return Ok(tx_to_value(tx, is_coinbase));
|
||||
fn vout_field(
|
||||
&self,
|
||||
vout: &TxOut,
|
||||
name: &str,
|
||||
index: Option<usize>,
|
||||
rest: &[Step],
|
||||
) -> Result<Value> {
|
||||
let scalar = |v| scalar_leaf(v, name, index, rest);
|
||||
match name {
|
||||
"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(self.address_value(&vout.script_pubkey)),
|
||||
other => Err(unknown("vout", other)),
|
||||
}
|
||||
}
|
||||
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 address_value(&self, s: &ScriptBuf) -> Value {
|
||||
Address::from_script(s, self.network)
|
||||
.map(|a| Value::String(a.to_string()))
|
||||
.unwrap_or(Value::Null)
|
||||
}
|
||||
}
|
||||
|
||||
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 mut obj = Map::with_capacity(VIN_FIELDS.len());
|
||||
for &name in VIN_FIELDS {
|
||||
obj.insert(
|
||||
name.into(),
|
||||
vin_field(vin, is_coinbase, name, None, &[]).expect("known vin field"),
|
||||
);
|
||||
}
|
||||
return Ok(Value::Object(obj));
|
||||
}
|
||||
let (step, rest) = pop(steps)?;
|
||||
let scalar = |v| scalar_leaf(v, step, rest);
|
||||
match step.name.as_str() {
|
||||
vin_field(vin, is_coinbase, &step.name, step.index, rest)
|
||||
}
|
||||
|
||||
fn vin_field(
|
||||
vin: &TxIn,
|
||||
is_coinbase: bool,
|
||||
name: &str,
|
||||
index: Option<usize>,
|
||||
rest: &[Step],
|
||||
) -> Result<Value> {
|
||||
let scalar = |v| scalar_leaf(v, name, index, rest);
|
||||
match name {
|
||||
"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)),
|
||||
"witness" => {
|
||||
if !rest.is_empty() {
|
||||
return Err(Error::Parse(
|
||||
"'witness' element has no fields to drill into".into(),
|
||||
));
|
||||
}
|
||||
let items: Vec<String> = vin
|
||||
.witness
|
||||
.iter()
|
||||
.map(|w| w.to_lower_hex_string())
|
||||
.collect();
|
||||
pick(&items, name, index, |_, hex| Ok(Value::String(hex.clone())))
|
||||
}
|
||||
"has_witness" => scalar(json!(!vin.witness.is_empty())),
|
||||
"is_rbf" => scalar(json!(vin.sequence.is_rbf())),
|
||||
"coinbase" => scalar(json!(is_coinbase)),
|
||||
@@ -165,33 +314,17 @@ fn resolve_vin(vin: &TxIn, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
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],
|
||||
name: &str,
|
||||
index: Option<usize>,
|
||||
mut resolve: impl FnMut(usize, &T) -> Result<Value>,
|
||||
) -> Result<Value> {
|
||||
match step.index {
|
||||
match index {
|
||||
Some(i) => {
|
||||
let item = items
|
||||
.get(i)
|
||||
.ok_or_else(|| out_of_range(&step.name, i, items.len()))?;
|
||||
.ok_or_else(|| out_of_range(name, i, items.len()))?;
|
||||
resolve(i, item)
|
||||
}
|
||||
None => Ok(Value::Array(
|
||||
@@ -210,14 +343,13 @@ fn pop(steps: &[Step]) -> Result<(&Step, &[Step])> {
|
||||
.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)));
|
||||
fn scalar_leaf(v: Value, name: &str, index: Option<usize>, rest: &[Step]) -> Result<Value> {
|
||||
if index.is_some() {
|
||||
return Err(Error::Parse(format!("'{name}' is not an array")));
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
return Err(Error::Parse(format!(
|
||||
"'{}' is a scalar; nothing to drill into",
|
||||
step.name
|
||||
"'{name}' has no fields to drill into"
|
||||
)));
|
||||
}
|
||||
Ok(v)
|
||||
@@ -233,59 +365,6 @@ fn unknown(level: &str, name: &str) -> Error {
|
||||
))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -307,15 +386,6 @@ fn subsidy_sats(height: u32) -> u64 {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -335,9 +405,3 @@ fn script_type(s: &ScriptBuf) -> &'static str {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn address_value(s: &ScriptBuf) -> Value {
|
||||
Address::from_script(s, Network::Bitcoin)
|
||||
.map(|a| Value::String(a.to_string()))
|
||||
.unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
@@ -15,16 +15,18 @@ impl Formatter {
|
||||
|
||||
pub fn format(&self, ctx: &Ctx) -> Result<String> {
|
||||
match self.mode {
|
||||
Mode::Bare => self.bare(ctx),
|
||||
Mode::Bare => self.bare(ctx, false),
|
||||
Mode::Tsv => self.tsv(ctx),
|
||||
Mode::Json => Ok(serde_json::to_string(&self.object(ctx)?)?),
|
||||
Mode::Pretty if self.fields.len() == 1 => self.bare(ctx, true),
|
||||
Mode::Pretty => Ok(serde_json::to_string_pretty(&self.object(ctx)?)?),
|
||||
}
|
||||
}
|
||||
|
||||
fn bare(&self, ctx: &Ctx) -> Result<String> {
|
||||
fn bare(&self, ctx: &Ctx, pretty: bool) -> Result<String> {
|
||||
Ok(match ctx.resolve(&self.fields[0])? {
|
||||
Value::String(s) => s,
|
||||
other if pretty => serde_json::to_string_pretty(&other)?,
|
||||
other => other.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,17 +37,19 @@ fn run() -> Result<()> {
|
||||
|
||||
let client = args.rpc()?;
|
||||
let (start, end) = Selector::parse(&args.selector, &client)?;
|
||||
let network = client.get_network()?;
|
||||
|
||||
let mode = Mode::pick(args.pretty, args.compact, args.paths.len());
|
||||
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);
|
||||
let parser_threads = std::thread::available_parallelism()
|
||||
let parser_threads = (std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(2)
|
||||
/ 2;
|
||||
/ 2)
|
||||
.max(1);
|
||||
for block in reader.range_with(start, end, parser_threads)?.iter() {
|
||||
let block = block?;
|
||||
let line = formatter.format(&Ctx::new(&block))?;
|
||||
let line = formatter.format(&Ctx::new(&block, network))?;
|
||||
if !line.is_empty() {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use brk_error::{Error, Result};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Mode {
|
||||
Bare,
|
||||
@@ -7,8 +9,18 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn pick(pretty: bool, compact: bool, n_fields: usize) -> Self {
|
||||
if pretty {
|
||||
pub fn pick(pretty: bool, compact: bool, n_fields: usize) -> Result<Self> {
|
||||
if pretty && compact {
|
||||
return Err(Error::Parse(
|
||||
"--pretty and --compact are mutually exclusive".into(),
|
||||
));
|
||||
}
|
||||
if compact && n_fields == 0 {
|
||||
return Err(Error::Parse(
|
||||
"--compact requires at least one field".into(),
|
||||
));
|
||||
}
|
||||
Ok(if pretty {
|
||||
Self::Pretty
|
||||
} else if n_fields == 0 {
|
||||
Self::Json
|
||||
@@ -18,6 +30,6 @@ impl Mode {
|
||||
Self::Tsv
|
||||
} else {
|
||||
Self::Json
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ 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)
|
||||
}
|
||||
let (a, b) = s.split_once("..").unwrap_or((s, s));
|
||||
let needs_tip = |p: &str| p == "tip" || p.starts_with("tip-");
|
||||
let tip = if needs_tip(a) || needs_tip(b) {
|
||||
Some(client.get_last_height()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let start = Self::endpoint(a, tip)?;
|
||||
let end = Self::endpoint(b, tip)?;
|
||||
if end < start {
|
||||
return Err(Error::Parse(format!(
|
||||
"range end {end} before start {start}"
|
||||
@@ -21,15 +23,15 @@ impl Selector {
|
||||
Ok((start, end))
|
||||
}
|
||||
|
||||
fn endpoint(s: &str, client: &Client) -> Result<Height> {
|
||||
fn endpoint(s: &str, tip: Option<Height>) -> Result<Height> {
|
||||
if s == "tip" {
|
||||
return client.get_last_height();
|
||||
return Ok(tip.expect("tip pre-resolved when input contains 'tip'"));
|
||||
}
|
||||
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()?;
|
||||
let tip = tip.expect("tip pre-resolved when input contains 'tip'");
|
||||
return tip
|
||||
.checked_sub(n)
|
||||
.ok_or_else(|| Error::Parse(format!("tip-{n} underflows genesis")));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use owo_colors::OwoColorize;
|
||||
use owo_colors::{OwoColorize, Stream};
|
||||
|
||||
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")
|
||||
@@ -7,18 +7,18 @@ const PH_W: usize = LABEL_W - FLAG_W - 1; // placeholder column width so flag+ph
|
||||
const GAP: usize = 4;
|
||||
|
||||
pub fn print() {
|
||||
println!("{} - inspect a Bitcoin Core block", "blk".bold());
|
||||
println!("{} - inspect a Bitcoin Core block", bold("blk"));
|
||||
println!();
|
||||
|
||||
section("USAGE");
|
||||
println!(
|
||||
" blk {} [{} ...] [OPTIONS]",
|
||||
"<selector>".bright_black(),
|
||||
"<field>".bright_black()
|
||||
dim("<selector>"),
|
||||
dim("<field>")
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"no fields = full block as JSON (analog of `bitcoin-cli getblock <hash> 2`)".bright_black()
|
||||
dim("no fields = full block as JSON (analog of `bitcoin-cli getblock <hash> 2`)")
|
||||
);
|
||||
println!();
|
||||
|
||||
@@ -32,15 +32,15 @@ pub fn print() {
|
||||
section("FIELDS");
|
||||
println!(
|
||||
" {}",
|
||||
"dotted paths drill into nested data; omit an index for arrays".bright_black()
|
||||
dim("dotted paths drill into nested data, omit an index for arrays")
|
||||
);
|
||||
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",
|
||||
"witness_txs, size, strippedsize, weight, subsidy,",
|
||||
"coinbase, coinbase_hex, header_hex, hex",
|
||||
]);
|
||||
println!();
|
||||
group_note("tx.i", "omit i for all txs");
|
||||
@@ -61,14 +61,14 @@ pub fn print() {
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"Naked tx / tx.i / vin / vout returns the whole sub-object as JSON.".bright_black()
|
||||
dim("Naked tx / tx.i / vin / vout returns the whole sub-object as JSON.")
|
||||
);
|
||||
println!();
|
||||
|
||||
section("OUTPUT");
|
||||
out("no fields", "full block JSON object, one per line (NDJSON)");
|
||||
out("1 field", "bare value, one per line");
|
||||
out("2+ fields", "compact JSON object, one per line (NDJSON)");
|
||||
out("2+ fields", "JSON object, one per line (NDJSON)");
|
||||
out("-p, --pretty", "pretty JSON object instead");
|
||||
out(
|
||||
"-c, --compact",
|
||||
@@ -115,18 +115,18 @@ pub fn print() {
|
||||
}
|
||||
|
||||
fn section(name: &str) {
|
||||
println!("{}", format!("{name}:").bold());
|
||||
println!("{}", bold(&format!("{name}:")));
|
||||
}
|
||||
|
||||
fn group(name: &str) {
|
||||
println!(" {}", format!("{name}:").bold());
|
||||
println!(" {}", bold(&format!("{name}:")));
|
||||
}
|
||||
|
||||
fn group_note(name: &str, note: &str) {
|
||||
println!(
|
||||
" {} {}",
|
||||
format!("{name}:").bold(),
|
||||
format!("({note})").bright_black()
|
||||
bold(&format!("{name}:")),
|
||||
dim(&format!("({note})"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ fn pad(s: &str, width: usize) -> String {
|
||||
fn sel(token: &str, desc: &str) {
|
||||
println!(
|
||||
" {}{}{}{desc}",
|
||||
token.bright_black(),
|
||||
dim(token),
|
||||
pad(token, SEL_W),
|
||||
" ".repeat(GAP),
|
||||
);
|
||||
@@ -161,12 +161,12 @@ fn opt(flag: &str, ph: &str, desc: &str, default: Option<&str>) {
|
||||
let head = format!(
|
||||
" {flag}{} {}{}{}",
|
||||
pad(flag, FLAG_W),
|
||||
ph.bright_black(),
|
||||
dim(ph),
|
||||
pad(ph, PH_W),
|
||||
" ".repeat(GAP),
|
||||
);
|
||||
match default {
|
||||
Some(d) => println!("{head}{desc} {}", d.bright_black()),
|
||||
Some(d) => println!("{head}{desc} {}", dim(d)),
|
||||
None => println!("{head}{desc}"),
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,15 @@ fn ex(cmd: &str, note: &str) {
|
||||
" {cmd}{}{}{}",
|
||||
pad(cmd, LABEL_W),
|
||||
" ".repeat(GAP),
|
||||
format!("# {note}").bright_black()
|
||||
dim(&format!("# {note}"))
|
||||
);
|
||||
}
|
||||
|
||||
fn bold(s: &str) -> String {
|
||||
s.if_supports_color(Stream::Stdout, |t| t.bold()).to_string()
|
||||
}
|
||||
|
||||
fn dim(s: &str) -> String {
|
||||
s.if_supports_color(Stream::Stdout, |t| t.bright_black())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user