diff --git a/Cargo.lock b/Cargo.lock index 4ecff97e4..075024d03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,7 +535,7 @@ dependencies = [ "brk_iterator", "brk_logger", "brk_mcp", - "brk_monitor", + "brk_mempool", "brk_query", "brk_reader", "brk_rpc", @@ -593,7 +593,7 @@ dependencies = [ "brk_indexer", "brk_iterator", "brk_logger", - "brk_monitor", + "brk_mempool", "brk_query", "brk_reader", "brk_rpc", @@ -733,7 +733,7 @@ dependencies = [ ] [[package]] -name = "brk_monitor" +name = "brk_mempool" version = "0.0.111" dependencies = [ "brk_error", @@ -755,12 +755,13 @@ dependencies = [ "brk_computer", "brk_error", "brk_indexer", - "brk_monitor", + "brk_mempool", "brk_reader", "brk_rpc", "brk_traversable", "brk_types", "derive_deref", + "jiff", "quickmatch", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2cdc79f69..93444b3db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ brk_query = { version = "0.0.111", path = "crates/brk_query", features = ["tokio brk_iterator = { version = "0.0.111", path = "crates/brk_iterator" } brk_logger = { version = "0.0.111", path = "crates/brk_logger" } brk_mcp = { version = "0.0.111", path = "crates/brk_mcp" } -brk_monitor = { version = "0.0.111", path = "crates/brk_monitor" } +brk_mempool = { version = "0.0.111", path = "crates/brk_mempool" } brk_reader = { version = "0.0.111", path = "crates/brk_reader" } brk_rpc = { version = "0.0.111", path = "crates/brk_rpc" } brk_server = { version = "0.0.111", path = "crates/brk_server" } diff --git a/crates/brk/Cargo.toml b/crates/brk/Cargo.toml index 0700c40be..9dccca879 100644 --- a/crates/brk/Cargo.toml +++ b/crates/brk/Cargo.toml @@ -21,7 +21,7 @@ full = [ "iterator", "logger", "mcp", - "monitor", + "mempool", "query", "reader", "rpc", @@ -41,7 +41,7 @@ indexer = ["brk_indexer"] iterator = ["brk_iterator"] logger = ["brk_logger"] mcp = ["brk_mcp"] -monitor = ["brk_monitor"] +mempool = ["brk_mempool"] query = ["brk_query"] reader = ["brk_reader"] rpc = ["brk_rpc"] @@ -62,7 +62,7 @@ brk_indexer = { workspace = true, optional = true } brk_iterator = { workspace = true, optional = true } brk_logger = { workspace = true, optional = true } brk_mcp = { workspace = true, optional = true } -brk_monitor = { workspace = true, optional = true } +brk_mempool = { workspace = true, optional = true } brk_query = { workspace = true, optional = true } brk_reader = { workspace = true, optional = true } brk_rpc = { workspace = true, optional = true } diff --git a/crates/brk/src/lib.rs b/crates/brk/src/lib.rs index 63b6c3d70..23d724a3c 100644 --- a/crates/brk/src/lib.rs +++ b/crates/brk/src/lib.rs @@ -44,9 +44,9 @@ pub use brk_logger as logger; #[doc(inline)] pub use brk_mcp as mcp; -#[cfg(feature = "monitor")] +#[cfg(feature = "mempool")] #[doc(inline)] -pub use brk_monitor as monitor; +pub use brk_mempool as mempool; #[cfg(feature = "query")] #[doc(inline)] diff --git a/crates/brk_cli/Cargo.toml b/crates/brk_cli/Cargo.toml index 0ca8d3b64..3c7d96ee4 100644 --- a/crates/brk_cli/Cargo.toml +++ b/crates/brk_cli/Cargo.toml @@ -16,7 +16,7 @@ brk_error = { workspace = true } brk_fetcher = { workspace = true } brk_indexer = { workspace = true } brk_iterator = { workspace = true } -brk_monitor = { workspace = true } +brk_mempool = { workspace = true } brk_query = { workspace = true } brk_logger = { workspace = true } brk_reader = { workspace = true } diff --git a/crates/brk_cli/src/main.rs b/crates/brk_cli/src/main.rs index f6fc9e10b..d884dcb48 100644 --- a/crates/brk_cli/src/main.rs +++ b/crates/brk_cli/src/main.rs @@ -14,7 +14,7 @@ use brk_computer::Computer; use brk_error::Result; use brk_indexer::Indexer; use brk_iterator::Blocks; -use brk_monitor::Mempool; +use brk_mempool::Mempool; use brk_query::AsyncQuery; use brk_reader::Reader; use brk_server::{Server, VERSION}; diff --git a/crates/brk_computer/src/indexes.rs b/crates/brk_computer/src/indexes.rs index c57807b2b..67afacc52 100644 --- a/crates/brk_computer/src/indexes.rs +++ b/crates/brk_computer/src/indexes.rs @@ -82,6 +82,7 @@ pub struct Vecs { pub txindex_to_input_count: EagerVec>, pub txindex_to_output_count: EagerVec>, pub txindex_to_txindex: LazyVecFrom1, + pub txinindex_to_txindex: EagerVec>, pub txinindex_to_txinindex: LazyVecFrom1, pub txinindex_to_txoutindex: EagerVec>, pub txoutindex_to_txoutindex: LazyVecFrom1, @@ -121,6 +122,7 @@ impl Vecs { } let this = Self { + txinindex_to_txindex: eager!("txindex"), txinindex_to_txoutindex: eager!("txoutindex"), txoutindex_to_txoutindex: lazy!("txoutindex", indexer.vecs.txout.txoutindex_to_value), txinindex_to_txinindex: lazy!("txinindex", indexer.vecs.txin.txinindex_to_outpoint), @@ -251,6 +253,13 @@ impl Vecs { // TxInIndex // --- + self.txinindex_to_txindex.compute_finer( + starting_indexes.txinindex, + &indexer.vecs.tx.txindex_to_first_txinindex, + &indexer.vecs.txin.txinindex_to_outpoint, + exit, + )?; + let txindex_to_first_txoutindex = &indexer.vecs.tx.txindex_to_first_txoutindex; let txindex_to_first_txoutindex_reader = txindex_to_first_txoutindex.create_reader(); self.txinindex_to_txoutindex.compute_transform( diff --git a/crates/brk_monitor/Cargo.toml b/crates/brk_mempool/Cargo.toml similarity index 83% rename from crates/brk_monitor/Cargo.toml rename to crates/brk_mempool/Cargo.toml index b2bece04c..cac0c637a 100644 --- a/crates/brk_monitor/Cargo.toml +++ b/crates/brk_mempool/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "brk_monitor" -description = "A Bitcoin mempool monitor with real-time synchronization" +name = "brk_mempool" +description = "Bitcoin mempool monitor with fee estimation" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/brk_monitor/DESIGN.md b/crates/brk_mempool/DESIGN.md similarity index 100% rename from crates/brk_monitor/DESIGN.md rename to crates/brk_mempool/DESIGN.md diff --git a/crates/brk_monitor/README.md b/crates/brk_mempool/README.md similarity index 100% rename from crates/brk_monitor/README.md rename to crates/brk_mempool/README.md diff --git a/crates/brk_monitor/build.rs b/crates/brk_mempool/build.rs similarity index 100% rename from crates/brk_monitor/build.rs rename to crates/brk_mempool/build.rs diff --git a/crates/brk_monitor/examples/mempool.rs b/crates/brk_mempool/examples/mempool.rs similarity index 98% rename from crates/brk_monitor/examples/mempool.rs rename to crates/brk_mempool/examples/mempool.rs index 64eac9915..78d9e26f2 100644 --- a/crates/brk_monitor/examples/mempool.rs +++ b/crates/brk_mempool/examples/mempool.rs @@ -1,7 +1,7 @@ use std::{thread, time::Duration}; use brk_error::Result; -use brk_monitor::Mempool; +use brk_mempool::Mempool; use brk_rpc::{Auth, Client}; fn main() -> Result<()> { diff --git a/crates/brk_monitor/src/mempool/addresses.rs b/crates/brk_mempool/src/addresses.rs similarity index 100% rename from crates/brk_monitor/src/mempool/addresses.rs rename to crates/brk_mempool/src/addresses.rs diff --git a/crates/brk_monitor/src/block_builder/graph.rs b/crates/brk_mempool/src/block_builder/graph.rs similarity index 81% rename from crates/brk_monitor/src/block_builder/graph.rs rename to crates/brk_mempool/src/block_builder/graph.rs index d3c18e91a..8bdb5a2cb 100644 --- a/crates/brk_monitor/src/block_builder/graph.rs +++ b/crates/brk_mempool/src/block_builder/graph.rs @@ -4,7 +4,7 @@ use brk_types::TxidPrefix; use rustc_hash::FxHashMap; use super::tx_node::TxNode; -use crate::mempool::Entry; +use crate::entry::Entry; use crate::types::{PoolIndex, TxIndex}; /// Type-safe wrapper around Vec that only allows PoolIndex access. @@ -84,12 +84,20 @@ pub fn build_graph(entries: &[Option]) -> Graph { }) .collect(); - // Build child relationships (reverse of parents) - for i in 0..nodes.len() { - let parents = nodes[i].parents.clone(); - for parent_idx in parents { - nodes[parent_idx.as_usize()].children.push(PoolIndex::from(i)); - } + // Collect parent->child edges (avoids cloning each node's parents) + let edges: Vec<(usize, PoolIndex)> = nodes + .iter() + .enumerate() + .flat_map(|(i, node)| { + node.parents + .iter() + .map(move |&p| (p.as_usize(), PoolIndex::from(i))) + }) + .collect(); + + // Build child relationships + for (parent_idx, child_idx) in edges { + nodes[parent_idx].children.push(child_idx); } Graph(nodes) diff --git a/crates/brk_monitor/src/block_builder/heap_entry.rs b/crates/brk_mempool/src/block_builder/heap_entry.rs similarity index 100% rename from crates/brk_monitor/src/block_builder/heap_entry.rs rename to crates/brk_mempool/src/block_builder/heap_entry.rs diff --git a/crates/brk_monitor/src/block_builder/mod.rs b/crates/brk_mempool/src/block_builder/mod.rs similarity index 97% rename from crates/brk_monitor/src/block_builder/mod.rs rename to crates/brk_mempool/src/block_builder/mod.rs index e333b7890..3bbcab541 100644 --- a/crates/brk_monitor/src/block_builder/mod.rs +++ b/crates/brk_mempool/src/block_builder/mod.rs @@ -13,7 +13,7 @@ mod partitioner; mod selector; mod tx_node; -use crate::mempool::Entry; +use crate::entry::Entry; use crate::types::SelectedTx; /// Target vsize per block (~1MB, derived from 4MW weight limit). diff --git a/crates/brk_monitor/src/block_builder/package.rs b/crates/brk_mempool/src/block_builder/package.rs similarity index 100% rename from crates/brk_monitor/src/block_builder/package.rs rename to crates/brk_mempool/src/block_builder/package.rs diff --git a/crates/brk_monitor/src/block_builder/partitioner.rs b/crates/brk_mempool/src/block_builder/partitioner.rs similarity index 100% rename from crates/brk_monitor/src/block_builder/partitioner.rs rename to crates/brk_mempool/src/block_builder/partitioner.rs diff --git a/crates/brk_monitor/src/block_builder/selector.rs b/crates/brk_mempool/src/block_builder/selector.rs similarity index 91% rename from crates/brk_monitor/src/block_builder/selector.rs rename to crates/brk_mempool/src/block_builder/selector.rs index c549750a5..2777ba801 100644 --- a/crates/brk_monitor/src/block_builder/selector.rs +++ b/crates/brk_mempool/src/block_builder/selector.rs @@ -2,6 +2,7 @@ use std::collections::BinaryHeap; use brk_types::FeeRate; use rustc_hash::FxHashSet; +use smallvec::SmallVec; use super::graph::Graph; use super::heap_entry::HeapEntry; @@ -53,9 +54,9 @@ pub fn select_packages(graph: &mut Graph, num_blocks: usize) -> Vec { } /// Select a tx and all its unselected ancestors in topological order. -fn select_with_ancestors(graph: &mut Graph, pool_idx: PoolIndex) -> Vec { - let mut result: Vec = Vec::new(); - let mut stack: Vec<(PoolIndex, bool)> = vec![(pool_idx, false)]; +fn select_with_ancestors(graph: &mut Graph, pool_idx: PoolIndex) -> SmallVec<[PoolIndex; 8]> { + let mut result: SmallVec<[PoolIndex; 8]> = SmallVec::new(); + let mut stack: SmallVec<[(PoolIndex, bool); 16]> = smallvec::smallvec![(pool_idx, false)]; while let Some((idx, parents_done)) = stack.pop() { if graph[idx].selected { @@ -85,7 +86,7 @@ fn update_descendants(graph: &mut Graph, selected_idx: PoolIndex, heap: &mut Bin // Track visited to avoid double-updates in diamond patterns let mut visited: FxHashSet = FxHashSet::default(); - let mut stack: Vec = graph[selected_idx].children.to_vec(); + let mut stack: SmallVec<[PoolIndex; 16]> = graph[selected_idx].children.iter().copied().collect(); while let Some(child_idx) = stack.pop() { if !visited.insert(child_idx) { diff --git a/crates/brk_monitor/src/block_builder/tx_node.rs b/crates/brk_mempool/src/block_builder/tx_node.rs similarity index 100% rename from crates/brk_monitor/src/block_builder/tx_node.rs rename to crates/brk_mempool/src/block_builder/tx_node.rs diff --git a/crates/brk_monitor/src/mempool/entry.rs b/crates/brk_mempool/src/entry.rs similarity index 92% rename from crates/brk_monitor/src/mempool/entry.rs rename to crates/brk_mempool/src/entry.rs index ad525a370..20ebb78de 100644 --- a/crates/brk_monitor/src/mempool/entry.rs +++ b/crates/brk_mempool/src/entry.rs @@ -1,4 +1,5 @@ use brk_types::{FeeRate, MempoolEntryInfo, Sats, Txid, TxidPrefix, VSize}; +use smallvec::SmallVec; /// A mempool transaction entry. /// @@ -13,8 +14,8 @@ pub struct Entry { pub ancestor_fee: Sats, /// Pre-computed ancestor vsize (self + all ancestors, no double-counting) pub ancestor_vsize: VSize, - /// Parent txid prefixes (transactions this tx depends on) - pub depends: Vec, + /// Parent txid prefixes (most txs have 0-2 parents) + pub depends: SmallVec<[TxidPrefix; 2]>, } impl Entry { diff --git a/crates/brk_mempool/src/entry_pool.rs b/crates/brk_mempool/src/entry_pool.rs new file mode 100644 index 000000000..d2f327ce2 --- /dev/null +++ b/crates/brk_mempool/src/entry_pool.rs @@ -0,0 +1,51 @@ +use brk_types::TxidPrefix; +use rustc_hash::FxHashMap; + +use crate::entry::Entry; +use crate::types::TxIndex; + +/// Pool of mempool entries with slot recycling. +/// +/// Uses a slot-based storage where removed entries leave holes +/// that get reused for new entries, avoiding index invalidation. +#[derive(Default)] +pub struct EntryPool { + entries: Vec>, + prefix_to_idx: FxHashMap, + free_slots: Vec, +} + +impl EntryPool { + /// Insert an entry, returning its index. + pub fn insert(&mut self, prefix: TxidPrefix, entry: Entry) -> TxIndex { + let idx = match self.free_slots.pop() { + Some(idx) => { + self.entries[idx.as_usize()] = Some(entry); + idx + } + None => { + let idx = TxIndex::from(self.entries.len()); + self.entries.push(Some(entry)); + idx + } + }; + + self.prefix_to_idx.insert(prefix, idx); + idx + } + + /// Remove an entry by its txid prefix. + pub fn remove(&mut self, prefix: &TxidPrefix) { + if let Some(idx) = self.prefix_to_idx.remove(prefix) { + if let Some(slot) = self.entries.get_mut(idx.as_usize()) { + *slot = None; + } + self.free_slots.push(idx); + } + } + + /// Get the entries slice for block building. + pub fn entries(&self) -> &[Option] { + &self.entries + } +} diff --git a/crates/brk_monitor/src/lib.rs b/crates/brk_mempool/src/lib.rs similarity index 63% rename from crates/brk_monitor/src/lib.rs rename to crates/brk_mempool/src/lib.rs index de567c624..9d213c49b 100644 --- a/crates/brk_monitor/src/lib.rs +++ b/crates/brk_mempool/src/lib.rs @@ -1,14 +1,18 @@ -//! Bitcoin mempool monitor. +//! Bitcoin mempool monitor with fee estimation. //! //! Provides real-time mempool tracking with: //! - Fee estimation via projected blocks //! - Address mempool stats //! - CPFP-aware block building +mod addresses; mod block_builder; -mod mempool; +mod entry; +mod entry_pool; mod projected_blocks; +mod sync; +mod tx_store; mod types; -pub use mempool::{Mempool, MempoolInner}; pub use projected_blocks::{BlockStats, RecommendedFees, Snapshot}; +pub use sync::{Mempool, MempoolInner}; diff --git a/crates/brk_monitor/src/projected_blocks/fees.rs b/crates/brk_mempool/src/projected_blocks/fees.rs similarity index 100% rename from crates/brk_monitor/src/projected_blocks/fees.rs rename to crates/brk_mempool/src/projected_blocks/fees.rs diff --git a/crates/brk_monitor/src/projected_blocks/mod.rs b/crates/brk_mempool/src/projected_blocks/mod.rs similarity index 100% rename from crates/brk_monitor/src/projected_blocks/mod.rs rename to crates/brk_mempool/src/projected_blocks/mod.rs diff --git a/crates/brk_monitor/src/projected_blocks/snapshot.rs b/crates/brk_mempool/src/projected_blocks/snapshot.rs similarity index 97% rename from crates/brk_monitor/src/projected_blocks/snapshot.rs rename to crates/brk_mempool/src/projected_blocks/snapshot.rs index 56b127ce3..622ee8ea8 100644 --- a/crates/brk_monitor/src/projected_blocks/snapshot.rs +++ b/crates/brk_mempool/src/projected_blocks/snapshot.rs @@ -2,7 +2,7 @@ use brk_types::RecommendedFees; use super::fees; use super::stats::{self, BlockStats}; -use crate::mempool::Entry; +use crate::entry::Entry; use crate::types::{SelectedTx, TxIndex}; /// Immutable snapshot of projected blocks. diff --git a/crates/brk_monitor/src/projected_blocks/stats.rs b/crates/brk_mempool/src/projected_blocks/stats.rs similarity index 97% rename from crates/brk_monitor/src/projected_blocks/stats.rs rename to crates/brk_mempool/src/projected_blocks/stats.rs index 1495338e4..7c7019a81 100644 --- a/crates/brk_monitor/src/projected_blocks/stats.rs +++ b/crates/brk_mempool/src/projected_blocks/stats.rs @@ -1,6 +1,6 @@ use brk_types::{FeeRate, Sats, VSize}; -use crate::mempool::Entry; +use crate::entry::Entry; use crate::types::SelectedTx; /// Statistics for a single projected block. @@ -45,7 +45,7 @@ pub fn compute_block_stats(selected: &[SelectedTx], entries: &[Option]) - } } - fee_rates.sort(); + fee_rates.sort_unstable(); BlockStats { tx_count: selected.len() as u32, diff --git a/crates/brk_monitor/src/mempool/monitor.rs b/crates/brk_mempool/src/sync.rs similarity index 67% rename from crates/brk_monitor/src/mempool/monitor.rs rename to crates/brk_mempool/src/sync.rs index 73fb4d0a4..112cb6ac9 100644 --- a/crates/brk_monitor/src/mempool/monitor.rs +++ b/crates/brk_mempool/src/sync.rs @@ -13,13 +13,14 @@ use brk_types::{MempoolEntryInfo, MempoolInfo, TxWithHex, Txid, TxidPrefix}; use derive_deref::Deref; use log::{error, info}; use parking_lot::{RwLock, RwLockReadGuard}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; -use super::addresses::AddressTracker; -use super::entry::Entry; +use crate::addresses::AddressTracker; use crate::block_builder::build_projected_blocks; +use crate::entry::Entry; +use crate::entry_pool::EntryPool; use crate::projected_blocks::{BlockStats, RecommendedFees, Snapshot}; -use crate::types::TxIndex; +use crate::tx_store::TxStore; /// Max new txs to fetch full data for per update cycle (for address tracking). const MAX_TX_FETCHES_PER_CYCLE: usize = 10_000; @@ -27,17 +28,6 @@ const MAX_TX_FETCHES_PER_CYCLE: usize = 10_000; /// Minimum interval between rebuilds (milliseconds). const MIN_REBUILD_INTERVAL_MS: u64 = 1000; -/// Block building state - grouped for atomic locking. -#[derive(Default)] -struct BlockBuildingState { - /// Slot-based entry storage - entries: Vec>, - /// TxidPrefix -> slot index - txid_prefix_to_idx: FxHashMap, - /// Recycled slot indices - free_indices: Vec, -} - /// Mempool monitor. /// /// Thread-safe wrapper around `MempoolInner`. Free to clone. @@ -56,11 +46,9 @@ pub struct MempoolInner { // Mempool state info: RwLock, - txs: RwLock>, + txs: RwLock, addresses: RwLock, - - // Block building data (single lock for consistency) - block_state: RwLock, + entries: RwLock, // Projected blocks snapshot snapshot: RwLock, @@ -75,9 +63,9 @@ impl MempoolInner { Self { client, info: RwLock::new(MempoolInfo::default()), - txs: RwLock::new(FxHashMap::default()), + txs: RwLock::new(TxStore::default()), addresses: RwLock::new(AddressTracker::default()), - block_state: RwLock::new(BlockBuildingState::default()), + entries: RwLock::new(EntryPool::default()), snapshot: RwLock::new(Snapshot::default()), dirty: AtomicBool::new(false), last_rebuild_ms: AtomicU64::new(0), @@ -100,7 +88,7 @@ impl MempoolInner { self.snapshot.read().block_stats.clone() } - pub fn get_txs(&self) -> RwLockReadGuard<'_, FxHashMap> { + pub fn get_txs(&self) -> RwLockReadGuard<'_, TxStore> { self.txs.read() } @@ -122,9 +110,7 @@ impl MempoolInner { pub fn update(&self) -> Result<()> { let entries_info = self.client.get_raw_mempool_verbose()?; - let current_txids: FxHashSet = entries_info.iter().map(|e| e.txid.clone()).collect(); - - let new_txs = self.fetch_new_txs(¤t_txids); + let new_txs = self.fetch_new_txs(&entries_info); let has_changes = self.apply_changes(&entries_info, new_txs); if has_changes { @@ -137,12 +123,13 @@ impl MempoolInner { } /// Fetch full transaction data for new txids (needed for address tracking). - fn fetch_new_txs(&self, current_txids: &FxHashSet) -> FxHashMap { + fn fetch_new_txs(&self, entries_info: &[MempoolEntryInfo]) -> FxHashMap { let txids_to_fetch: Vec = { let txs = self.txs.read(); - current_txids + entries_info .iter() - .filter(|txid| !txs.contains_key(*txid)) + .map(|e| &e.txid) + .filter(|txid| !txs.contains(txid)) .take(MAX_TX_FETCHES_PER_CYCLE) .cloned() .collect() @@ -165,7 +152,7 @@ impl MempoolInner { entries_info: &[MempoolEntryInfo], new_txs: FxHashMap, ) -> bool { - let current_entries: FxHashMap = entries_info + let entries_by_prefix: FxHashMap = entries_info .iter() .map(|e| (TxidPrefix::from(&e.txid), e)) .collect(); @@ -173,58 +160,37 @@ impl MempoolInner { let mut info = self.info.write(); let mut txs = self.txs.write(); let mut addresses = self.addresses.write(); - let mut block_state = self.block_state.write(); + let mut entries = self.entries.write(); let mut had_removals = false; let had_additions = !new_txs.is_empty(); // Remove transactions no longer in mempool - txs.retain(|txid, tx_with_hex| { - let prefix = TxidPrefix::from(txid); - if current_entries.contains_key(&prefix) { - return true; - } + txs.retain_or_remove( + |txid| entries_by_prefix.contains_key(&TxidPrefix::from(txid)), + |txid, tx_with_hex| { + had_removals = true; + let tx = tx_with_hex.tx(); + let prefix = TxidPrefix::from(txid); - had_removals = true; - let tx = tx_with_hex.tx(); - - info.remove(tx); - addresses.remove_tx(tx, txid); - - if let Some(idx) = block_state.txid_prefix_to_idx.remove(&prefix) { - if let Some(slot) = block_state.entries.get_mut(idx.as_usize()) { - *slot = None; - } - block_state.free_indices.push(idx); - } - - false - }); + info.remove(tx); + addresses.remove_tx(tx, txid); + entries.remove(&prefix); + }, + ); // Add new transactions for (txid, tx_with_hex) in &new_txs { let tx = tx_with_hex.tx(); let prefix = TxidPrefix::from(txid); - let Some(entry_info) = current_entries.get(&prefix) else { + let Some(entry_info) = entries_by_prefix.get(&prefix) else { continue; }; - let entry = Entry::from_info(entry_info); - info.add(tx); addresses.add_tx(tx, txid); - - let idx = if let Some(idx) = block_state.free_indices.pop() { - block_state.entries[idx.as_usize()] = Some(entry); - idx - } else { - let idx = TxIndex::from(block_state.entries.len()); - block_state.entries.push(Some(entry)); - idx - }; - - block_state.txid_prefix_to_idx.insert(prefix, idx); + entries.insert(prefix, Entry::from_info(entry_info)); } txs.extend(new_txs); @@ -264,10 +230,11 @@ impl MempoolInner { /// Rebuild projected blocks snapshot. fn rebuild_projected_blocks(&self) { - let block_state = self.block_state.read(); + let entries = self.entries.read(); + let entries_slice = entries.entries(); - let blocks = build_projected_blocks(&block_state.entries); - let snapshot = Snapshot::build(blocks, &block_state.entries); + let blocks = build_projected_blocks(entries_slice); + let snapshot = Snapshot::build(blocks, entries_slice); *self.snapshot.write() = snapshot; } diff --git a/crates/brk_mempool/src/tx_store.rs b/crates/brk_mempool/src/tx_store.rs new file mode 100644 index 000000000..726582cfc --- /dev/null +++ b/crates/brk_mempool/src/tx_store.rs @@ -0,0 +1,44 @@ +use std::ops::Deref; + +use brk_types::{TxWithHex, Txid}; +use rustc_hash::FxHashMap; + +/// Store of full transaction data for API access. +#[derive(Default)] +pub struct TxStore(FxHashMap); + +impl Deref for TxStore { + type Target = FxHashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TxStore { + /// Check if a transaction exists. + pub fn contains(&self, txid: &Txid) -> bool { + self.0.contains_key(txid) + } + + /// Add transactions in bulk. + pub fn extend(&mut self, txs: FxHashMap) { + self.0.extend(txs); + } + + /// Keep items matching predicate, call `on_remove` for each removed item. + pub fn retain_or_remove(&mut self, mut keep: K, mut on_remove: R) + where + K: FnMut(&Txid) -> bool, + R: FnMut(&Txid, &TxWithHex), + { + self.0.retain(|txid, tx| { + if keep(txid) { + true + } else { + on_remove(txid, tx); + false + } + }); + } +} diff --git a/crates/brk_monitor/src/types/mod.rs b/crates/brk_mempool/src/types/mod.rs similarity index 100% rename from crates/brk_monitor/src/types/mod.rs rename to crates/brk_mempool/src/types/mod.rs diff --git a/crates/brk_monitor/src/types/pool_index.rs b/crates/brk_mempool/src/types/pool_index.rs similarity index 100% rename from crates/brk_monitor/src/types/pool_index.rs rename to crates/brk_mempool/src/types/pool_index.rs diff --git a/crates/brk_monitor/src/types/selected_tx.rs b/crates/brk_mempool/src/types/selected_tx.rs similarity index 100% rename from crates/brk_monitor/src/types/selected_tx.rs rename to crates/brk_mempool/src/types/selected_tx.rs diff --git a/crates/brk_monitor/src/types/tx_index.rs b/crates/brk_mempool/src/types/tx_index.rs similarity index 100% rename from crates/brk_monitor/src/types/tx_index.rs rename to crates/brk_mempool/src/types/tx_index.rs diff --git a/crates/brk_monitor/src/mempool/mod.rs b/crates/brk_monitor/src/mempool/mod.rs deleted file mode 100644 index acbd315ae..000000000 --- a/crates/brk_monitor/src/mempool/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod addresses; -mod entry; -mod monitor; - -pub use entry::Entry; -pub use monitor::{Mempool, MempoolInner}; diff --git a/crates/brk_query/Cargo.toml b/crates/brk_query/Cargo.toml index 609f5a2ce..9f0fb7cea 100644 --- a/crates/brk_query/Cargo.toml +++ b/crates/brk_query/Cargo.toml @@ -16,12 +16,13 @@ bitcoin = { workspace = true } brk_computer = { workspace = true } brk_error = { workspace = true } brk_indexer = { workspace = true } -brk_monitor = { workspace = true } +brk_mempool = { workspace = true } brk_reader = { workspace = true } brk_rpc = { workspace = true } brk_traversable = { workspace = true } brk_types = { workspace = true } derive_deref = { workspace = true } +jiff = { workspace = true } # quickmatch = { path = "../../../quickmatch" } quickmatch = "0.1.8" schemars = { workspace = true } diff --git a/crates/brk_query/src/async.rs b/crates/brk_query/src/async.rs index 4f5da9930..2d228b4ae 100644 --- a/crates/brk_query/src/async.rs +++ b/crates/brk_query/src/async.rs @@ -3,11 +3,12 @@ use std::collections::BTreeMap; use brk_computer::Computer; use brk_error::Result; use brk_indexer::Indexer; -use brk_monitor::Mempool; +use brk_mempool::Mempool; use brk_reader::Reader; use brk_types::{ - Address, AddressStats, BlockInfo, BlockStatus, Height, Index, IndexInfo, Limit, MempoolInfo, - Metric, MetricCount, RecommendedFees, Transaction, TreeNode, TxStatus, Txid, TxidPath, Utxo, + Address, AddressStats, BlockInfo, BlockStatus, BlockTimestamp, DifficultyAdjustment, Height, + Index, IndexInfo, Limit, MempoolBlock, MempoolInfo, Metric, MetricCount, RecommendedFees, + Timestamp, Transaction, TreeNode, TxOutspend, TxStatus, Txid, TxidPath, Utxo, Vout, }; use tokio::task::spawn_blocking; @@ -57,6 +58,11 @@ impl AsyncQuery { spawn_blocking(move || query.get_address_utxos(address)).await? } + pub async fn get_address_mempool_txids(&self, address: Address) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_address_mempool_txids(address)).await? + } + pub async fn get_transaction(&self, txid: TxidPath) -> Result { let query = self.0.clone(); spawn_blocking(move || query.get_transaction(txid)).await? @@ -72,6 +78,16 @@ impl AsyncQuery { spawn_blocking(move || query.get_transaction_hex(txid)).await? } + pub async fn get_tx_outspend(&self, txid: TxidPath, vout: Vout) -> Result { + let query = self.0.clone(); + spawn_blocking(move || query.get_tx_outspend(txid, vout)).await? + } + + pub async fn get_tx_outspends(&self, txid: TxidPath) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_tx_outspends(txid)).await? + } + pub async fn get_block(&self, hash: String) -> Result { let query = self.0.clone(); spawn_blocking(move || query.get_block(&hash)).await? @@ -82,6 +98,10 @@ impl AsyncQuery { spawn_blocking(move || query.get_block_by_height(height)).await? } + pub async fn get_block_by_timestamp(&self, timestamp: Timestamp) -> Result { + self.0.get_block_by_timestamp(timestamp) + } + pub async fn get_block_status(&self, hash: String) -> Result { let query = self.0.clone(); spawn_blocking(move || query.get_block_status(&hash)).await? @@ -97,6 +117,20 @@ impl AsyncQuery { spawn_blocking(move || query.get_block_txids(&hash)).await? } + pub async fn get_block_txs(&self, hash: String, start_index: usize) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_block_txs(&hash, start_index)).await? + } + + pub async fn get_block_txid_at_index(&self, hash: String, index: usize) -> Result { + self.0.get_block_txid_at_index(&hash, index) + } + + pub async fn get_block_raw(&self, hash: String) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_block_raw(&hash)).await? + } + pub async fn get_mempool_info(&self) -> Result { self.0.get_mempool_info() } @@ -109,6 +143,15 @@ impl AsyncQuery { self.0.get_recommended_fees() } + pub async fn get_mempool_blocks(&self) -> Result> { + self.0.get_mempool_blocks() + } + + pub async fn get_difficulty_adjustment(&self) -> Result { + let query = self.0.clone(); + spawn_blocking(move || query.get_difficulty_adjustment()).await? + } + pub async fn match_metric(&self, metric: Metric, limit: Limit) -> Result> { let query = self.0.clone(); spawn_blocking(move || Ok(query.match_metric(&metric, limit))).await? diff --git a/crates/brk_query/src/chain/addr/mempool_txids.rs b/crates/brk_query/src/chain/addr/mempool_txids.rs new file mode 100644 index 000000000..4a5015b3b --- /dev/null +++ b/crates/brk_query/src/chain/addr/mempool_txids.rs @@ -0,0 +1,24 @@ +use std::str::FromStr; + +use brk_error::{Error, Result}; +use brk_types::{Address, AddressBytes, Txid}; + +use crate::Query; + +/// Maximum number of mempool txids to return +const MAX_MEMPOOL_TXIDS: usize = 50; + +/// Get mempool transaction IDs for an address +pub fn get_address_mempool_txids(address: Address, query: &Query) -> Result> { + let mempool = query.mempool().ok_or(Error::Str("Mempool not available"))?; + + let bytes = AddressBytes::from_str(&address.address)?; + let addresses = mempool.get_addresses(); + + let txids: Vec = addresses + .get(&bytes) + .map(|(_, txids)| txids.iter().take(MAX_MEMPOOL_TXIDS).cloned().collect()) + .unwrap_or_default(); + + Ok(txids) +} diff --git a/crates/brk_query/src/chain/addr/mod.rs b/crates/brk_query/src/chain/addr/mod.rs index fef10e4a1..fbbbc124d 100644 --- a/crates/brk_query/src/chain/addr/mod.rs +++ b/crates/brk_query/src/chain/addr/mod.rs @@ -1,8 +1,12 @@ mod addr; +mod mempool_txids; mod resolve; mod txids; mod utxos; +mod validate; pub use addr::*; +pub use mempool_txids::*; pub use txids::*; pub use utxos::*; +pub use validate::*; diff --git a/crates/brk_query/src/chain/addr/validate.rs b/crates/brk_query/src/chain/addr/validate.rs new file mode 100644 index 000000000..948d2db16 --- /dev/null +++ b/crates/brk_query/src/chain/addr/validate.rs @@ -0,0 +1,41 @@ +use bitcoin::hex::DisplayHex; +use brk_types::{AddressBytes, AddressValidation, OutputType}; + +/// Validate a Bitcoin address and return details +pub fn validate_address(address: &str) -> AddressValidation { + let Ok(script) = AddressBytes::address_to_script(address) else { + return AddressValidation::invalid(); + }; + + let output_type = OutputType::from(&script); + let script_hex = script.as_bytes().to_lower_hex_string(); + + let is_script = matches!(output_type, OutputType::P2SH); + let is_witness = matches!( + output_type, + OutputType::P2WPKH | OutputType::P2WSH | OutputType::P2TR | OutputType::P2A + ); + + let (witness_version, witness_program) = if is_witness { + let version = script.witness_version().map(|v| v.to_num()); + // Witness program is after the version byte and push opcode + let program = if script.len() > 2 { + Some(script.as_bytes()[2..].to_lower_hex_string()) + } else { + None + }; + (version, program) + } else { + (None, None) + }; + + AddressValidation { + isvalid: true, + address: Some(address.to_string()), + script_pub_key: Some(script_hex), + isscript: Some(is_script), + iswitness: Some(is_witness), + witness_version, + witness_program, + } +} diff --git a/crates/brk_query/src/chain/block/by_timestamp.rs b/crates/brk_query/src/chain/block/by_timestamp.rs new file mode 100644 index 000000000..1ad5d772c --- /dev/null +++ b/crates/brk_query/src/chain/block/by_timestamp.rs @@ -0,0 +1,75 @@ +use brk_error::{Error, Result}; +use brk_types::{BlockTimestamp, Date, DateIndex, Height, Timestamp}; +use jiff::Timestamp as JiffTimestamp; +use vecdb::{AnyVec, GenericStoredVec, TypedVecIterator}; + +use crate::Query; + +/// Get the block closest to a given timestamp using dateindex for fast lookup +pub fn get_block_by_timestamp(timestamp: Timestamp, query: &Query) -> Result { + let indexer = query.indexer(); + let computer = query.computer(); + + let max_height = query.get_height(); + let max_height_usize: usize = max_height.into(); + + if max_height_usize == 0 { + return Err(Error::Str("No blocks indexed")); + } + + let target = timestamp; + let date = Date::from(target); + let dateindex = DateIndex::try_from(date).unwrap_or_default(); + + // Get first height of the target date + let first_height_of_day = computer + .indexes + .dateindex_to_first_height + .read_once(dateindex) + .unwrap_or(Height::from(0usize)); + + let start: usize = usize::from(first_height_of_day).min(max_height_usize); + + // Use iterator for efficient sequential access + let mut timestamp_iter = indexer.vecs.block.height_to_timestamp.iter()?; + + // Search forward from start to find the last block <= target timestamp + let mut best_height = start; + let mut best_ts = timestamp_iter.get_unwrap(Height::from(start)); + + for h in (start + 1)..=max_height_usize { + let height = Height::from(h); + let block_ts = timestamp_iter.get_unwrap(height); + if block_ts <= target { + best_height = h; + best_ts = block_ts; + } else { + break; + } + } + + // Check one block before start in case we need to go backward + if start > 0 && best_ts > target { + let prev_height = Height::from(start - 1); + let prev_ts = timestamp_iter.get_unwrap(prev_height); + if prev_ts <= target { + best_height = start - 1; + best_ts = prev_ts; + } + } + + let height = Height::from(best_height); + let blockhash = indexer.vecs.block.height_to_blockhash.iter()?.get_unwrap(height); + + // Convert timestamp to ISO 8601 format + let ts_secs: i64 = (*best_ts).into(); + let iso_timestamp = JiffTimestamp::from_second(ts_secs) + .map(|t| t.to_string()) + .unwrap_or_else(|_| best_ts.to_string()); + + Ok(BlockTimestamp { + height, + hash: blockhash, + timestamp: iso_timestamp, + }) +} diff --git a/crates/brk_query/src/chain/block/mod.rs b/crates/brk_query/src/chain/block/mod.rs index a29c9ed18..ef6def8d3 100644 --- a/crates/brk_query/src/chain/block/mod.rs +++ b/crates/brk_query/src/chain/block/mod.rs @@ -1,11 +1,19 @@ +mod by_timestamp; mod height_by_hash; mod info; mod list; +mod raw; mod status; +mod txid_at_index; mod txids; +mod txs; +pub use by_timestamp::*; pub use height_by_hash::*; pub use info::*; pub use list::*; +pub use raw::*; pub use status::*; +pub use txid_at_index::*; pub use txids::*; +pub use txs::*; diff --git a/crates/brk_query/src/chain/block/raw.rs b/crates/brk_query/src/chain/block/raw.rs new file mode 100644 index 000000000..62c6be7c6 --- /dev/null +++ b/crates/brk_query/src/chain/block/raw.rs @@ -0,0 +1,29 @@ +use brk_error::{Error, Result}; +use brk_types::Height; +use vecdb::{AnyVec, GenericStoredVec}; + +use crate::Query; + +/// Get raw block bytes by height +pub fn get_block_raw(height: Height, query: &Query) -> Result> { + let indexer = query.indexer(); + let computer = query.computer(); + let reader = query.reader(); + + let max_height = Height::from( + indexer + .vecs + .block + .height_to_blockhash + .len() + .saturating_sub(1), + ); + if height > max_height { + return Err(Error::Str("Block height out of range")); + } + + let position = computer.blks.height_to_position.read_once(height)?; + let size = indexer.vecs.block.height_to_total_size.read_once(height)?; + + reader.read_raw_bytes(position, *size as usize) +} diff --git a/crates/brk_query/src/chain/block/txid_at_index.rs b/crates/brk_query/src/chain/block/txid_at_index.rs new file mode 100644 index 000000000..355785df8 --- /dev/null +++ b/crates/brk_query/src/chain/block/txid_at_index.rs @@ -0,0 +1,36 @@ +use brk_error::{Error, Result}; +use brk_types::{Height, TxIndex, Txid}; +use vecdb::{AnyVec, GenericStoredVec, TypedVecIterator}; + +use crate::Query; + +/// Get a single txid at a specific index within a block +pub fn get_block_txid_at_index(height: Height, index: usize, query: &Query) -> Result { + let indexer = query.indexer(); + + let max_height = query.get_height(); + if height > max_height { + return Err(Error::Str("Block height out of range")); + } + + let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?; + let next_first_txindex = indexer + .vecs + .tx + .height_to_first_txindex + .read_once(height.incremented()) + .unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len())); + + let first: usize = first_txindex.into(); + let next: usize = next_first_txindex.into(); + let tx_count = next - first; + + if index >= tx_count { + return Err(Error::Str("Transaction index out of range")); + } + + let txindex = TxIndex::from(first + index); + let txid = indexer.vecs.tx.txindex_to_txid.iter()?.get_unwrap(txindex); + + Ok(txid) +} diff --git a/crates/brk_query/src/chain/block/txs.rs b/crates/brk_query/src/chain/block/txs.rs new file mode 100644 index 000000000..790f5c77b --- /dev/null +++ b/crates/brk_query/src/chain/block/txs.rs @@ -0,0 +1,45 @@ +use brk_error::{Error, Result}; +use brk_types::{Height, Transaction, TxIndex}; +use vecdb::{AnyVec, GenericStoredVec}; + +use crate::{Query, chain::tx::get_transaction_by_index}; + +pub const BLOCK_TXS_PAGE_SIZE: usize = 25; + +/// Get paginated transactions in a block by height +pub fn get_block_txs(height: Height, start_index: usize, query: &Query) -> Result> { + let indexer = query.indexer(); + + let max_height = query.get_height(); + if height > max_height { + return Err(Error::Str("Block height out of range")); + } + + let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?; + let next_first_txindex = indexer + .vecs + .tx + .height_to_first_txindex + .read_once(height.incremented()) + .unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len())); + + let first: usize = first_txindex.into(); + let next: usize = next_first_txindex.into(); + let tx_count = next - first; + + if start_index >= tx_count { + return Ok(Vec::new()); + } + + let end_index = (start_index + BLOCK_TXS_PAGE_SIZE).min(tx_count); + let count = end_index - start_index; + + let mut txs = Vec::with_capacity(count); + for i in start_index..end_index { + let txindex = TxIndex::from(first + i); + let tx = get_transaction_by_index(txindex, query)?; + txs.push(tx); + } + + Ok(txs) +} diff --git a/crates/brk_query/src/chain/mempool/blocks.rs b/crates/brk_query/src/chain/mempool/blocks.rs new file mode 100644 index 000000000..6f0dc4186 --- /dev/null +++ b/crates/brk_query/src/chain/mempool/blocks.rs @@ -0,0 +1,20 @@ +use brk_error::{Error, Result}; +use brk_types::MempoolBlock; + +use crate::Query; + +/// Get projected mempool blocks for fee estimation +pub fn get_mempool_blocks(query: &Query) -> Result> { + let mempool = query.mempool().ok_or(Error::Str("Mempool not available"))?; + + let block_stats = mempool.get_block_stats(); + + let blocks = block_stats + .into_iter() + .map(|stats| { + MempoolBlock::new(stats.tx_count, stats.total_vsize, stats.total_fee, stats.fee_range) + }) + .collect(); + + Ok(blocks) +} diff --git a/crates/brk_query/src/chain/mempool/mod.rs b/crates/brk_query/src/chain/mempool/mod.rs index 4b2ff80e4..03ee1d197 100644 --- a/crates/brk_query/src/chain/mempool/mod.rs +++ b/crates/brk_query/src/chain/mempool/mod.rs @@ -1,7 +1,9 @@ +mod blocks; mod fees; mod info; mod txids; +pub use blocks::*; pub use fees::*; pub use info::*; pub use txids::*; diff --git a/crates/brk_query/src/chain/mining/difficulty.rs b/crates/brk_query/src/chain/mining/difficulty.rs new file mode 100644 index 000000000..12795bb43 --- /dev/null +++ b/crates/brk_query/src/chain/mining/difficulty.rs @@ -0,0 +1,120 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use brk_error::Result; +use brk_types::{DifficultyAdjustment, DifficultyEpoch, Height}; +use vecdb::GenericStoredVec; + +use crate::Query; + +/// Blocks per difficulty epoch (2 weeks target) +const BLOCKS_PER_EPOCH: u32 = 2016; + +/// Target block time in seconds (10 minutes) +const TARGET_BLOCK_TIME: u64 = 600; + +/// Get difficulty adjustment information +pub fn get_difficulty_adjustment(query: &Query) -> Result { + let indexer = query.indexer(); + let computer = query.computer(); + let current_height = query.get_height(); + let current_height_u32: u32 = current_height.into(); + + // Get current epoch + let current_epoch = computer + .indexes + .height_to_difficultyepoch + .read_once(current_height)?; + let current_epoch_usize: usize = current_epoch.into(); + + // Get epoch start height + let epoch_start_height = computer + .indexes + .difficultyepoch_to_first_height + .read_once(current_epoch)?; + let epoch_start_u32: u32 = epoch_start_height.into(); + + // Calculate epoch progress + let next_retarget_height = epoch_start_u32 + BLOCKS_PER_EPOCH; + let blocks_into_epoch = current_height_u32 - epoch_start_u32; + let remaining_blocks = next_retarget_height - current_height_u32; + let progress_percent = (blocks_into_epoch as f64 / BLOCKS_PER_EPOCH as f64) * 100.0; + + // Get timestamps using difficultyepoch_to_timestamp for epoch start + let epoch_start_timestamp = computer + .chain + .difficultyepoch_to_timestamp + .read_once(current_epoch)?; + let current_timestamp = indexer + .vecs + .block + .height_to_timestamp + .read_once(current_height)?; + + // Calculate average block time in current epoch + let elapsed_time = (*current_timestamp - *epoch_start_timestamp) as u64; + let time_avg = if blocks_into_epoch > 0 { + elapsed_time / blocks_into_epoch as u64 + } else { + TARGET_BLOCK_TIME + }; + + // Estimate remaining time and retarget date + let remaining_time = remaining_blocks as u64 * time_avg; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(*current_timestamp as u64); + let estimated_retarget_date = now + remaining_time; + + // Calculate expected vs actual time for difficulty change estimate + let expected_time = blocks_into_epoch as u64 * TARGET_BLOCK_TIME; + let difficulty_change = if elapsed_time > 0 && blocks_into_epoch > 0 { + ((expected_time as f64 / elapsed_time as f64) - 1.0) * 100.0 + } else { + 0.0 + }; + + // Time offset from expected schedule + let time_offset = expected_time as i64 - elapsed_time as i64; + + // Calculate previous retarget using stored difficulty values + let previous_retarget = if current_epoch_usize > 0 { + let prev_epoch = DifficultyEpoch::from(current_epoch_usize - 1); + let prev_epoch_start = computer + .indexes + .difficultyepoch_to_first_height + .read_once(prev_epoch)?; + + let prev_difficulty = indexer + .vecs + .block + .height_to_difficulty + .read_once(prev_epoch_start)?; + let curr_difficulty = indexer + .vecs + .block + .height_to_difficulty + .read_once(epoch_start_height)?; + + if *prev_difficulty > 0.0 { + ((*curr_difficulty / *prev_difficulty) - 1.0) * 100.0 + } else { + 0.0 + } + } else { + 0.0 + }; + + Ok(DifficultyAdjustment { + progress_percent, + difficulty_change, + estimated_retarget_date, + remaining_blocks, + remaining_time, + previous_retarget, + next_retarget_height: Height::from(next_retarget_height), + time_avg, + adjusted_time_avg: time_avg, + time_offset, + }) +} diff --git a/crates/brk_query/src/chain/mining/mod.rs b/crates/brk_query/src/chain/mining/mod.rs new file mode 100644 index 000000000..c2e5ec30a --- /dev/null +++ b/crates/brk_query/src/chain/mining/mod.rs @@ -0,0 +1,3 @@ +mod difficulty; + +pub use difficulty::*; diff --git a/crates/brk_query/src/chain/mod.rs b/crates/brk_query/src/chain/mod.rs index 4bff5f5bc..5a7d8e98d 100644 --- a/crates/brk_query/src/chain/mod.rs +++ b/crates/brk_query/src/chain/mod.rs @@ -1,9 +1,11 @@ mod addr; mod block; mod mempool; +mod mining; mod tx; pub use addr::*; pub use block::*; pub use mempool::*; +pub use mining::*; pub use tx::*; diff --git a/crates/brk_query/src/chain/tx/mod.rs b/crates/brk_query/src/chain/tx/mod.rs index 5f1ee6fb7..0c9b721db 100644 --- a/crates/brk_query/src/chain/tx/mod.rs +++ b/crates/brk_query/src/chain/tx/mod.rs @@ -1,7 +1,9 @@ mod hex; +mod outspend; mod status; mod tx; pub use hex::*; +pub use outspend::*; pub use status::*; pub use tx::*; diff --git a/crates/brk_query/src/chain/tx/outspend.rs b/crates/brk_query/src/chain/tx/outspend.rs new file mode 100644 index 000000000..cba52206d --- /dev/null +++ b/crates/brk_query/src/chain/tx/outspend.rs @@ -0,0 +1,160 @@ +use std::str::FromStr; + +use brk_error::{Error, Result}; +use brk_types::{TxInIndex, TxOutspend, TxStatus, Txid, TxidPath, TxidPrefix, Vin, Vout}; +use vecdb::{GenericStoredVec, TypedVecIterator}; + +use crate::Query; + +/// Get the spend status of a specific output +pub fn get_tx_outspend( + TxidPath { txid }: TxidPath, + vout: Vout, + query: &Query, +) -> Result { + let Ok(txid) = bitcoin::Txid::from_str(&txid) else { + return Err(Error::InvalidTxid); + }; + + let txid = Txid::from(txid); + + // Mempool outputs are unspent in on-chain terms + if let Some(mempool) = query.mempool() + && mempool.get_txs().contains_key(&txid) + { + return Ok(TxOutspend::UNSPENT); + } + + // Look up confirmed transaction + let prefix = TxidPrefix::from(&txid); + let indexer = query.indexer(); + let Ok(Some(txindex)) = indexer + .stores + .txidprefix_to_txindex + .get(&prefix) + .map(|opt| opt.map(|cow| cow.into_owned())) + else { + return Err(Error::UnknownTxid); + }; + + // Calculate txoutindex + let first_txoutindex = indexer + .vecs + .tx + .txindex_to_first_txoutindex + .read_once(txindex)?; + let txoutindex = first_txoutindex + vout; + + // Look up spend status + let computer = query.computer(); + let txinindex = computer.stateful.txoutindex_to_txinindex.read_once(txoutindex)?; + + if txinindex == TxInIndex::UNSPENT { + return Ok(TxOutspend::UNSPENT); + } + + get_outspend_details(txinindex, query) +} + +/// Get the spend status of all outputs in a transaction +pub fn get_tx_outspends(TxidPath { txid }: TxidPath, query: &Query) -> Result> { + let Ok(txid) = bitcoin::Txid::from_str(&txid) else { + return Err(Error::InvalidTxid); + }; + + let txid = Txid::from(txid); + + // Mempool outputs are unspent in on-chain terms + if let Some(mempool) = query.mempool() + && let Some(tx_with_hex) = mempool.get_txs().get(&txid) + { + let output_count = tx_with_hex.tx().output.len(); + return Ok(vec![TxOutspend::UNSPENT; output_count]); + } + + // Look up confirmed transaction + let prefix = TxidPrefix::from(&txid); + let indexer = query.indexer(); + let Ok(Some(txindex)) = indexer + .stores + .txidprefix_to_txindex + .get(&prefix) + .map(|opt| opt.map(|cow| cow.into_owned())) + else { + return Err(Error::UnknownTxid); + }; + + // Get output range + let first_txoutindex = indexer + .vecs + .tx + .txindex_to_first_txoutindex + .read_once(txindex)?; + let next_first_txoutindex = indexer + .vecs + .tx + .txindex_to_first_txoutindex + .read_once(txindex.incremented())?; + let output_count = usize::from(next_first_txoutindex) - usize::from(first_txoutindex); + + // Get spend status for each output + let computer = query.computer(); + let mut txoutindex_to_txinindex_iter = computer.stateful.txoutindex_to_txinindex.iter()?; + + let mut outspends = Vec::with_capacity(output_count); + for i in 0..output_count { + let txoutindex = first_txoutindex + Vout::from(i); + let txinindex = txoutindex_to_txinindex_iter.get_unwrap(txoutindex); + + if txinindex == TxInIndex::UNSPENT { + outspends.push(TxOutspend::UNSPENT); + } else { + outspends.push(get_outspend_details(txinindex, query)?); + } + } + + Ok(outspends) +} + +/// Get spending transaction details from a txinindex +fn get_outspend_details(txinindex: TxInIndex, query: &Query) -> Result { + let indexer = query.indexer(); + let computer = query.computer(); + + // Look up spending txindex directly + let spending_txindex = computer.indexes.txinindex_to_txindex.read_once(txinindex)?; + + // Calculate vin + let spending_first_txinindex = indexer + .vecs + .tx + .txindex_to_first_txinindex + .read_once(spending_txindex)?; + let vin = Vin::from(usize::from(txinindex) - usize::from(spending_first_txinindex)); + + // Get spending tx details + let spending_txid = indexer.vecs.tx.txindex_to_txid.read_once(spending_txindex)?; + let spending_height = indexer.vecs.tx.txindex_to_height.read_once(spending_txindex)?; + let block_hash = indexer + .vecs + .block + .height_to_blockhash + .read_once(spending_height)?; + let block_time = indexer + .vecs + .block + .height_to_timestamp + .read_once(spending_height)?; + + Ok(TxOutspend { + spent: true, + txid: Some(spending_txid), + vin: Some(vin), + status: Some(TxStatus { + confirmed: true, + block_height: Some(spending_height), + block_hash: Some(block_hash), + block_time: Some(block_time), + }), + }) +} diff --git a/crates/brk_query/src/lib.rs b/crates/brk_query/src/lib.rs index 805952609..27f3aad59 100644 --- a/crates/brk_query/src/lib.rs +++ b/crates/brk_query/src/lib.rs @@ -6,12 +6,13 @@ use std::{collections::BTreeMap, sync::Arc}; use brk_computer::Computer; use brk_error::{Error, Result}; use brk_indexer::Indexer; -use brk_monitor::Mempool; +use brk_mempool::Mempool; use brk_reader::Reader; use brk_traversable::TreeNode; use brk_types::{ - Address, AddressStats, BlockInfo, BlockStatus, Format, Height, Index, IndexInfo, Limit, - MempoolInfo, Metric, MetricCount, RecommendedFees, Transaction, TxStatus, Txid, TxidPath, Utxo, + Address, AddressStats, BlockInfo, BlockStatus, BlockTimestamp, Format, Height, Index, + IndexInfo, Limit, MempoolInfo, Metric, MetricCount, RecommendedFees, Timestamp, Transaction, + TxOutspend, TxStatus, Txid, TxidPath, Utxo, Vout, }; use vecdb::{AnyExportableVec, AnyStoredVec}; @@ -31,12 +32,16 @@ pub use pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam}; pub use params::{Params, ParamsDeprec, ParamsOpt}; use vecs::Vecs; +pub use crate::chain::BLOCK_TXS_PAGE_SIZE; +pub use crate::chain::validate_address; use crate::{ chain::{ - get_address, get_address_txids, get_address_utxos, get_block_by_height, - get_block_status_by_height, get_block_txids, get_blocks, get_height_by_hash, - get_mempool_info, get_mempool_txids, get_recommended_fees, get_transaction, - get_transaction_hex, get_transaction_status, + get_address, get_address_mempool_txids, get_address_txids, get_address_utxos, + get_block_by_height, get_block_by_timestamp, get_block_raw, get_block_status_by_height, + get_block_txid_at_index, get_block_txids, get_block_txs, get_blocks, + get_difficulty_adjustment, get_height_by_hash, get_mempool_blocks, get_mempool_info, + get_mempool_txids, get_recommended_fees, get_transaction, get_transaction_hex, + get_transaction_status, get_tx_outspend, get_tx_outspends, }, vecs::{IndexToVec, MetricToVec}, }; @@ -93,6 +98,10 @@ impl Query { get_address_utxos(address, self) } + pub fn get_address_mempool_txids(&self, address: Address) -> Result> { + get_address_mempool_txids(address, self) + } + pub fn get_transaction(&self, txid: TxidPath) -> Result { get_transaction(txid, self) } @@ -105,6 +114,14 @@ impl Query { get_transaction_hex(txid, self) } + pub fn get_tx_outspend(&self, txid: TxidPath, vout: Vout) -> Result { + get_tx_outspend(txid, vout, self) + } + + pub fn get_tx_outspends(&self, txid: TxidPath) -> Result> { + get_tx_outspends(txid, self) + } + pub fn get_block(&self, hash: &str) -> Result { let height = get_height_by_hash(hash, self)?; get_block_by_height(height, self) @@ -114,6 +131,10 @@ impl Query { get_block_by_height(height, self) } + pub fn get_block_by_timestamp(&self, timestamp: Timestamp) -> Result { + get_block_by_timestamp(timestamp, self) + } + pub fn get_block_status(&self, hash: &str) -> Result { let height = get_height_by_hash(hash, self)?; get_block_status_by_height(height, self) @@ -128,6 +149,21 @@ impl Query { get_block_txids(height, self) } + pub fn get_block_txs(&self, hash: &str, start_index: usize) -> Result> { + let height = get_height_by_hash(hash, self)?; + get_block_txs(height, start_index, self) + } + + pub fn get_block_txid_at_index(&self, hash: &str, index: usize) -> Result { + let height = get_height_by_hash(hash, self)?; + get_block_txid_at_index(height, index, self) + } + + pub fn get_block_raw(&self, hash: &str) -> Result> { + let height = get_height_by_hash(hash, self)?; + get_block_raw(height, self) + } + pub fn get_mempool_info(&self) -> Result { get_mempool_info(self) } @@ -140,6 +176,14 @@ impl Query { get_recommended_fees(self) } + pub fn get_mempool_blocks(&self) -> Result> { + get_mempool_blocks(self) + } + + pub fn get_difficulty_adjustment(&self) -> Result { + get_difficulty_adjustment(self) + } + pub fn match_metric(&self, metric: &Metric, limit: Limit) -> Vec<&'static str> { self.vecs().matches(metric, limit) } diff --git a/crates/brk_server/src/api/addresses/mod.rs b/crates/brk_server/src/api/addresses/mod.rs index b7d111f4e..39e535ecc 100644 --- a/crates/brk_server/src/api/addresses/mod.rs +++ b/crates/brk_server/src/api/addresses/mod.rs @@ -5,7 +5,8 @@ use axum::{ response::{Redirect, Response}, routing::get, }; -use brk_types::{Address, AddressStats, AddressTxidsParam, Txid, Utxo}; +use brk_query::validate_address; +use brk_types::{Address, AddressStats, AddressTxidsParam, AddressValidation, Txid, Utxo}; use crate::{ VERSION, @@ -93,5 +94,71 @@ impl AddressRoutes for ApiRouter { .server_error() ), ) + .api_route( + "/api/address/{address}/txs/mempool", + get_with(async | + headers: HeaderMap, + Path(address): Path
, + State(state): State + | { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_address_mempool_txids(address).await.to_json_response(&etag) + }, |op| op + .addresses_tag() + .summary("Address mempool transactions") + .description("Get unconfirmed transaction IDs for an address from the mempool (up to 50).") + .ok_response::>() + .not_modified() + .bad_request() + .not_found() + .server_error() + ), + ) + .api_route( + "/api/address/{address}/txs/chain/{after_txid}", + get_with(async | + headers: HeaderMap, + Path((address, after_txid)): Path<(Address, Option)>, + State(state): State + | { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_address_txids(address, after_txid, 25).await.to_json_response(&etag) + }, |op| op + .addresses_tag() + .summary("Address confirmed transactions") + .description("Get confirmed transaction IDs for an address, 25 per page. Use after_txid for pagination.") + .ok_response::>() + .not_modified() + .bad_request() + .not_found() + .server_error() + ), + ) + .api_route( + "/api/v1/validate-address/{address}", + get_with(async | + headers: HeaderMap, + Path(address): Path, + State(state): State + | { + let etag = VERSION; + if headers.has_etag(etag) { + return Response::new_not_modified(); + } + Response::new_json(validate_address(&address), etag) + }, |op| op + .addresses_tag() + .summary("Validate address") + .description("Validate a Bitcoin address and get information about its type and scriptPubKey.") + .ok_response::() + .not_modified() + ), + ) } } diff --git a/crates/brk_server/src/api/blocks/mod.rs b/crates/brk_server/src/api/blocks/mod.rs index 7728bf67a..f32025b39 100644 --- a/crates/brk_server/src/api/blocks/mod.rs +++ b/crates/brk_server/src/api/blocks/mod.rs @@ -5,7 +5,11 @@ use axum::{ response::{Redirect, Response}, routing::get, }; -use brk_types::{BlockHashPath, BlockInfo, BlockStatus, Height, HeightPath, StartHeightPath, Txid}; +use brk_query::BLOCK_TXS_PAGE_SIZE; +use brk_types::{ + BlockHashPath, BlockHashStartIndexPath, BlockHashTxIndexPath, BlockInfo, BlockStatus, + BlockTimestamp, Height, HeightPath, StartHeightPath, TimestampPath, Transaction, Txid, +}; use crate::{ VERSION, @@ -161,5 +165,111 @@ impl BlockRoutes for ApiRouter { }, ), ) + .api_route( + "/api/block/{hash}/txs/{start_index}", + get_with( + async |headers: HeaderMap, + Path(path): Path, + State(state): State| { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_block_txs(path.hash, path.start_index).await.to_json_response(&etag) + }, + |op| { + op.blocks_tag() + .summary("Block transactions (paginated)") + .description(&format!( + "Retrieve transactions in a block by block hash, starting from the specified index. Returns up to {} transactions at a time.", + BLOCK_TXS_PAGE_SIZE + )) + .ok_response::>() + .not_modified() + .bad_request() + .not_found() + .server_error() + }, + ), + ) + .api_route( + "/api/block/{hash}/txid/{index}", + get_with( + async |headers: HeaderMap, + Path(path): Path, + State(state): State| { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_block_txid_at_index(path.hash, path.index).await.to_display_response(&etag) + }, + |op| { + op.blocks_tag() + .summary("Transaction ID at index") + .description( + "Retrieve a single transaction ID at a specific index within a block. Returns plain text txid.", + ) + .ok_response::() + .not_modified() + .bad_request() + .not_found() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/blocks/timestamp/{timestamp}", + get_with( + async |headers: HeaderMap, + Path(path): Path, + State(state): State| { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_block_by_timestamp(path.timestamp) + .await + .to_json_response(&etag) + }, + |op| { + op.blocks_tag() + .summary("Block by timestamp") + .description("Find the block closest to a given UNIX timestamp.") + .ok_response::() + .not_modified() + .bad_request() + .not_found() + .server_error() + }, + ), + ) + .api_route( + "/api/block/{hash}/raw", + get_with( + async |headers: HeaderMap, + Path(path): Path, + State(state): State| { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_block_raw(path.hash).await.to_bytes_response(&etag) + }, + |op| { + op.blocks_tag() + .summary("Raw block") + .description( + "Returns the raw block data in binary format.", + ) + .ok_response::>() + .not_modified() + .bad_request() + .not_found() + .server_error() + }, + ), + ) } } diff --git a/crates/brk_server/src/api/mempool/mod.rs b/crates/brk_server/src/api/mempool/mod.rs index 4b4b1a054..bdfcf0677 100644 --- a/crates/brk_server/src/api/mempool/mod.rs +++ b/crates/brk_server/src/api/mempool/mod.rs @@ -5,7 +5,7 @@ use axum::{ response::{Redirect, Response}, routing::get, }; -use brk_types::{MempoolInfo, RecommendedFees, Txid}; +use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid}; use crate::{ VERSION, @@ -82,5 +82,25 @@ impl MempoolRoutes for ApiRouter { }, ), ) + .api_route( + "/api/v1/fees/mempool-blocks", + get_with( + async |headers: HeaderMap, State(state): State| { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_mempool_blocks().await.to_json_response(&etag) + }, + |op| { + op.mempool_tag() + .summary("Projected mempool blocks") + .description("Get projected blocks from the mempool for fee estimation. Each block contains statistics about transactions that would be included if a block were mined now.") + .ok_response::>() + .not_modified() + .server_error() + }, + ), + ) } } diff --git a/crates/brk_server/src/api/mining/mod.rs b/crates/brk_server/src/api/mining/mod.rs index e69de29bb..50c44127f 100644 --- a/crates/brk_server/src/api/mining/mod.rs +++ b/crates/brk_server/src/api/mining/mod.rs @@ -0,0 +1,51 @@ +use aide::axum::{ApiRouter, routing::get_with}; +use axum::{ + extract::State, + http::HeaderMap, + response::{Redirect, Response}, + routing::get, +}; +use brk_types::DifficultyAdjustment; + +use crate::{ + VERSION, + extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended}, +}; + +use super::AppState; + +pub trait MiningRoutes { + fn add_mining_routes(self) -> Self; +} + +impl MiningRoutes for ApiRouter { + fn add_mining_routes(self) -> Self { + self.route( + "/api/v1/mining", + get(Redirect::temporary("/api#tag/mining")), + ) + .api_route( + "/api/v1/difficulty-adjustment", + get_with( + async |headers: HeaderMap, State(state): State| { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_difficulty_adjustment() + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Difficulty adjustment") + .description("Get current difficulty adjustment information including progress through the current epoch, estimated retarget date, and difficulty change prediction.") + .ok_response::() + .not_modified() + .server_error() + }, + ), + ) + } +} diff --git a/crates/brk_server/src/api/mod.rs b/crates/brk_server/src/api/mod.rs index 4cd704ad0..82c58c52b 100644 --- a/crates/brk_server/src/api/mod.rs +++ b/crates/brk_server/src/api/mod.rs @@ -16,7 +16,7 @@ use crate::{ VERSION, api::{ addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes, - metrics::ApiMetricsRoutes, transactions::TxRoutes, + metrics::ApiMetricsRoutes, mining::MiningRoutes, transactions::TxRoutes, }, extended::{HeaderMapExtended, ResponseExtended, TransformResponseExtended}, }; @@ -27,6 +27,7 @@ mod addresses; mod blocks; mod mempool; mod metrics; +mod mining; mod openapi; mod transactions; @@ -41,6 +42,7 @@ impl ApiRoutes for ApiRouter { self.add_addresses_routes() .add_block_routes() .add_mempool_routes() + .add_mining_routes() .add_tx_routes() .add_metrics_routes() .route("/api/server", get(Redirect::temporary("/api#tag/server"))) diff --git a/crates/brk_server/src/api/transactions/mod.rs b/crates/brk_server/src/api/transactions/mod.rs index 3ab4f810e..d22e68ec8 100644 --- a/crates/brk_server/src/api/transactions/mod.rs +++ b/crates/brk_server/src/api/transactions/mod.rs @@ -5,7 +5,7 @@ use axum::{ response::{Redirect, Response}, routing::get, }; -use brk_types::{Transaction, TxStatus, TxidPath}; +use brk_types::{Transaction, TxOutspend, TxStatus, TxidPath, TxidVoutPath}; use crate::{ VERSION, @@ -104,5 +104,60 @@ impl TxRoutes for ApiRouter { .server_error(), ), ) + .api_route( + "/api/tx/{txid}/outspend/{vout}", + get_with( + async | + headers: HeaderMap, + Path(path): Path, + State(state): State + | { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + let txid = TxidPath { txid: path.txid }; + state.get_tx_outspend(txid, path.vout).await.to_json_response(&etag) + }, + |op| op + .transactions_tag() + .summary("Output spend status") + .description( + "Get the spending status of a transaction output. Returns whether the output has been spent and, if so, the spending transaction details.", + ) + .ok_response::() + .not_modified() + .bad_request() + .not_found() + .server_error(), + ), + ) + .api_route( + "/api/tx/{txid}/outspends", + get_with( + async | + headers: HeaderMap, + Path(txid): Path, + State(state): State + | { + let etag = format!("{VERSION}-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_tx_outspends(txid).await.to_json_response(&etag) + }, + |op| op + .transactions_tag() + .summary("All output spend statuses") + .description( + "Get the spending status of all outputs in a transaction. Returns an array with the spend status for each output.", + ) + .ok_response::>() + .not_modified() + .bad_request() + .not_found() + .server_error(), + ), + ) } } diff --git a/crates/brk_server/src/extended/header_map.rs b/crates/brk_server/src/extended/header_map.rs index 2b658f600..a5569b049 100644 --- a/crates/brk_server/src/extended/header_map.rs +++ b/crates/brk_server/src/extended/header_map.rs @@ -47,6 +47,7 @@ pub trait HeaderMapExtended { fn insert_content_type_text_html(&mut self); fn insert_content_type_text_plain(&mut self); fn insert_content_type_font_woff2(&mut self); + fn insert_content_type_octet_stream(&mut self); } impl HeaderMapExtended for HeaderMap { @@ -203,4 +204,11 @@ impl HeaderMapExtended for HeaderMap { fn insert_content_type_font_woff2(&mut self) { self.insert(header::CONTENT_TYPE, "font/woff2".parse().unwrap()); } + + fn insert_content_type_octet_stream(&mut self) { + self.insert( + header::CONTENT_TYPE, + "application/octet-stream".parse().unwrap(), + ); + } } diff --git a/crates/brk_server/src/extended/response.rs b/crates/brk_server/src/extended/response.rs index aac3b5054..973ce52a0 100644 --- a/crates/brk_server/src/extended/response.rs +++ b/crates/brk_server/src/extended/response.rs @@ -20,6 +20,8 @@ where T: Serialize; fn new_text(value: &str, etag: &str) -> Self; fn new_text_with(status: StatusCode, value: &str, etag: &str) -> Self; + fn new_bytes(value: Vec, etag: &str) -> Self; + fn new_bytes_with(status: StatusCode, value: Vec, etag: &str) -> Self; } impl ResponseExtended for Response { @@ -68,4 +70,19 @@ impl ResponseExtended for Response { headers.insert_etag(etag); response } + + fn new_bytes(value: Vec, etag: &str) -> Self { + Self::new_bytes_with(StatusCode::default(), value, etag) + } + + fn new_bytes_with(status: StatusCode, value: Vec, etag: &str) -> Self { + let mut response = Response::builder().body(value.into()).unwrap(); + *response.status_mut() = status; + let headers = response.headers_mut(); + headers.insert_cors(); + headers.insert_content_type_octet_stream(); + headers.insert_cache_control_must_revalidate(); + headers.insert_etag(etag); + response + } } diff --git a/crates/brk_server/src/extended/result.rs b/crates/brk_server/src/extended/result.rs index d4d76c5e8..b09a0ec66 100644 --- a/crates/brk_server/src/extended/result.rs +++ b/crates/brk_server/src/extended/result.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use axum::{http::StatusCode, response::Response}; use brk_error::{Error, Result}; use serde::Serialize; @@ -12,6 +14,12 @@ pub trait ResultExtended { fn to_text_response(self, etag: &str) -> Response where T: AsRef; + fn to_display_response(self, etag: &str) -> Response + where + T: Display; + fn to_bytes_response(self, etag: &str) -> Response + where + T: Into>; } impl ResultExtended for Result { @@ -50,4 +58,24 @@ impl ResultExtended for Result { Err((status, message)) => Response::new_text_with(status, &message, etag), } } + + fn to_display_response(self, etag: &str) -> Response + where + T: Display, + { + match self.with_status() { + Ok(value) => Response::new_text(&value.to_string(), etag), + Err((status, message)) => Response::new_text_with(status, &message, etag), + } + } + + fn to_bytes_response(self, etag: &str) -> Response + where + T: Into>, + { + match self.with_status() { + Ok(value) => Response::new_bytes(value.into(), etag), + Err((status, message)) => Response::new_bytes_with(status, message.into_bytes(), etag), + } + } } diff --git a/crates/brk_types/src/addressvalidation.rs b/crates/brk_types/src/addressvalidation.rs new file mode 100644 index 000000000..d5b8f5191 --- /dev/null +++ b/crates/brk_types/src/addressvalidation.rs @@ -0,0 +1,48 @@ +use schemars::JsonSchema; +use serde::Serialize; + +/// Address validation result +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct AddressValidation { + /// Whether the address is valid + pub isvalid: bool, + + /// The validated address + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + + /// The scriptPubKey in hex + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "scriptPubKey")] + pub script_pub_key: Option, + + /// Whether this is a script address (P2SH) + #[serde(skip_serializing_if = "Option::is_none")] + pub isscript: Option, + + /// Whether this is a witness address + #[serde(skip_serializing_if = "Option::is_none")] + pub iswitness: Option, + + /// Witness version (0 for P2WPKH/P2WSH, 1 for P2TR) + #[serde(skip_serializing_if = "Option::is_none")] + pub witness_version: Option, + + /// Witness program in hex + #[serde(skip_serializing_if = "Option::is_none")] + pub witness_program: Option, +} + +impl AddressValidation { + pub fn invalid() -> Self { + Self { + isvalid: false, + address: None, + script_pub_key: None, + isscript: None, + iswitness: None, + witness_version: None, + witness_program: None, + } + } +} diff --git a/crates/brk_types/src/blockhashstartindexpath.rs b/crates/brk_types/src/blockhashstartindexpath.rs new file mode 100644 index 000000000..8e74856f1 --- /dev/null +++ b/crates/brk_types/src/blockhashstartindexpath.rs @@ -0,0 +1,13 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +#[derive(Deserialize, JsonSchema)] +pub struct BlockHashStartIndexPath { + /// Bitcoin block hash + #[schemars(example = &"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")] + pub hash: String, + + /// Starting transaction index (0-based) + #[schemars(example = 0)] + pub start_index: usize, +} diff --git a/crates/brk_types/src/blockhashtxindexpath.rs b/crates/brk_types/src/blockhashtxindexpath.rs new file mode 100644 index 000000000..cc64fd432 --- /dev/null +++ b/crates/brk_types/src/blockhashtxindexpath.rs @@ -0,0 +1,13 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +#[derive(Deserialize, JsonSchema)] +pub struct BlockHashTxIndexPath { + /// Bitcoin block hash + #[schemars(example = &"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")] + pub hash: String, + + /// Transaction index within the block (0-based) + #[schemars(example = 0)] + pub index: usize, +} diff --git a/crates/brk_types/src/blocktimestamp.rs b/crates/brk_types/src/blocktimestamp.rs new file mode 100644 index 000000000..c4d9c3aa1 --- /dev/null +++ b/crates/brk_types/src/blocktimestamp.rs @@ -0,0 +1,17 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::{BlockHash, Height}; + +/// Block information returned for timestamp queries +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct BlockTimestamp { + /// Block height + pub height: Height, + + /// Block hash + pub hash: BlockHash, + + /// Block timestamp in ISO 8601 format + pub timestamp: String, +} diff --git a/crates/brk_types/src/difficultyadjustment.rs b/crates/brk_types/src/difficultyadjustment.rs new file mode 100644 index 000000000..97ace9760 --- /dev/null +++ b/crates/brk_types/src/difficultyadjustment.rs @@ -0,0 +1,49 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::Height; + +/// Difficulty adjustment information. +#[derive(Debug, Clone, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct DifficultyAdjustment { + /// Progress through current difficulty epoch (0-100%) + #[schemars(example = 44.4)] + pub progress_percent: f64, + + /// Estimated difficulty change at next retarget (%) + #[schemars(example = 2.5)] + pub difficulty_change: f64, + + /// Estimated Unix timestamp of next retarget + #[schemars(example = 1627762478)] + pub estimated_retarget_date: u64, + + /// Blocks remaining until retarget + #[schemars(example = 1121)] + pub remaining_blocks: u32, + + /// Estimated seconds until retarget + #[schemars(example = 665977)] + pub remaining_time: u64, + + /// Previous difficulty adjustment (%) + #[schemars(example = -4.8)] + pub previous_retarget: f64, + + /// Height of next retarget + #[schemars(example = 741888)] + pub next_retarget_height: Height, + + /// Average block time in current epoch (seconds) + #[schemars(example = 580)] + pub time_avg: u64, + + /// Time-adjusted average (accounting for timestamp manipulation) + #[schemars(example = 580)] + pub adjusted_time_avg: u64, + + /// Time offset from expected schedule (seconds) + #[schemars(example = 0)] + pub time_offset: i64, +} diff --git a/crates/brk_types/src/feerate.rs b/crates/brk_types/src/feerate.rs index 2fe420983..5a5fb4a49 100644 --- a/crates/brk_types/src/feerate.rs +++ b/crates/brk_types/src/feerate.rs @@ -12,6 +12,12 @@ use super::{Sats, VSize}; #[derive(Debug, Default, Clone, Copy, Serialize, Pco, JsonSchema)] pub struct FeeRate(f64); +impl FeeRate { + pub fn new(fr: f64) -> Self { + Self(fr) + } +} + impl From<(Sats, VSize)> for FeeRate { #[inline] fn from((sats, vsize): (Sats, VSize)) -> Self { diff --git a/crates/brk_types/src/lib.rs b/crates/brk_types/src/lib.rs index 97adefddc..b7b4b00c5 100644 --- a/crates/brk_types/src/lib.rs +++ b/crates/brk_types/src/lib.rs @@ -11,6 +11,7 @@ mod addressindextxindex; mod addressmempoolstats; mod addresstxidsparam; mod addressstats; +mod addressvalidation; mod anyaddressindex; mod bitcoin; mod blkmetadata; @@ -18,14 +19,18 @@ mod blkposition; mod block; mod blockhash; mod blockhashpath; +mod blockhashstartindexpath; +mod blockhashtxindexpath; mod blockhashprefix; mod blockinfo; mod blockstatus; +mod blocktimestamp; mod bytes; mod cents; mod date; mod dateindex; mod decadeindex; +mod difficultyadjustment; mod difficultyepoch; mod dollars; mod emptyaddressdata; @@ -44,6 +49,7 @@ mod loadedaddressdata; mod loadedaddressindex; mod metric; mod mempoolentryinfo; +mod mempoolblock; mod mempoolinfo; mod metriccount; mod metrics; @@ -71,6 +77,8 @@ mod p2wshaddressindex; mod p2wshbytes; mod pool; mod poolid; +mod poolsresponse; +mod poolstats; mod pools; mod quarterindex; mod rawlocktime; @@ -88,16 +96,19 @@ mod stored_u32; mod stored_u64; mod stored_u8; mod timestamp; +mod timestamppath; mod treenode; mod tx; mod txid; mod txidpath; mod txidprefix; +mod txidvoutpath; mod txin; mod txindex; mod txinindex; mod txout; mod txoutindex; +mod txoutspend; mod txstatus; mod txversion; mod txwithhex; @@ -121,6 +132,7 @@ pub use addressindextxindex::*; pub use addressmempoolstats::*; pub use addressstats::*; pub use addresstxidsparam::*; +pub use addressvalidation::*; pub use anyaddressindex::*; pub use bitcoin::*; pub use blkmetadata::*; @@ -128,14 +140,18 @@ pub use blkposition::*; pub use block::*; pub use blockhash::*; pub use blockhashpath::*; +pub use blockhashstartindexpath::*; +pub use blockhashtxindexpath::*; pub use blockhashprefix::*; pub use blockinfo::*; pub use blockstatus::*; +pub use blocktimestamp::*; pub use bytes::*; pub use cents::*; pub use date::*; pub use dateindex::*; pub use decadeindex::*; +pub use difficultyadjustment::*; pub use difficultyepoch::*; pub use dollars::*; pub use emptyaddressdata::*; @@ -153,6 +169,7 @@ pub use limit::*; pub use loadedaddressdata::*; pub use loadedaddressindex::*; pub use mempoolentryinfo::*; +pub use mempoolblock::*; pub use mempoolinfo::*; pub use metric::*; pub use metriccount::*; @@ -182,6 +199,8 @@ pub use p2wshbytes::*; pub use pool::*; pub use poolid::*; pub use pools::*; +pub use poolsresponse::*; +pub use poolstats::*; pub use quarterindex::*; pub use rawlocktime::*; pub use recommendedfees::*; @@ -198,16 +217,19 @@ pub use stored_u16::*; pub use stored_u32::*; pub use stored_u64::*; pub use timestamp::*; +pub use timestamppath::*; pub use treenode::*; pub use tx::*; pub use txid::*; pub use txidpath::*; pub use txidprefix::*; +pub use txidvoutpath::*; pub use txin::*; pub use txindex::*; pub use txinindex::*; pub use txout::*; pub use txoutindex::*; +pub use txoutspend::*; pub use txstatus::*; pub use txversion::*; pub use txwithhex::*; diff --git a/crates/brk_types/src/mempoolblock.rs b/crates/brk_types/src/mempoolblock.rs new file mode 100644 index 000000000..277c18987 --- /dev/null +++ b/crates/brk_types/src/mempoolblock.rs @@ -0,0 +1,64 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::{FeeRate, Sats, VSize}; + +/// Block info in a mempool.space like format for fee estimation. +#[derive(Debug, Clone, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct MempoolBlock { + /// Total block size in weight units + #[schemars(example = 3993472)] + pub block_size: u64, + + /// Total block virtual size in vbytes + #[schemars(example = 998368.0)] + pub block_v_size: f64, + + /// Number of transactions in the projected block + #[schemars(example = 863)] + pub n_tx: u32, + + /// Total fees in satoshis + #[schemars(example = 8875608)] + pub total_fees: Sats, + + /// Median fee rate in sat/vB + #[schemars(example = 10.5)] + pub median_fee: FeeRate, + + /// Fee rate range: [min, 10%, 25%, 50%, 75%, 90%, max] + #[schemars(example = example_fee_range())] + pub fee_range: [FeeRate; 7], +} + +fn example_fee_range() -> [FeeRate; 7] { + [ + FeeRate::new(1.0), + FeeRate::new(2.42), + FeeRate::new(8.1), + FeeRate::new(10.14), + FeeRate::new(11.05), + FeeRate::new(12.04), + FeeRate::new(302.11), + ] +} + +impl MempoolBlock { + pub fn new( + tx_count: u32, + total_vsize: VSize, + total_fee: Sats, + fee_range: [FeeRate; 7], + ) -> Self { + let vsize_f64 = *total_vsize as f64; + Self { + block_size: *total_vsize * 4, // weight = vsize * 4 + block_v_size: vsize_f64, + n_tx: tx_count, + total_fees: total_fee, + median_fee: fee_range[3], + fee_range, + } + } +} diff --git a/crates/brk_types/src/pool.rs b/crates/brk_types/src/pool.rs index a7bb88ae8..132f55104 100644 --- a/crates/brk_types/src/pool.rs +++ b/crates/brk_types/src/pool.rs @@ -1,12 +1,31 @@ +use schemars::JsonSchema; +use serde::Serialize; + use super::PoolId; -#[derive(Debug)] +/// Mining pool information +#[derive(Debug, Serialize, JsonSchema)] pub struct Pool { + /// Unique pool identifier pub id: PoolId, + + /// Pool name pub name: &'static str, + + /// Known payout addresses for pool identification + #[serde(skip)] pub addresses: Box<[&'static str]>, + + /// Coinbase tags used to identify blocks mined by this pool + #[serde(skip)] pub tags: Box<[&'static str]>, + + /// Lowercase coinbase tags for case-insensitive matching + #[serde(skip)] + #[schemars(skip)] pub tags_lowercase: Box<[String]>, + + /// Pool website URL pub link: &'static str, } diff --git a/crates/brk_types/src/poolid.rs b/crates/brk_types/src/poolid.rs index ca1c59a44..71a69374f 100644 --- a/crates/brk_types/src/poolid.rs +++ b/crates/brk_types/src/poolid.rs @@ -1,4 +1,5 @@ use num_enum::{FromPrimitive, IntoPrimitive}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum::Display; use vecdb::{Bytes, Formattable}; @@ -17,6 +18,7 @@ use vecdb::{Bytes, Formattable}; Ord, Serialize, Deserialize, + JsonSchema, FromPrimitive, IntoPrimitive, )] diff --git a/crates/brk_types/src/poolsresponse.rs b/crates/brk_types/src/poolsresponse.rs new file mode 100644 index 000000000..fe349b5f9 --- /dev/null +++ b/crates/brk_types/src/poolsresponse.rs @@ -0,0 +1,15 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::PoolStats; + +/// Mining pools response for a time period +#[derive(Debug, Serialize, JsonSchema)] +pub struct PoolsResponse { + /// List of pools sorted by block count descending + pub pools: Vec, + + /// Total blocks in the time period + #[serde(rename = "blockCount")] + pub block_count: u32, +} diff --git a/crates/brk_types/src/poolstats.rs b/crates/brk_types/src/poolstats.rs new file mode 100644 index 000000000..76dac7c1f --- /dev/null +++ b/crates/brk_types/src/poolstats.rs @@ -0,0 +1,19 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::Pool; + +/// Mining pool with block statistics for a time period +#[derive(Debug, Serialize, JsonSchema)] +pub struct PoolStats { + /// Pool information + #[serde(flatten)] + pub pool: &'static Pool, + + /// Number of blocks mined in the time period + #[serde(rename = "blockCount")] + pub block_count: u32, + + /// Pool's share of total blocks (0.0 - 1.0) + pub share: f64, +} diff --git a/crates/brk_types/src/timestamp.rs b/crates/brk_types/src/timestamp.rs index 2cf497399..c2b073d4a 100644 --- a/crates/brk_types/src/timestamp.rs +++ b/crates/brk_types/src/timestamp.rs @@ -3,13 +3,26 @@ use std::ops::{Add, AddAssign, Div}; use derive_deref::Deref; use jiff::{civil::date, tz::TimeZone}; use schemars::JsonSchema; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco}; use super::Date; /// Timestamp -#[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Pco, JsonSchema)] +#[derive( + Debug, + Deref, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Pco, + JsonSchema, +)] pub struct Timestamp(u32); pub const ONE_HOUR_IN_SEC: u32 = 60 * 60; diff --git a/crates/brk_types/src/timestamppath.rs b/crates/brk_types/src/timestamppath.rs new file mode 100644 index 000000000..1a5140fc2 --- /dev/null +++ b/crates/brk_types/src/timestamppath.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::Timestamp; + +#[derive(Deserialize, JsonSchema)] +pub struct TimestampPath { + /// UNIX timestamp in seconds + #[schemars(example = 1672531200)] + pub timestamp: Timestamp, +} diff --git a/crates/brk_types/src/txidvoutpath.rs b/crates/brk_types/src/txidvoutpath.rs new file mode 100644 index 000000000..ffa156ce2 --- /dev/null +++ b/crates/brk_types/src/txidvoutpath.rs @@ -0,0 +1,15 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::Vout; + +#[derive(Deserialize, JsonSchema)] +pub struct TxidVoutPath { + /// Bitcoin transaction id + #[schemars(example = &"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")] + pub txid: String, + + /// Output index + #[schemars(example = 0)] + pub vout: Vout, +} diff --git a/crates/brk_types/src/txoutspend.rs b/crates/brk_types/src/txoutspend.rs new file mode 100644 index 000000000..73fa41c99 --- /dev/null +++ b/crates/brk_types/src/txoutspend.rs @@ -0,0 +1,32 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::{TxStatus, Txid, Vin}; + +/// Status of an output indicating whether it has been spent +#[derive(Debug, Clone, Serialize, JsonSchema)] +pub struct TxOutspend { + /// Whether the output has been spent + pub spent: bool, + + /// Transaction ID of the spending transaction (only present if spent) + #[serde(skip_serializing_if = "Option::is_none")] + pub txid: Option, + + /// Input index in the spending transaction (only present if spent) + #[serde(skip_serializing_if = "Option::is_none")] + pub vin: Option, + + /// Status of the spending transaction (only present if spent) + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +impl TxOutspend { + pub const UNSPENT: Self = Self { + spent: false, + txid: None, + vin: None, + status: None, + }; +} diff --git a/crates/brk_types/src/vin.rs b/crates/brk_types/src/vin.rs index 827a5db5c..60e42e13c 100644 --- a/crates/brk_types/src/vin.rs +++ b/crates/brk_types/src/vin.rs @@ -1,6 +1,9 @@ use derive_deref::Deref; +use schemars::JsonSchema; +use serde::Serialize; -#[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +/// Input index in the spending transaction +#[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)] pub struct Vin(u16); impl Vin { diff --git a/crates/brk_types/src/vout.rs b/crates/brk_types/src/vout.rs index 7d573dc0c..c3cb06a89 100644 --- a/crates/brk_types/src/vout.rs +++ b/crates/brk_types/src/vout.rs @@ -1,6 +1,6 @@ use derive_deref::Deref; use schemars::JsonSchema; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use vecdb::{Bytes, Formattable}; /// Index of the output being spent in the previous transaction @@ -15,6 +15,7 @@ use vecdb::{Bytes, Formattable}; PartialOrd, Ord, Serialize, + Deserialize, JsonSchema, Bytes, Hash,