mempool: snapshot 5 + query: new tools + server: endpoints

This commit is contained in:
nym21
2025-12-14 02:06:14 +01:00
parent db5d784ff7
commit b491b1f41f
79 changed files with 1588 additions and 129 deletions
Generated
+5 -4
View File
@@ -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",
+1 -1
View File
@@ -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" }
+3 -3
View File
@@ -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 }
+2 -2
View File
@@ -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)]
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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};
+9
View File
@@ -82,6 +82,7 @@ pub struct Vecs {
pub txindex_to_input_count: EagerVec<PcoVec<TxIndex, StoredU64>>,
pub txindex_to_output_count: EagerVec<PcoVec<TxIndex, StoredU64>>,
pub txindex_to_txindex: LazyVecFrom1<TxIndex, TxIndex, TxIndex, Txid>,
pub txinindex_to_txindex: EagerVec<PcoVec<TxInIndex, TxIndex>>,
pub txinindex_to_txinindex: LazyVecFrom1<TxInIndex, TxInIndex, TxInIndex, OutPoint>,
pub txinindex_to_txoutindex: EagerVec<PcoVec<TxInIndex, TxOutIndex>>,
pub txoutindex_to_txoutindex: LazyVecFrom1<TxOutIndex, TxOutIndex, TxOutIndex, Sats>,
@@ -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(
@@ -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
@@ -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<()> {
@@ -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<TxNode> that only allows PoolIndex access.
@@ -84,12 +84,20 @@ pub fn build_graph(entries: &[Option<Entry>]) -> 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)
@@ -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).
@@ -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<Package> {
}
/// Select a tx and all its unselected ancestors in topological order.
fn select_with_ancestors(graph: &mut Graph, pool_idx: PoolIndex) -> Vec<PoolIndex> {
let mut result: Vec<PoolIndex> = 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<PoolIndex> = FxHashSet::default();
let mut stack: Vec<PoolIndex> = 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) {
@@ -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<TxidPrefix>,
/// Parent txid prefixes (most txs have 0-2 parents)
pub depends: SmallVec<[TxidPrefix; 2]>,
}
impl Entry {
+51
View File
@@ -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<Option<Entry>>,
prefix_to_idx: FxHashMap<TxidPrefix, TxIndex>,
free_slots: Vec<TxIndex>,
}
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<Entry>] {
&self.entries
}
}
@@ -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};
@@ -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.
@@ -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<Entry>]) -
}
}
fee_rates.sort();
fee_rates.sort_unstable();
BlockStats {
tx_count: selected.len() as u32,
@@ -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<Option<Entry>>,
/// TxidPrefix -> slot index
txid_prefix_to_idx: FxHashMap<TxidPrefix, TxIndex>,
/// Recycled slot indices
free_indices: Vec<TxIndex>,
}
/// Mempool monitor.
///
/// Thread-safe wrapper around `MempoolInner`. Free to clone.
@@ -56,11 +46,9 @@ pub struct MempoolInner {
// Mempool state
info: RwLock<MempoolInfo>,
txs: RwLock<FxHashMap<Txid, TxWithHex>>,
txs: RwLock<TxStore>,
addresses: RwLock<AddressTracker>,
// Block building data (single lock for consistency)
block_state: RwLock<BlockBuildingState>,
entries: RwLock<EntryPool>,
// Projected blocks snapshot
snapshot: RwLock<Snapshot>,
@@ -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<Txid, TxWithHex>> {
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<Txid> = entries_info.iter().map(|e| e.txid.clone()).collect();
let new_txs = self.fetch_new_txs(&current_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<Txid>) -> FxHashMap<Txid, TxWithHex> {
fn fetch_new_txs(&self, entries_info: &[MempoolEntryInfo]) -> FxHashMap<Txid, TxWithHex> {
let txids_to_fetch: Vec<Txid> = {
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<Txid, TxWithHex>,
) -> bool {
let current_entries: FxHashMap<TxidPrefix, &MempoolEntryInfo> = entries_info
let entries_by_prefix: FxHashMap<TxidPrefix, &MempoolEntryInfo> = 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;
}
+44
View File
@@ -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<Txid, TxWithHex>);
impl Deref for TxStore {
type Target = FxHashMap<Txid, TxWithHex>;
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<Txid, TxWithHex>) {
self.0.extend(txs);
}
/// Keep items matching predicate, call `on_remove` for each removed item.
pub fn retain_or_remove<K, R>(&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
}
});
}
}
-6
View File
@@ -1,6 +0,0 @@
mod addresses;
mod entry;
mod monitor;
pub use entry::Entry;
pub use monitor::{Mempool, MempoolInner};
+2 -1
View File
@@ -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 }
+46 -3
View File
@@ -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<Vec<Txid>> {
let query = self.0.clone();
spawn_blocking(move || query.get_address_mempool_txids(address)).await?
}
pub async fn get_transaction(&self, txid: TxidPath) -> Result<Transaction> {
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<TxOutspend> {
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<Vec<TxOutspend>> {
let query = self.0.clone();
spawn_blocking(move || query.get_tx_outspends(txid)).await?
}
pub async fn get_block(&self, hash: String) -> Result<BlockInfo> {
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<BlockTimestamp> {
self.0.get_block_by_timestamp(timestamp)
}
pub async fn get_block_status(&self, hash: String) -> Result<BlockStatus> {
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<Vec<Transaction>> {
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<Txid> {
self.0.get_block_txid_at_index(&hash, index)
}
pub async fn get_block_raw(&self, hash: String) -> Result<Vec<u8>> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_raw(&hash)).await?
}
pub async fn get_mempool_info(&self) -> Result<MempoolInfo> {
self.0.get_mempool_info()
}
@@ -109,6 +143,15 @@ impl AsyncQuery {
self.0.get_recommended_fees()
}
pub async fn get_mempool_blocks(&self) -> Result<Vec<MempoolBlock>> {
self.0.get_mempool_blocks()
}
pub async fn get_difficulty_adjustment(&self) -> Result<DifficultyAdjustment> {
let query = self.0.clone();
spawn_blocking(move || query.get_difficulty_adjustment()).await?
}
pub async fn match_metric(&self, metric: Metric, limit: Limit) -> Result<Vec<&'static str>> {
let query = self.0.clone();
spawn_blocking(move || Ok(query.match_metric(&metric, limit))).await?
@@ -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<Vec<Txid>> {
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<Txid> = addresses
.get(&bytes)
.map(|(_, txids)| txids.iter().take(MAX_MEMPOOL_TXIDS).cloned().collect())
.unwrap_or_default();
Ok(txids)
}
+4
View File
@@ -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::*;
@@ -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,
}
}
@@ -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<BlockTimestamp> {
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,
})
}
+8
View File
@@ -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::*;
+29
View File
@@ -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<Vec<u8>> {
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)
}
@@ -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<Txid> {
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)
}
+45
View File
@@ -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<Vec<Transaction>> {
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)
}
@@ -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<Vec<MempoolBlock>> {
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)
}
@@ -1,7 +1,9 @@
mod blocks;
mod fees;
mod info;
mod txids;
pub use blocks::*;
pub use fees::*;
pub use info::*;
pub use txids::*;
@@ -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<DifficultyAdjustment> {
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,
})
}
+3
View File
@@ -0,0 +1,3 @@
mod difficulty;
pub use difficulty::*;
+2
View File
@@ -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::*;
+2
View File
@@ -1,7 +1,9 @@
mod hex;
mod outspend;
mod status;
mod tx;
pub use hex::*;
pub use outspend::*;
pub use status::*;
pub use tx::*;
+160
View File
@@ -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<TxOutspend> {
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<Vec<TxOutspend>> {
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<TxOutspend> {
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),
}),
})
}
+51 -7
View File
@@ -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<Vec<Txid>> {
get_address_mempool_txids(address, self)
}
pub fn get_transaction(&self, txid: TxidPath) -> Result<Transaction> {
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<TxOutspend> {
get_tx_outspend(txid, vout, self)
}
pub fn get_tx_outspends(&self, txid: TxidPath) -> Result<Vec<TxOutspend>> {
get_tx_outspends(txid, self)
}
pub fn get_block(&self, hash: &str) -> Result<BlockInfo> {
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<BlockTimestamp> {
get_block_by_timestamp(timestamp, self)
}
pub fn get_block_status(&self, hash: &str) -> Result<BlockStatus> {
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<Vec<Transaction>> {
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<Txid> {
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<Vec<u8>> {
let height = get_height_by_hash(hash, self)?;
get_block_raw(height, self)
}
pub fn get_mempool_info(&self) -> Result<MempoolInfo> {
get_mempool_info(self)
}
@@ -140,6 +176,14 @@ impl Query {
get_recommended_fees(self)
}
pub fn get_mempool_blocks(&self) -> Result<Vec<brk_types::MempoolBlock>> {
get_mempool_blocks(self)
}
pub fn get_difficulty_adjustment(&self) -> Result<brk_types::DifficultyAdjustment> {
get_difficulty_adjustment(self)
}
pub fn match_metric(&self, metric: &Metric, limit: Limit) -> Vec<&'static str> {
self.vecs().matches(metric, limit)
}
+68 -1
View File
@@ -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<AppState> {
.server_error()
),
)
.api_route(
"/api/address/{address}/txs/mempool",
get_with(async |
headers: HeaderMap,
Path(address): Path<Address>,
State(state): State<AppState>
| {
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::<Vec<Txid>>()
.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<Txid>)>,
State(state): State<AppState>
| {
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::<Vec<Txid>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
),
)
.api_route(
"/api/v1/validate-address/{address}",
get_with(async |
headers: HeaderMap,
Path(address): Path<String>,
State(state): State<AppState>
| {
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::<AddressValidation>()
.not_modified()
),
)
}
}
+111 -1
View File
@@ -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<AppState> {
},
),
)
.api_route(
"/api/block/{hash}/txs/{start_index}",
get_with(
async |headers: HeaderMap,
Path(path): Path<BlockHashStartIndexPath>,
State(state): State<AppState>| {
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::<Vec<Transaction>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/block/{hash}/txid/{index}",
get_with(
async |headers: HeaderMap,
Path(path): Path<BlockHashTxIndexPath>,
State(state): State<AppState>| {
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::<Txid>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/v1/mining/blocks/timestamp/{timestamp}",
get_with(
async |headers: HeaderMap,
Path(path): Path<TimestampPath>,
State(state): State<AppState>| {
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::<BlockTimestamp>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/block/{hash}/raw",
get_with(
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
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::<Vec<u8>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
}
}
+21 -1
View File
@@ -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<AppState> {
},
),
)
.api_route(
"/api/v1/fees/mempool-blocks",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
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::<Vec<MempoolBlock>>()
.not_modified()
.server_error()
},
),
)
}
}
+51
View File
@@ -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<AppState> {
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<AppState>| {
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::<DifficultyAdjustment>()
.not_modified()
.server_error()
},
),
)
}
}
+3 -1
View File
@@ -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<AppState> {
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")))
+56 -1
View File
@@ -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<AppState> {
.server_error(),
),
)
.api_route(
"/api/tx/{txid}/outspend/{vout}",
get_with(
async |
headers: HeaderMap,
Path(path): Path<TxidVoutPath>,
State(state): State<AppState>
| {
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::<TxOutspend>()
.not_modified()
.bad_request()
.not_found()
.server_error(),
),
)
.api_route(
"/api/tx/{txid}/outspends",
get_with(
async |
headers: HeaderMap,
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
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::<Vec<TxOutspend>>()
.not_modified()
.bad_request()
.not_found()
.server_error(),
),
)
}
}
@@ -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(),
);
}
}
@@ -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<u8>, etag: &str) -> Self;
fn new_bytes_with(status: StatusCode, value: Vec<u8>, etag: &str) -> Self;
}
impl ResponseExtended for Response<Body> {
@@ -68,4 +70,19 @@ impl ResponseExtended for Response<Body> {
headers.insert_etag(etag);
response
}
fn new_bytes(value: Vec<u8>, etag: &str) -> Self {
Self::new_bytes_with(StatusCode::default(), value, etag)
}
fn new_bytes_with(status: StatusCode, value: Vec<u8>, 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
}
}
+28
View File
@@ -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<T> {
fn to_text_response(self, etag: &str) -> Response
where
T: AsRef<str>;
fn to_display_response(self, etag: &str) -> Response
where
T: Display;
fn to_bytes_response(self, etag: &str) -> Response
where
T: Into<Vec<u8>>;
}
impl<T> ResultExtended<T> for Result<T> {
@@ -50,4 +58,24 @@ impl<T> ResultExtended<T> for Result<T> {
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<Vec<u8>>,
{
match self.with_status() {
Ok(value) => Response::new_bytes(value.into(), etag),
Err((status, message)) => Response::new_bytes_with(status, message.into_bytes(), etag),
}
}
}
+48
View File
@@ -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<String>,
/// The scriptPubKey in hex
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "scriptPubKey")]
pub script_pub_key: Option<String>,
/// Whether this is a script address (P2SH)
#[serde(skip_serializing_if = "Option::is_none")]
pub isscript: Option<bool>,
/// Whether this is a witness address
#[serde(skip_serializing_if = "Option::is_none")]
pub iswitness: Option<bool>,
/// Witness version (0 for P2WPKH/P2WSH, 1 for P2TR)
#[serde(skip_serializing_if = "Option::is_none")]
pub witness_version: Option<u8>,
/// Witness program in hex
#[serde(skip_serializing_if = "Option::is_none")]
pub witness_program: Option<String>,
}
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,
}
}
}
@@ -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,
}
@@ -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,
}
+17
View File
@@ -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,
}
@@ -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,
}
+6
View File
@@ -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 {
+22
View File
@@ -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::*;
+64
View File
@@ -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,
}
}
}
+20 -1
View File
@@ -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,
}
+2
View File
@@ -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,
)]
+15
View File
@@ -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<PoolStats>,
/// Total blocks in the time period
#[serde(rename = "blockCount")]
pub block_count: u32,
}
+19
View File
@@ -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,
}
+15 -2
View File
@@ -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;
+11
View File
@@ -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,
}
+15
View File
@@ -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,
}
+32
View File
@@ -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<Txid>,
/// Input index in the spending transaction (only present if spent)
#[serde(skip_serializing_if = "Option::is_none")]
pub vin: Option<Vin>,
/// Status of the spending transaction (only present if spent)
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<TxStatus>,
}
impl TxOutspend {
pub const UNSPENT: Self = Self {
spent: false,
txid: None,
vin: None,
status: None,
};
}
+4 -1
View File
@@ -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 {
+2 -1
View File
@@ -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,