diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 7c8bfb67d..ab0a03702 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -8953,7 +8953,7 @@ pub struct BrkClient { impl BrkClient { /// Client version. - pub const VERSION: &'static str = "v0.3.0-beta.8"; + pub const VERSION: &'static str = "v0.3.0-beta.9"; /// Create a new client with the given base URL. pub fn new(base_url: impl Into) -> Self { @@ -9734,7 +9734,7 @@ impl BrkClient { /// Projected mempool blocks /// - /// Get projected blocks from the mempool for fee estimation. + /// Projected blocks for fee estimation. Block 0 reflects Bitcoin Core's actual next-block selection; blocks 1+ are a fee-tier approximation. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)* /// @@ -9745,7 +9745,7 @@ impl BrkClient { /// Recommended fees /// - /// Get recommended fee rates for different confirmation targets. + /// Recommended fee rates by confirmation target. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)* /// @@ -9756,7 +9756,7 @@ impl BrkClient { /// Precise recommended fees /// - /// Get recommended fee rates with up to 3 decimal places. + /// Recommended fee rates with sub-integer precision. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)* /// @@ -9778,10 +9778,10 @@ impl BrkClient { /// Mempool content hash /// - /// Returns an opaque `u64` that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled. + /// Returns an opaque hash that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled. /// /// Endpoint: `GET /api/mempool/hash` - pub fn get_mempool_hash(&self) -> Result { + pub fn get_mempool_hash(&self) -> Result { self.base.get_json(&format!("/api/mempool/hash")) } @@ -9829,6 +9829,24 @@ impl BrkClient { self.base.get_json(&format!("/api/v1/fullrbf/replacements")) } + /// Projected next block template + /// + /// Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. + /// + /// Endpoint: `GET /api/v1/mempool/block-template` + pub fn get_block_template(&self) -> Result { + self.base.get_json(&format!("/api/v1/mempool/block-template")) + } + + /// Block template diff since hash + /// + /// Delta of the projected next block since ``. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. + /// + /// Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}` + pub fn get_block_template_diff(&self, hash: NextBlockHash) -> Result { + self.base.get_json(&format!("/api/v1/mempool/block-template/diff/{hash}")) + } + /// Live BTC/USD price /// /// Returns the current BTC/USD price in dollars, derived from on-chain round-dollar output patterns in the last 12 blocks plus mempool. diff --git a/crates/brk_logger/src/lib.rs b/crates/brk_logger/src/lib.rs index 428863fcc..722f6151d 100644 --- a/crates/brk_logger/src/lib.rs +++ b/crates/brk_logger/src/lib.rs @@ -23,6 +23,7 @@ const MAX_LOG_AGE_DAYS: u64 = 7; /// `*.txt` file older than 7 days is pruned on startup. pub fn init(dir: Option<&Path>) -> io::Result<()> { tracing_log::LogTracer::init().ok(); + install_panic_hook(); #[cfg(debug_assertions)] const DEFAULT_LEVEL: &str = "debug"; @@ -65,6 +66,24 @@ pub fn init(dir: Option<&Path>) -> io::Result<()> { Ok(()) } +fn install_panic_hook() { + std::panic::set_hook(Box::new(|info| { + let location = info + .location() + .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) + .unwrap_or_else(|| "unknown".to_string()); + let payload = info.payload(); + let msg = payload + .downcast_ref::<&str>() + .copied() + .map(str::to_owned) + .or_else(|| payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "Box".to_owned()); + let backtrace = std::backtrace::Backtrace::capture(); + tracing::error!(location, backtrace = %backtrace, "panic: {msg}"); + })); +} + /// Register a hook that gets called for every log message. pub fn register_hook(hook: F) -> Result<(), &'static str> where diff --git a/crates/brk_mempool/src/lib.rs b/crates/brk_mempool/src/lib.rs index 27c0bc2f5..028ffffd5 100644 --- a/crates/brk_mempool/src/lib.rs +++ b/crates/brk_mempool/src/lib.rs @@ -30,9 +30,11 @@ use std::{ use brk_error::Result; use brk_rpc::Client; use brk_types::{ - AddrBytes, AddrMempoolStats, FeeRate, MempoolInfo, MempoolRecentTx, OutpointPrefix, OutputType, - Sats, Timestamp, Transaction, TxOut, Txid, TxidPrefix, Vin, Vout, + AddrBytes, AddrMempoolStats, BlockTemplate, BlockTemplateDiff, FeeRate, MempoolBlock, + MempoolInfo, MempoolRecentTx, NextBlockHash, OutpointPrefix, OutputType, Sats, Timestamp, + Transaction, TxOut, Txid, TxidPrefix, Vin, Vout, }; +use rustc_hash::FxHashSet; use parking_lot::{RwLock, RwLockReadGuard}; use tracing::error; @@ -102,10 +104,58 @@ impl Mempool { self.snapshot().block_stats.clone() } - pub fn next_block_hash(&self) -> u64 { + pub fn next_block_hash(&self) -> NextBlockHash { self.snapshot().next_block_hash } + /// Full projected next block: Core's `getblocktemplate` selection + /// (block 0) with aggregate stats and full tx bodies in GBT order. + pub fn block_template(&self) -> BlockTemplate { + let snap = self.snapshot(); + let stats = MempoolBlock::from(&snap.block_stats[0]); + let txids: Vec = snap.blocks[0] + .iter() + .map(|idx| snap.txs[idx.as_usize()].txid) + .collect(); + let transactions = self.collect_txs(&txids); + BlockTemplate { + hash: snap.next_block_hash, + stats, + transactions, + } + } + + /// Delta of the projected next block since `since`. `None` when + /// `since` has aged out of the rebuilder's history (server should + /// 404 → client falls back to `block_template`). `removed` is just + /// txids; `added` carries full bodies so clients can patch their + /// local view in one round trip. + pub fn block_template_diff(&self, since: NextBlockHash) -> Option { + let past = self.0.rebuilder.historical_block0(since)?; + let snap = self.snapshot(); + let current: FxHashSet = snap.blocks[0] + .iter() + .map(|idx| snap.txs[idx.as_usize()].txid) + .collect(); + let added_txids: Vec = current.difference(&past).copied().collect(); + let removed: Vec = past.difference(¤t).copied().collect(); + let added = self.collect_txs(&added_txids); + Some(BlockTemplateDiff { + hash: snap.next_block_hash, + since, + added, + removed, + }) + } + + fn collect_txs(&self, txids: &[Txid]) -> Vec { + let state = self.read(); + txids + .iter() + .filter_map(|txid| state.txs.get(txid).cloned()) + .collect() + } + pub fn addr_state_hash(&self, addr: &AddrBytes) -> u64 { self.read().addrs.stats_hash(addr) } diff --git a/crates/brk_mempool/src/steps/rebuilder/mod.rs b/crates/brk_mempool/src/steps/rebuilder/mod.rs index 1785f45c8..41607e59e 100644 --- a/crates/brk_mempool/src/steps/rebuilder/mod.rs +++ b/crates/brk_mempool/src/steps/rebuilder/mod.rs @@ -1,10 +1,13 @@ -use std::sync::{ - Arc, - atomic::{AtomicBool, AtomicU64, Ordering}, +use std::{ + collections::VecDeque, + sync::{ + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, + }, }; use brk_rpc::BlockTemplateTx; -use brk_types::{FeeRate, TxidPrefix}; +use brk_types::{FeeRate, NextBlockHash, Txid, TxidPrefix}; use parking_lot::RwLock; use rustc_hash::FxHashSet; @@ -20,10 +23,13 @@ pub use brk_types::RecommendedFees; pub use snapshot::{BlockStats, SnapTx, Snapshot, TxIndex}; const NUM_BLOCKS: usize = 8; +const HISTORY: usize = 10; #[derive(Default)] pub struct Rebuilder { snapshot: RwLock>, + /// Past block-0 txid sets keyed by `next_block_hash`, oldest first. + history: RwLock)>>, dirty: AtomicBool, rebuild_count: AtomicU64, skip_clean: AtomicU64, @@ -49,11 +55,38 @@ impl Rebuilder { self.skip_clean.fetch_add(1, Ordering::Relaxed); return; } - *self.snapshot.write() = Arc::new(Self::build_snapshot(lock, gbt, min_fee)); + let snap = Self::build_snapshot(lock, gbt, min_fee); + let block0_set: FxHashSet = snap.blocks[0] + .iter() + .map(|idx| snap.txs[idx.as_usize()].txid) + .collect(); + let next_hash = snap.next_block_hash; + *self.snapshot.write() = Arc::new(snap); + self.push_history(next_hash, block0_set); self.dirty.store(false, Ordering::Release); self.rebuild_count.fetch_add(1, Ordering::Relaxed); } + fn push_history(&self, hash: NextBlockHash, set: FxHashSet) { + let mut hist = self.history.write(); + hist.retain(|(h, _)| *h != hash); + hist.push_back((hash, set)); + while hist.len() > HISTORY { + hist.pop_front(); + } + } + + /// Past block-0 txid set for `hash`, or `None` if it has aged out + /// (or was never seen). Used by `block_template_diff` to decide + /// 200 vs 404. + pub fn historical_block0(&self, hash: NextBlockHash) -> Option> { + self.history + .read() + .iter() + .find(|(h, _)| *h == hash) + .map(|(_, set)| set.clone()) + } + pub fn rebuild_count(&self) -> u64 { self.rebuild_count.load(Ordering::Relaxed) } diff --git a/crates/brk_mempool/src/steps/rebuilder/snapshot/mod.rs b/crates/brk_mempool/src/steps/rebuilder/snapshot/mod.rs index 79e2e5465..57f5647df 100644 --- a/crates/brk_mempool/src/steps/rebuilder/snapshot/mod.rs +++ b/crates/brk_mempool/src/steps/rebuilder/snapshot/mod.rs @@ -11,7 +11,7 @@ pub use tx_index::TxIndex; use std::hash::{DefaultHasher, Hash, Hasher}; -use brk_types::{FeeRate, RecommendedFees, TxidPrefix}; +use brk_types::{FeeRate, NextBlockHash, RecommendedFees, TxidPrefix}; use fees::Fees; @@ -30,7 +30,7 @@ pub struct Snapshot { pub fees: RecommendedFees, /// Content hash of the projected next block. Same value as the /// mempool ETag. - pub next_block_hash: u64, + pub next_block_hash: NextBlockHash, /// Per-snapshot `TxidPrefix -> TxIndex` index, so live queries can /// resolve a prefix to the snapshot's compact index without /// re-walking `txs`. Built once by `builder::build_txs` and reused @@ -70,13 +70,13 @@ impl Snapshot { } } - fn hash_next_block(blocks: &[Vec]) -> u64 { + fn hash_next_block(blocks: &[Vec]) -> NextBlockHash { let Some(block) = blocks.first() else { - return 0; + return NextBlockHash::ZERO; }; let mut hasher = DefaultHasher::new(); block.hash(&mut hasher); - hasher.finish() + NextBlockHash::new(hasher.finish()) } pub fn tx(&self, idx: TxIndex) -> Option<&SnapTx> { diff --git a/crates/brk_mempool/src/steps/rebuilder/snapshot/stats.rs b/crates/brk_mempool/src/steps/rebuilder/snapshot/stats.rs index bbde8c9ba..954a30b1f 100644 --- a/crates/brk_mempool/src/steps/rebuilder/snapshot/stats.rs +++ b/crates/brk_mempool/src/steps/rebuilder/snapshot/stats.rs @@ -1,4 +1,4 @@ -use brk_types::{FeeRate, Sats, VSize, get_weighted_percentile}; +use brk_types::{FeeRate, MempoolBlock, Sats, VSize, get_weighted_percentile}; use super::{SnapTx, TxIndex}; @@ -83,3 +83,9 @@ impl BlockStats { self.fee_range[3] } } + +impl From<&BlockStats> for MempoolBlock { + fn from(s: &BlockStats) -> Self { + Self::new(s.tx_count, s.total_size, s.total_vsize, s.total_fee, s.fee_range) + } +} diff --git a/crates/brk_query/src/impl/mempool.rs b/crates/brk_query/src/impl/mempool.rs index 885dd4ed1..b9e68fe42 100644 --- a/crates/brk_query/src/impl/mempool.rs +++ b/crates/brk_query/src/impl/mempool.rs @@ -2,9 +2,9 @@ use crate::Query; use brk_error::{Error, Result}; use brk_mempool::{Mempool, PrevoutResolver, RbfForTx, RbfNode}; use brk_types::{ - CheckedSub, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, OutputType, RbfResponse, - RbfTx, RecommendedFees, ReplacementNode, Sats, Timestamp, TxOut, TxOutIndex, Txid, TxidPrefix, - TypeIndex, + BlockTemplate, BlockTemplateDiff, CheckedSub, FeeRate, MempoolBlock, MempoolInfo, + MempoolRecentTx, NextBlockHash, OutputType, RbfResponse, RbfTx, RecommendedFees, + ReplacementNode, Sats, Timestamp, TxOut, TxOutIndex, Txid, TxidPrefix, TypeIndex, }; const RECENT_REPLACEMENTS_LIMIT: usize = 25; @@ -28,23 +28,7 @@ impl Query { pub fn mempool_blocks(&self) -> Result> { let mempool = self.require_mempool()?; - - let block_stats = mempool.block_stats(); - - let blocks = block_stats - .into_iter() - .map(|stats| { - MempoolBlock::new( - stats.tx_count, - stats.total_size, - stats.total_vsize, - stats.total_fee, - stats.fee_range, - ) - }) - .collect(); - - Ok(blocks) + Ok(mempool.block_stats().iter().map(MempoolBlock::from).collect()) } /// Indexer-backed resolver for confirmed-parent prevouts. Pass @@ -172,7 +156,22 @@ impl Query { /// Content hash of the projected next block. Same value as the /// mempool ETag. Polling lets monitors detect a stalled sync. - pub fn mempool_hash(&self) -> Result { + pub fn mempool_hash(&self) -> Result { Ok(self.require_mempool()?.next_block_hash()) } + + /// Full projected next block (Core's `getblocktemplate` selection) + /// with stats and full tx bodies in GBT order. + pub fn block_template(&self) -> Result { + Ok(self.require_mempool()?.block_template()) + } + + /// Delta of the projected next block since `since`. `NotFound` + /// when `since` has aged out (client should fall back to + /// `block_template`). + pub fn block_template_diff(&self, since: NextBlockHash) -> Result { + self.require_mempool()? + .block_template_diff(since) + .ok_or_else(|| Error::NotFound(format!("unknown since hash: {since}"))) + } } diff --git a/crates/brk_rpc/src/methods.rs b/crates/brk_rpc/src/methods.rs index 4c13bad75..daa6fd422 100644 --- a/crates/brk_rpc/src/methods.rs +++ b/crates/brk_rpc/src/methods.rs @@ -373,22 +373,25 @@ impl Client { /// Verbose mempool listing + Core's projected next block + live /// `mempoolminfee`, fetched in a single bitcoind round-trip. - /// Validates that every GBT txid is present in the verbose listing - /// and returns `Ok(None)` on mismatch so the caller can skip the - /// cycle (within-batch races inside bitcoind are rare; persistent - /// drift is bug-shaped). Other failures bubble up as `Err`. + /// `getblocktemplate` runs first so that any tx arriving between + /// the two intra-batch calls lands in the verbose listing only, + /// preserving GBT ⊆ verbose for the common race. Validates that + /// every GBT txid is present in the verbose listing and returns + /// `Ok(None)` on mismatch so the caller can skip the cycle: + /// republishing block 0 with missing txids would diverge from + /// Core's exact selection. Other failures bubble up as `Err`. pub fn fetch_mempool_state(&self) -> Result> { let requests: [(&str, Vec); 3] = [ - ("getrawmempool", vec![Value::Bool(true)]), ( "getblocktemplate", vec![serde_json::json!({ "rules": ["segwit"] })], ), + ("getrawmempool", vec![Value::Bool(true)]), ("getmempoolinfo", vec![]), ]; let mut out = self.0.call_mixed_batch(&requests)?.into_iter(); - let verbose_raw = out.next().ok_or(Error::Internal("missing verbose"))??; let gbt_raw = out.next().ok_or(Error::Internal("missing gbt"))??; + let verbose_raw = out.next().ok_or(Error::Internal("missing verbose"))??; let info_raw = out.next().ok_or(Error::Internal("missing mempoolinfo"))??; let verbose: FxHashMap = serde_json::from_str(verbose_raw.get())?; diff --git a/crates/brk_server/src/api/fees.rs b/crates/brk_server/src/api/fees.rs index 44b363296..d2ea56b78 100644 --- a/crates/brk_server/src/api/fees.rs +++ b/crates/brk_server/src/api/fees.rs @@ -27,7 +27,7 @@ impl FeesRoutes for ApiRouter { op.id("get_mempool_blocks") .fees_tag() .summary("Projected mempool blocks") - .description("Get projected blocks from the mempool for fee estimation.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*") + .description("Projected blocks for fee estimation. Block 0 reflects Bitcoin Core's actual next-block selection; blocks 1+ are a fee-tier approximation.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*") .json_response::>() .not_modified() .server_error() @@ -48,7 +48,7 @@ impl FeesRoutes for ApiRouter { op.id("get_recommended_fees") .fees_tag() .summary("Recommended fees") - .description("Get recommended fee rates for different confirmation targets.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*") + .description("Recommended fee rates by confirmation target.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*") .json_response::() .not_modified() .server_error() @@ -69,7 +69,7 @@ impl FeesRoutes for ApiRouter { op.id("get_precise_fees") .fees_tag() .summary("Precise recommended fees") - .description("Get recommended fee rates with up to 3 decimal places.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*") + .description("Recommended fee rates with sub-integer precision.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*") .json_response::() .not_modified() .server_error() diff --git a/crates/brk_server/src/api/mempool.rs b/crates/brk_server/src/api/mempool.rs index b53b7d77e..2f809a546 100644 --- a/crates/brk_server/src/api/mempool.rs +++ b/crates/brk_server/src/api/mempool.rs @@ -1,11 +1,18 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ - extract::State, + extract::{Path, State}, http::{HeaderMap, Uri}, }; -use brk_types::{Dollars, MempoolInfo, MempoolRecentTx, ReplacementNode, Txid}; +use brk_types::{ + BlockTemplate, BlockTemplateDiff, Dollars, MempoolInfo, MempoolRecentTx, NextBlockHash, + ReplacementNode, Txid, +}; -use crate::{AppState, extended::TransformResponseExtended, params::Empty}; +use crate::{ + AppState, + extended::TransformResponseExtended, + params::{Empty, NextBlockHashParam}, +}; pub trait MempoolRoutes { fn add_mempool_routes(self) -> Self; @@ -44,8 +51,8 @@ impl MempoolRoutes for ApiRouter { op.id("get_mempool_hash") .mempool_tag() .summary("Mempool content hash") - .description("Returns an opaque `u64` that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled.") - .json_response::() + .description("Returns an opaque hash that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled.") + .json_response::() .not_modified() .server_error() }, @@ -131,6 +138,53 @@ impl MempoolRoutes for ApiRouter { }, ), ) + .api_route( + "/api/v1/mempool/block-template", + get_with( + async |uri: Uri, headers: HeaderMap, _: Empty, State(state): State| { + state + .respond_json(&headers, state.mempool_strategy(), &uri, |q| { + q.block_template() + }) + .await + }, + |op| { + op.id("get_block_template") + .mempool_tag() + .summary("Projected next block template") + .description("Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template.") + .json_response::() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mempool/block-template/diff/{hash}", + get_with( + async |uri: Uri, + headers: HeaderMap, + Path(path): Path, + _: Empty, + State(state): State| { + state + .respond_json(&headers, state.mempool_strategy(), &uri, move |q| { + q.block_template_diff(path.hash) + }) + .await + }, + |op| { + op.id("get_block_template_diff") + .mempool_tag() + .summary("Block template diff since hash") + .description("Delta of the projected next block since ``. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.") + .json_response::() + .not_modified() + .not_found() + .server_error() + }, + ), + ) .api_route( "/api/mempool/price", get_with( diff --git a/crates/brk_server/src/lib.rs b/crates/brk_server/src/lib.rs index cf18becc3..09777101d 100644 --- a/crates/brk_server/src/lib.rs +++ b/crates/brk_server/src/lib.rs @@ -102,9 +102,11 @@ impl Server { let response_time_layer = axum::middleware::from_fn( async |request: Request, next: Next| -> Response { let uri = request.uri().clone(); + let method = request.method().clone(); let start = Instant::now(); let mut response = next.run(request).await; response.extensions_mut().insert(uri); + response.extensions_mut().insert(method); response.headers_mut().insert( "X-Response-Time", format!("{}us", start.elapsed().as_micros()) @@ -182,14 +184,19 @@ impl Server { .on_response( |response: &Response, latency: Duration, _: &tracing::Span| { let status = response.status().as_u16(); - let unknown = Uri::from_static("/unknown"); - let uri = response.extensions().get::().unwrap_or(&unknown); + let unknown_uri = Uri::from_static("/unknown"); + let unknown_method = axum::http::Method::default(); + let uri = response.extensions().get::().unwrap_or(&unknown_uri); + let method = response + .extensions() + .get::() + .unwrap_or(&unknown_method); match response.status() { - StatusCode::OK => info!(status, %uri, ?latency), + StatusCode::OK => info!(%method, status, %uri, ?latency), StatusCode::NOT_MODIFIED | StatusCode::TEMPORARY_REDIRECT - | StatusCode::PERMANENT_REDIRECT => info!(status, %uri, ?latency), - _ => error!(status, %uri, ?latency), + | StatusCode::PERMANENT_REDIRECT => info!(%method, status, %uri, ?latency), + _ => error!(%method, status, %uri, ?latency), } }, ) @@ -209,8 +216,6 @@ impl Server { let router = router .with_state(state) .merge(website_router) - .layer(response_time_layer) - .layer(trace_layer) .layer(TimeoutLayer::with_status_code( StatusCode::GATEWAY_TIMEOUT, REQUEST_TIMEOUT, @@ -242,7 +247,9 @@ impl Server { .or_else(|| panic.downcast_ref::<&str>().copied()) .unwrap_or("Unknown panic"); Error::internal(msg).into_response() - })); + })) + .layer(response_time_layer) + .layer(trace_layer); let (listener, port) = match port { Some(port) => { diff --git a/crates/brk_server/src/params/mod.rs b/crates/brk_server/src/params/mod.rs index 2a00f1e1d..804109e75 100644 --- a/crates/brk_server/src/params/mod.rs +++ b/crates/brk_server/src/params/mod.rs @@ -6,6 +6,7 @@ mod blockhash_start_index; mod blockhash_tx_index; mod empty; mod height_param; +mod next_block_hash_param; mod pool_slug_param; mod series_param; mod time_period_param; @@ -25,6 +26,7 @@ pub use blockhash_start_index::*; pub use blockhash_tx_index::*; pub use empty::*; pub use height_param::*; +pub use next_block_hash_param::*; pub use pool_slug_param::*; pub use series_param::*; pub use time_period_param::*; diff --git a/crates/brk_server/src/params/next_block_hash_param.rs b/crates/brk_server/src/params/next_block_hash_param.rs new file mode 100644 index 000000000..8f58fbaa2 --- /dev/null +++ b/crates/brk_server/src/params/next_block_hash_param.rs @@ -0,0 +1,10 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use brk_types::NextBlockHash; + +/// `since` hash for `/api/v1/mining/block-template/diff/{hash}`. +#[derive(Deserialize, JsonSchema)] +pub struct NextBlockHashParam { + pub hash: NextBlockHash, +} diff --git a/crates/brk_server/src/state.rs b/crates/brk_server/src/state.rs index b000ab15e..d2949c10c 100644 --- a/crates/brk_server/src/state.rs +++ b/crates/brk_server/src/state.rs @@ -124,7 +124,7 @@ impl AppState { if let Some(mempool) = q.mempool() && mempool.contains_txid(txid) { - return CacheStrategy::MempoolHash(mempool.next_block_hash()); + return CacheStrategy::MempoolHash(mempool.next_block_hash().into()); } if let Ok((_, height)) = q.resolve_tx(txid) && let Ok(block_hash) = q.block_hash_by_height(height) @@ -136,7 +136,7 @@ impl AppState { } pub fn mempool_strategy(&self) -> CacheStrategy { - let hash = self.sync(|q| q.mempool().map(|m| m.next_block_hash()).unwrap_or(0)); + let hash = self.sync(|q| q.mempool().map(|m| m.next_block_hash().into()).unwrap_or(0)); CacheStrategy::MempoolHash(hash) } diff --git a/crates/brk_types/src/block_template.rs b/crates/brk_types/src/block_template.rs new file mode 100644 index 000000000..e568de0bd --- /dev/null +++ b/crates/brk_types/src/block_template.rs @@ -0,0 +1,21 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{MempoolBlock, NextBlockHash, Transaction}; + +/// Projected next-block contents from Bitcoin Core's `getblocktemplate` +/// (block 0 of the snapshot). Returned by +/// `GET /api/v1/mining/block-template`. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BlockTemplate { + /// Pass back as `` on + /// `/api/v1/mining/block-template/diff/{hash}` to fetch deltas. + pub hash: NextBlockHash, + + /// Aggregate stats for this block (size, vsize, fee range, ...). + pub stats: MempoolBlock, + + /// Full transaction bodies in `getblocktemplate` order. + pub transactions: Vec, +} diff --git a/crates/brk_types/src/block_template_diff.rs b/crates/brk_types/src/block_template_diff.rs new file mode 100644 index 000000000..7df21ba28 --- /dev/null +++ b/crates/brk_types/src/block_template_diff.rs @@ -0,0 +1,25 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{NextBlockHash, Transaction, Txid}; + +/// Delta between the current `getblocktemplate` projection and a prior +/// one identified by `since`. Returned by +/// `GET /api/v1/mining/block-template/diff/{hash}`. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BlockTemplateDiff { + /// Current next-block hash. Use as `since` on the next diff call. + pub hash: NextBlockHash, + + /// Echoed prior hash the diff was computed against. + pub since: NextBlockHash, + + /// Full bodies of transactions that joined the projected next + /// block since `since`. + pub added: Vec, + + /// Txids that left the projected next block since `since` + /// (confirmed, evicted, replaced, or pushed past block 0). + pub removed: Vec, +} diff --git a/crates/brk_types/src/lib.rs b/crates/brk_types/src/lib.rs index 2cc13d60b..bc708b264 100644 --- a/crates/brk_types/src/lib.rs +++ b/crates/brk_types/src/lib.rs @@ -80,6 +80,8 @@ mod hour4; mod index; mod index_info; mod limit; +mod block_template; +mod block_template_diff; mod mempool_block; mod mempool_entry_info; mod mempool_info; @@ -90,6 +92,7 @@ mod minute30; mod month1; mod month3; mod month6; +mod next_block_hash; mod ohlc; mod op_return_index; mod option_ext; @@ -225,6 +228,8 @@ pub use block_rewards_entry::*; pub use block_size_entry::*; pub use block_sizes_weights::*; pub use block_status::*; +pub use block_template::*; +pub use block_template_diff::*; pub use block_timestamp::*; pub use block_tx_index::*; pub use block_weight_entry::*; @@ -283,6 +288,7 @@ pub use minute30::*; pub use month1::*; pub use month3::*; pub use month6::*; +pub use next_block_hash::*; pub use ohlc::*; pub use op_return_index::*; pub use option_ext::*; diff --git a/crates/brk_types/src/next_block_hash.rs b/crates/brk_types/src/next_block_hash.rs new file mode 100644 index 000000000..84c54903f --- /dev/null +++ b/crates/brk_types/src/next_block_hash.rs @@ -0,0 +1,51 @@ +use std::fmt; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Content hash of the projected next block (block 0 of the mempool +/// snapshot). Same value as the mempool ETag. Opaque token: pass back +/// as `since` on `/api/v1/mining/block-template/diff/{hash}` to fetch +/// deltas. +#[derive( + Debug, + Default, + Clone, + Copy, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +#[serde(transparent)] +pub struct NextBlockHash(u64); + +impl NextBlockHash { + pub const ZERO: Self = Self(0); + + pub const fn new(value: u64) -> Self { + Self(value) + } +} + +impl fmt::Display for NextBlockHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for NextBlockHash { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + fn from(value: NextBlockHash) -> Self { + value.0 + } +} diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 647583f8a..91cb73e9a 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -281,6 +281,30 @@ Matches mempool.space/bitcoin-cli behavior. * @property {(Height|null)=} height - Block height (only if in best chain) * @property {(BlockHash|null)=} nextBest - Hash of the next block in the best chain (null if tip) */ +/** + * Projected next-block contents from Bitcoin Core's `getblocktemplate` + * (block 0 of the snapshot). Returned by + * `GET /api/v1/mining/block-template`. + * + * @typedef {Object} BlockTemplate + * @property {NextBlockHash} hash - Pass back as `` on +`/api/v1/mining/block-template/diff/{hash}` to fetch deltas. + * @property {MempoolBlock} stats - Aggregate stats for this block (size, vsize, fee range, ...). + * @property {Transaction[]} transactions - Full transaction bodies in `getblocktemplate` order. + */ +/** + * Delta between the current `getblocktemplate` projection and a prior + * one identified by `since`. Returned by + * `GET /api/v1/mining/block-template/diff/{hash}`. + * + * @typedef {Object} BlockTemplateDiff + * @property {NextBlockHash} hash - Current next-block hash. Use as `since` on the next diff call. + * @property {NextBlockHash} since - Echoed prior hash the diff was computed against. + * @property {Transaction[]} added - Full bodies of transactions that joined the projected next +block since `since`. + * @property {Txid[]} removed - Txids that left the projected next block since `since` +(confirmed, evicted, replaced, or pushed past block 0). + */ /** * Block information returned for timestamp queries * @@ -711,6 +735,20 @@ ancestors and no descendants (matches mempool.space). /** @typedef {number} Month1 */ /** @typedef {number} Month3 */ /** @typedef {number} Month6 */ +/** + * Content hash of the projected next block (block 0 of the mempool + * snapshot). Same value as the mempool ETag. Opaque token: pass back + * as `since` on `/api/v1/mining/block-template/diff/{hash}` to fetch + * deltas. + * + * @typedef {number} NextBlockHash + */ +/** + * `since` hash for `/api/v1/mining/block-template/diff/{hash}`. + * + * @typedef {Object} NextBlockHashParam + * @property {NextBlockHash} hash + */ /** * OHLC (Open, High, Low, Close) data in cents * @@ -11620,7 +11658,7 @@ class BrkClient extends BrkClientBase { /** * Projected mempool blocks * - * Get projected blocks from the mempool for fee estimation. + * Projected blocks for fee estimation. Block 0 reflects Bitcoin Core's actual next-block selection; blocks 1+ are a fee-tier approximation. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)* * @@ -11636,7 +11674,7 @@ class BrkClient extends BrkClientBase { /** * Recommended fees * - * Get recommended fee rates for different confirmation targets. + * Recommended fee rates by confirmation target. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)* * @@ -11652,7 +11690,7 @@ class BrkClient extends BrkClientBase { /** * Precise recommended fees * - * Get recommended fee rates with up to 3 decimal places. + * Recommended fee rates with sub-integer precision. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)* * @@ -11684,11 +11722,11 @@ class BrkClient extends BrkClientBase { /** * Mempool content hash * - * Returns an opaque `u64` that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled. + * Returns an opaque hash that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled. * * Endpoint: `GET /api/mempool/hash` - * @param {{ signal?: AbortSignal, onValue?: (value: number) => void }} [options] - * @returns {Promise} + * @param {{ signal?: AbortSignal, onValue?: (value: NextBlockHash) => void }} [options] + * @returns {Promise} */ async getMempoolHash({ signal, onValue } = {}) { const path = `/api/mempool/hash`; @@ -11759,6 +11797,36 @@ class BrkClient extends BrkClientBase { return this.getJson(path, { signal, onValue }); } + /** + * Projected next block template + * + * Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. + * + * Endpoint: `GET /api/v1/mempool/block-template` + * @param {{ signal?: AbortSignal, onValue?: (value: BlockTemplate) => void }} [options] + * @returns {Promise} + */ + async getBlockTemplate({ signal, onValue } = {}) { + const path = `/api/v1/mempool/block-template`; + return this.getJson(path, { signal, onValue }); + } + + /** + * Block template diff since hash + * + * Delta of the projected next block since ``. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. + * + * Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}` + * + * @param {NextBlockHash} hash + * @param {{ signal?: AbortSignal, onValue?: (value: BlockTemplateDiff) => void }} [options] + * @returns {Promise} + */ + async getBlockTemplateDiff(hash, { signal, onValue } = {}) { + const path = `/api/v1/mempool/block-template/diff/${hash}`; + return this.getJson(path, { signal, onValue }); + } + /** * Live BTC/USD price * diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index d730161a4..8bfbc5eee 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -68,6 +68,37 @@ BlockHash = str # Position of a transaction within a single block (0 = coinbase). # Distinct from `TxIndex`, which is the chain-wide global tx index. BlockTxIndex = int +# Content hash of the projected next block (block 0 of the mempool +# snapshot). Same value as the mempool ETag. Opaque token: pass back +# as `since` on `/api/v1/mining/block-template/diff/{hash}` to fetch +# deltas. +NextBlockHash = int +# Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps. +RawLockTime = int +# BIP-141 sigop cost. The block-level budget is 80,000, so a `u32` +# fits a single tx's count with room to spare. +# +# Witness sigops count as 1; legacy and P2SH-redeem sigops count as 4. +# Five vbytes per sigop is the policy adjustment Core applies in +# `nSigOpCost` to discourage sigop-heavy txs (`max(weight/4, sigops*5)`). +SigOps = int +# Index of the output being spent in the previous transaction +Vout = int +# Transaction witness: a stack of byte arrays, one per witness item. +# +# Wraps `bitcoin::Witness` (single-buffer layout with offsets, much +# more compact than `Vec>`). Serializes as a JSON array of +# hex strings - the format used by Bitcoin Core REST and mempool.space +# and matching brk's `script_sig: ScriptBuf` (bytes internally, hex +# on the wire). +Witness = List[str] +# Chain-wide transaction index (0 = the genesis coinbase). For an +# in-block position, use `BlockTxIndex` instead. +TxIndex = int +# Raw transaction version (i32) from Bitcoin protocol. +# Unlike TxVersion (u8, indexed), this preserves non-standard values +# used in coinbase txs for miner signaling/branding. +TxVersionRaw = int # Unsigned cents (u64) - for values that should never be negative. # Used for invested capital, realized cap, etc. Cents = int @@ -106,13 +137,6 @@ UrpdAggregation = Literal["raw", "lin200", "lin500", "lin1000", "log10", "log50" # Position of a transaction inside a `CpfpCluster.txs` array. Cluster-local, # has no meaning outside the enclosing cluster. CpfpClusterTxIndex = int -# BIP-141 sigop cost. The block-level budget is 80,000, so a `u32` -# fits a single tx's count with room to spare. -# -# Witness sigops count as 1; legacy and P2SH-redeem sigops count as 4. -# Five vbytes per sigop is the policy adjustment Core applies in -# `nSigOpCost` to discourage sigop-heavy txs (`max(weight/4, sigops*5)`). -SigOps = int # Virtual size in vbytes (weight / 4, rounded up). Max block vsize is ~1,000,000 vB. VSize = int # Date in YYYYMMDD format stored as u32 @@ -179,8 +203,6 @@ P2WPKHAddrIndex = TypeIndex P2WPKHBytes = U8x20 P2WSHAddrIndex = TypeIndex P2WSHBytes = U8x32 -# Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps. -RawLockTime = int # Fractional satoshis (f64) - for representing USD prices in sats # # Formula: `sats_fract = usd_value * 100_000_000 / btc_price` @@ -219,23 +241,6 @@ StoredU64 = int # Used to specify the lookback window for pool statistics, hashrate calculations, # and other time-based mining series. TimePeriod = Literal["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"] -# Index of the output being spent in the previous transaction -Vout = int -# Transaction witness: a stack of byte arrays, one per witness item. -# -# Wraps `bitcoin::Witness` (single-buffer layout with offsets, much -# more compact than `Vec>`). Serializes as a JSON array of -# hex strings - the format used by Bitcoin Core REST and mempool.space -# and matching brk's `script_sig: ScriptBuf` (bytes internally, hex -# on the wire). -Witness = List[str] -# Chain-wide transaction index (0 = the genesis coinbase). For an -# in-block position, use `BlockTxIndex` instead. -TxIndex = int -# Raw transaction version (i32) from Bitcoin protocol. -# Unlike TxVersion (u8, indexed), this preserves non-standard values -# used in coinbase txs for miner signaling/branding. -TxVersionRaw = int # Hierarchical tree node for organizing series into categories TreeNode = Union[dict[str, "TreeNode"], "SeriesLeafWithSchema"] TxInIndex = int @@ -640,6 +645,142 @@ class BlockStatus(TypedDict): height: Union[Height, None] next_best: Union[BlockHash, None] +class MempoolBlock(TypedDict): + """ + Block info in a mempool.space like format for fee estimation. + + Attributes: + blockSize: Total serialized block size in bytes (witness + non-witness). + blockVSize: Total block virtual size in vbytes + nTx: Number of transactions in the projected block + totalFees: Total fees in satoshis + medianFee: Median fee rate in sat/vB + feeRange: Fee rate range: [min, 10%, 25%, 50%, 75%, 90%, max] + """ + blockSize: int + blockVSize: float + nTx: int + totalFees: Sats + medianFee: FeeRate + feeRange: List[FeeRate] + +class TxOut(TypedDict): + """ + Transaction output + + Attributes: + scriptpubkey: Script pubkey (locking script) + value: Value of the output in satoshis + """ + scriptpubkey: str + value: Sats + +class TxIn(TypedDict): + """ + Transaction input + + Attributes: + txid: Transaction ID of the output being spent + vout: Output index being spent (u16: coinbase is 65535, mempool.space uses u32: 4294967295) + prevout: Information about the previous output being spent + scriptsig: Signature script (hex, for non-SegWit inputs) + scriptsig_asm: Signature script in assembly format + witness: Witness data (stack items, present for SegWit inputs; hex-encoded on the wire) + is_coinbase: Whether this input is a coinbase (block reward) input + sequence: Input sequence number + inner_redeemscript_asm: Inner redeemscript in assembly (for P2SH-wrapped SegWit: scriptsig + witness both present) + inner_witnessscript_asm: Inner witnessscript in assembly (for P2WSH: last witness item decoded as script) + """ + txid: Txid + vout: Vout + prevout: Union[TxOut, None] + scriptsig: str + scriptsig_asm: str + witness: Witness + is_coinbase: bool + sequence: int + inner_redeemscript_asm: str + inner_witnessscript_asm: str + +class TxStatus(TypedDict): + """ + Transaction confirmation status + + Attributes: + confirmed: Whether the transaction is confirmed + block_height: Block height (only present if confirmed) + block_hash: Block hash (only present if confirmed) + block_time: Block timestamp (only present if confirmed) + """ + confirmed: bool + block_height: Union[Height, None] + block_hash: Union[BlockHash, None] + block_time: Union[Timestamp, None] + +class Transaction(TypedDict): + """ + Transaction information compatible with mempool.space API format + + Attributes: + index: Internal transaction index (brk-specific, not in mempool.space) + txid: Transaction ID + version: Transaction version (raw i32 from Bitcoin protocol, may contain non-standard values in coinbase txs) + locktime: Transaction lock time + vin: Transaction inputs + vout: Transaction outputs + size: Transaction size in bytes + weight: Transaction weight + sigops: Number of signature operations + fee: Transaction fee in satoshis + status: Confirmation status (confirmed, block height/hash/time) + """ + index: Union[TxIndex, None] + txid: Txid + version: TxVersionRaw + locktime: RawLockTime + vin: List[TxIn] + vout: List[TxOut] + size: int + weight: Weight + sigops: SigOps + fee: Sats + status: TxStatus + +class BlockTemplate(TypedDict): + """ + Projected next-block contents from Bitcoin Core's `getblocktemplate` + (block 0 of the snapshot). Returned by + `GET /api/v1/mining/block-template`. + + Attributes: + hash: Pass back as `` on +`/api/v1/mining/block-template/diff/{hash}` to fetch deltas. + stats: Aggregate stats for this block (size, vsize, fee range, ...). + transactions: Full transaction bodies in `getblocktemplate` order. + """ + hash: NextBlockHash + stats: MempoolBlock + transactions: List[Transaction] + +class BlockTemplateDiff(TypedDict): + """ + Delta between the current `getblocktemplate` projection and a prior + one identified by `since`. Returned by + `GET /api/v1/mining/block-template/diff/{hash}`. + + Attributes: + hash: Current next-block hash. Use as `since` on the next diff call. + since: Echoed prior hash the diff was computed against. + added: Full bodies of transactions that joined the projected next +block since `since`. + removed: Txids that left the projected next block since `since` +(confirmed, evicted, replaced, or pushed past block 0). + """ + hash: NextBlockHash + since: NextBlockHash + added: List[Transaction] + removed: List[Txid] + class BlockTimestamp(TypedDict): """ Block information returned for timestamp queries @@ -1022,25 +1163,6 @@ class LegacySeriesWithIndex(TypedDict): metric: SeriesName index: Index -class MempoolBlock(TypedDict): - """ - Block info in a mempool.space like format for fee estimation. - - Attributes: - blockSize: Total serialized block size in bytes (witness + non-witness). - blockVSize: Total block virtual size in vbytes - nTx: Number of transactions in the projected block - totalFees: Total fees in satoshis - medianFee: Median fee rate in sat/vB - feeRange: Fee rate range: [min, 10%, 25%, 50%, 75%, 90%, max] - """ - blockSize: int - blockVSize: float - nTx: int - totalFees: Sats - medianFee: FeeRate - feeRange: List[FeeRate] - class MempoolInfo(TypedDict): """ Mempool statistics with incrementally maintained fee histogram. @@ -1084,6 +1206,12 @@ class MerkleProof(TypedDict): merkle: List[str] pos: int +class NextBlockHashParam(TypedDict): + """ + `since` hash for `/api/v1/mining/block-template/diff/{hash}`. + """ + hash: NextBlockHash + class OHLCCents(TypedDict): """ OHLC (Open, High, Low, Close) data in cents @@ -1517,88 +1645,6 @@ class TimestampParam(TypedDict): """ timestamp: Timestamp -class TxOut(TypedDict): - """ - Transaction output - - Attributes: - scriptpubkey: Script pubkey (locking script) - value: Value of the output in satoshis - """ - scriptpubkey: str - value: Sats - -class TxIn(TypedDict): - """ - Transaction input - - Attributes: - txid: Transaction ID of the output being spent - vout: Output index being spent (u16: coinbase is 65535, mempool.space uses u32: 4294967295) - prevout: Information about the previous output being spent - scriptsig: Signature script (hex, for non-SegWit inputs) - scriptsig_asm: Signature script in assembly format - witness: Witness data (stack items, present for SegWit inputs; hex-encoded on the wire) - is_coinbase: Whether this input is a coinbase (block reward) input - sequence: Input sequence number - inner_redeemscript_asm: Inner redeemscript in assembly (for P2SH-wrapped SegWit: scriptsig + witness both present) - inner_witnessscript_asm: Inner witnessscript in assembly (for P2WSH: last witness item decoded as script) - """ - txid: Txid - vout: Vout - prevout: Union[TxOut, None] - scriptsig: str - scriptsig_asm: str - witness: Witness - is_coinbase: bool - sequence: int - inner_redeemscript_asm: str - inner_witnessscript_asm: str - -class TxStatus(TypedDict): - """ - Transaction confirmation status - - Attributes: - confirmed: Whether the transaction is confirmed - block_height: Block height (only present if confirmed) - block_hash: Block hash (only present if confirmed) - block_time: Block timestamp (only present if confirmed) - """ - confirmed: bool - block_height: Union[Height, None] - block_hash: Union[BlockHash, None] - block_time: Union[Timestamp, None] - -class Transaction(TypedDict): - """ - Transaction information compatible with mempool.space API format - - Attributes: - index: Internal transaction index (brk-specific, not in mempool.space) - txid: Transaction ID - version: Transaction version (raw i32 from Bitcoin protocol, may contain non-standard values in coinbase txs) - locktime: Transaction lock time - vin: Transaction inputs - vout: Transaction outputs - size: Transaction size in bytes - weight: Transaction weight - sigops: Number of signature operations - fee: Transaction fee in satoshis - status: Confirmation status (confirmed, block height/hash/time) - """ - index: Union[TxIndex, None] - txid: Txid - version: TxVersionRaw - locktime: RawLockTime - vin: List[TxIn] - vout: List[TxOut] - size: int - weight: Weight - sigops: SigOps - fee: Sats - status: TxStatus - class TxIndexParam(TypedDict): """ Transaction index path parameter @@ -8482,7 +8528,7 @@ class BrkClient(BrkClientBase): def get_mempool_blocks(self) -> List[MempoolBlock]: """Projected mempool blocks. - Get projected blocks from the mempool for fee estimation. + Projected blocks for fee estimation. Block 0 reflects Bitcoin Core's actual next-block selection; blocks 1+ are a fee-tier approximation. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)* @@ -8492,7 +8538,7 @@ class BrkClient(BrkClientBase): def get_recommended_fees(self) -> RecommendedFees: """Recommended fees. - Get recommended fee rates for different confirmation targets. + Recommended fee rates by confirmation target. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)* @@ -8502,7 +8548,7 @@ class BrkClient(BrkClientBase): def get_precise_fees(self) -> RecommendedFees: """Precise recommended fees. - Get recommended fee rates with up to 3 decimal places. + Recommended fee rates with sub-integer precision. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)* @@ -8519,10 +8565,10 @@ class BrkClient(BrkClientBase): Endpoint: `GET /api/mempool`""" return self.get_json('/api/mempool') - def get_mempool_hash(self) -> int: + def get_mempool_hash(self) -> NextBlockHash: """Mempool content hash. - Returns an opaque `u64` that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled. + Returns an opaque hash that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled. Endpoint: `GET /api/mempool/hash`""" return self.get_json('/api/mempool/hash') @@ -8567,6 +8613,22 @@ class BrkClient(BrkClientBase): Endpoint: `GET /api/v1/fullrbf/replacements`""" return self.get_json('/api/v1/fullrbf/replacements') + def get_block_template(self) -> BlockTemplate: + """Projected next block template. + + Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. + + Endpoint: `GET /api/v1/mempool/block-template`""" + return self.get_json('/api/v1/mempool/block-template') + + def get_block_template_diff(self, hash: NextBlockHash) -> BlockTemplateDiff: + """Block template diff since hash. + + Delta of the projected next block since ``. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. + + Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}`""" + return self.get_json(f'/api/v1/mempool/block-template/diff/{hash}') + def get_live_price(self) -> Dollars: """Live BTC/USD price.