mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-10 23:13:33 -07:00
mempool: snapshot 5 + query: new tools + server: endpoints
This commit is contained in:
Generated
+5
-4
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<()> {
|
||||
+15
-7
@@ -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)
|
||||
+1
-1
@@ -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).
|
||||
+5
-4
@@ -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 {
|
||||
@@ -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};
|
||||
+1
-1
@@ -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.
|
||||
+2
-2
@@ -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(¤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<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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
mod addresses;
|
||||
mod entry;
|
||||
mod monitor;
|
||||
|
||||
pub use entry::Entry;
|
||||
pub use monitor::{Mempool, MempoolInner};
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod difficulty;
|
||||
|
||||
pub use difficulty::*;
|
||||
@@ -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::*;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
mod hex;
|
||||
mod outspend;
|
||||
mod status;
|
||||
mod tx;
|
||||
|
||||
pub use hex::*;
|
||||
pub use outspend::*;
|
||||
pub use status::*;
|
||||
pub use tx::*;
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user