diff --git a/Cargo.lock b/Cargo.lock index 075024d03..ba2c494e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4189,7 +4189,7 @@ dependencies = [ [[package]] name = "rawdb" -version = "0.3.20" +version = "0.4.0" dependencies = [ "libc", "log", @@ -5370,7 +5370,7 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" [[package]] name = "vecdb" -version = "0.3.20" +version = "0.4.0" dependencies = [ "ctrlc", "log", @@ -5388,7 +5388,7 @@ dependencies = [ [[package]] name = "vecdb_derive" -version = "0.3.20" +version = "0.4.0" dependencies = [ "quote", "syn 2.0.111", diff --git a/crates/brk_binder/src/js.rs b/crates/brk_binder/src/js.rs index 0d908b1fa..df5a85fb9 100644 --- a/crates/brk_binder/src/js.rs +++ b/crates/brk_binder/src/js.rs @@ -63,8 +63,8 @@ export const POOL_ID_TO_POOL_NAME = /** @type {const} */ ({ contents += &sorted_pools .iter() .map(|pool| { - let id = pool.serialized_id(); - format!(" {id}: \"{}\",", pool.name) + let slug = pool.slug(); + format!(" {slug}: \"{}\",", pool.name) }) .collect::>() .join("\n"); diff --git a/crates/brk_computer/src/indexes.rs b/crates/brk_computer/src/indexes.rs index 67afacc52..c57807b2b 100644 --- a/crates/brk_computer/src/indexes.rs +++ b/crates/brk_computer/src/indexes.rs @@ -82,7 +82,6 @@ pub struct Vecs { pub txindex_to_input_count: EagerVec>, pub txindex_to_output_count: EagerVec>, pub txindex_to_txindex: LazyVecFrom1, - pub txinindex_to_txindex: EagerVec>, pub txinindex_to_txinindex: LazyVecFrom1, pub txinindex_to_txoutindex: EagerVec>, pub txoutindex_to_txoutindex: LazyVecFrom1, @@ -122,7 +121,6 @@ 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), @@ -253,13 +251,6 @@ impl Vecs { // TxInIndex // --- - self.txinindex_to_txindex.compute_finer( - starting_indexes.txinindex, - &indexer.vecs.tx.txindex_to_first_txinindex, - &indexer.vecs.txin.txinindex_to_outpoint, - exit, - )?; - let txindex_to_first_txoutindex = &indexer.vecs.tx.txindex_to_first_txoutindex; let txindex_to_first_txoutindex_reader = txindex_to_first_txoutindex.create_reader(); self.txinindex_to_txoutindex.compute_transform( diff --git a/crates/brk_computer/src/pools/mod.rs b/crates/brk_computer/src/pools/mod.rs index 005d9a9d2..8468391f9 100644 --- a/crates/brk_computer/src/pools/mod.rs +++ b/crates/brk_computer/src/pools/mod.rs @@ -4,7 +4,7 @@ use brk_error::Result; use brk_indexer::Indexer; use brk_store::AnyStore; use brk_traversable::Traversable; -use brk_types::{Address, AddressBytes, Height, OutputType, PoolId, Pools, TxOutIndex, pools}; +use brk_types::{Address, AddressBytes, Height, OutputType, PoolSlug, Pools, TxOutIndex, pools}; use rayon::prelude::*; use vecdb::{ AnyStoredVec, AnyVec, BytesVec, Database, Exit, GenericStoredVec, ImportableVec, IterableVec, @@ -24,8 +24,8 @@ pub struct Vecs { db: Database, pools: &'static Pools, - pub height_to_pool: BytesVec, - pub vecs: BTreeMap, + pub height_to_pool: BytesVec, + pub vecs: BTreeMap, } impl Vecs { @@ -48,13 +48,13 @@ impl Vecs { .map(|pool| { vecs::Vecs::forced_import( &db, - pool.id, + pool.slug, pools, version + Version::ZERO, indexes, price, ) - .map(|vecs| (pool.id, vecs)) + .map(|vecs| (pool.slug, vecs)) }) .collect::>>()?, pools, @@ -126,20 +126,36 @@ impl Vecs { let mut txindex_to_first_txoutindex_iter = indexer.vecs.tx.txindex_to_first_txoutindex.iter()?; let mut txindex_to_output_count_iter = indexes.txindex_to_output_count.iter(); - let mut txoutindex_to_outputtype_iter = indexer.vecs.txout.txoutindex_to_outputtype.iter()?; + let mut txoutindex_to_outputtype_iter = + indexer.vecs.txout.txoutindex_to_outputtype.iter()?; let mut txoutindex_to_typeindex_iter = indexer.vecs.txout.txoutindex_to_typeindex.iter()?; - let mut p2pk65addressindex_to_p2pk65bytes_iter = - indexer.vecs.address.p2pk65addressindex_to_p2pk65bytes.iter()?; - let mut p2pk33addressindex_to_p2pk33bytes_iter = - indexer.vecs.address.p2pk33addressindex_to_p2pk33bytes.iter()?; - let mut p2pkhaddressindex_to_p2pkhbytes_iter = - indexer.vecs.address.p2pkhaddressindex_to_p2pkhbytes.iter()?; + let mut p2pk65addressindex_to_p2pk65bytes_iter = indexer + .vecs + .address + .p2pk65addressindex_to_p2pk65bytes + .iter()?; + let mut p2pk33addressindex_to_p2pk33bytes_iter = indexer + .vecs + .address + .p2pk33addressindex_to_p2pk33bytes + .iter()?; + let mut p2pkhaddressindex_to_p2pkhbytes_iter = indexer + .vecs + .address + .p2pkhaddressindex_to_p2pkhbytes + .iter()?; let mut p2shaddressindex_to_p2shbytes_iter = indexer.vecs.address.p2shaddressindex_to_p2shbytes.iter()?; - let mut p2wpkhaddressindex_to_p2wpkhbytes_iter = - indexer.vecs.address.p2wpkhaddressindex_to_p2wpkhbytes.iter()?; - let mut p2wshaddressindex_to_p2wshbytes_iter = - indexer.vecs.address.p2wshaddressindex_to_p2wshbytes.iter()?; + let mut p2wpkhaddressindex_to_p2wpkhbytes_iter = indexer + .vecs + .address + .p2wpkhaddressindex_to_p2wpkhbytes + .iter()?; + let mut p2wshaddressindex_to_p2wshbytes_iter = indexer + .vecs + .address + .p2wshaddressindex_to_p2wshbytes + .iter()?; let mut p2traddressindex_to_p2trbytes_iter = indexer.vecs.address.p2traddressindex_to_p2trbytes.iter()?; let mut p2aaddressindex_to_p2abytes_iter = @@ -201,7 +217,7 @@ impl Vecs { .or_else(|| self.pools.find_from_coinbase_tag(&coinbase_tag)) .unwrap_or(unknown); - self.height_to_pool.push_if_needed(height, pool.id)?; + self.height_to_pool.push_if_needed(height, pool.slug)?; Ok(()) })?; diff --git a/crates/brk_computer/src/pools/vecs.rs b/crates/brk_computer/src/pools/vecs.rs index e9c711711..9e512a8b2 100644 --- a/crates/brk_computer/src/pools/vecs.rs +++ b/crates/brk_computer/src/pools/vecs.rs @@ -1,6 +1,6 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Height, PoolId, Pools, Sats, StoredF32, StoredU16, StoredU32}; +use brk_types::{Height, PoolSlug, Pools, Sats, StoredF32, StoredU16, StoredU32}; use vecdb::{Database, Exit, GenericStoredVec, IterableVec, VecIndex, Version}; use crate::{ @@ -16,7 +16,7 @@ use crate::{ #[derive(Clone, Traversable)] pub struct Vecs { - id: PoolId, + slug: PoolSlug, pub indexes_to_blocks_mined: ComputedVecsFromHeight, pub indexes_to_1w_blocks_mined: ComputedVecsFromDateIndex, @@ -36,15 +36,13 @@ pub struct Vecs { impl Vecs { pub fn forced_import( db: &Database, - id: PoolId, - pools: &Pools, + slug: PoolSlug, + _pools: &Pools, parent_version: Version, indexes: &indexes::Vecs, price: Option<&price::Vecs>, ) -> Result { - let pool = pools.get(id); - let name = pool.serialized_id(); - let suffix = |s: &str| format!("{name}_{s}"); + let suffix = |s: &str| format!("{}_{s}", slug.to_string()); let compute_dollars = price.is_some(); let version = parent_version + Version::ZERO; @@ -65,7 +63,7 @@ impl Vecs { } Ok(Self { - id, + slug, indexes_to_blocks_mined: ComputedVecsFromHeight::forced_import( db, &suffix("blocks_mined"), @@ -118,7 +116,7 @@ impl Vecs { &mut self, indexes: &indexes::Vecs, starting_indexes: &Indexes, - height_to_pool: &impl IterableVec, + height_to_pool: &impl IterableVec, chain: &chain::Vecs, price: Option<&price::Vecs>, exit: &Exit, @@ -131,7 +129,7 @@ impl Vecs { |(h, id, ..)| { ( h, - if id == self.id { + if id == self.slug { StoredU32::ONE } else { StoredU32::ZERO diff --git a/crates/brk_indexer/src/processor.rs b/crates/brk_indexer/src/processor.rs index 47d3b1c39..a5bcd04c1 100644 --- a/crates/brk_indexer/src/processor.rs +++ b/crates/brk_indexer/src/processor.rs @@ -595,6 +595,10 @@ impl<'a> BlockProcessor<'a> { .push_if_needed(txindex, txinindex)?; } + self.vecs + .txin + .txinindex_to_txindex + .push_if_needed(txinindex, txindex)?; self.vecs .txin .txinindex_to_outpoint diff --git a/crates/brk_indexer/src/vecs/txin.rs b/crates/brk_indexer/src/vecs/txin.rs index e9aa4c8ee..b79176e30 100644 --- a/crates/brk_indexer/src/vecs/txin.rs +++ b/crates/brk_indexer/src/vecs/txin.rs @@ -1,12 +1,13 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Height, OutPoint, TxInIndex, Version}; +use brk_types::{Height, OutPoint, TxInIndex, TxIndex, Version}; use vecdb::{AnyStoredVec, Database, GenericStoredVec, ImportableVec, PcoVec, Stamp}; #[derive(Clone, Traversable)] pub struct TxinVecs { pub height_to_first_txinindex: PcoVec, pub txinindex_to_outpoint: PcoVec, + pub txinindex_to_txindex: PcoVec, } impl TxinVecs { @@ -14,6 +15,7 @@ impl TxinVecs { Ok(Self { height_to_first_txinindex: PcoVec::forced_import(db, "first_txinindex", version)?, txinindex_to_outpoint: PcoVec::forced_import(db, "outpoint", version)?, + txinindex_to_txindex: PcoVec::forced_import(db, "txindex", version)?, }) } @@ -22,6 +24,8 @@ impl TxinVecs { .truncate_if_needed_with_stamp(height, stamp)?; self.txinindex_to_outpoint .truncate_if_needed_with_stamp(txinindex, stamp)?; + self.txinindex_to_txindex + .truncate_if_needed_with_stamp(txinindex, stamp)?; Ok(()) } @@ -29,6 +33,7 @@ impl TxinVecs { [ &mut self.height_to_first_txinindex as &mut dyn AnyStoredVec, &mut self.txinindex_to_outpoint, + &mut self.txinindex_to_txindex, ] .into_iter() } diff --git a/crates/brk_mempool/src/sync.rs b/crates/brk_mempool/src/sync.rs index 112cb6ac9..e816de983 100644 --- a/crates/brk_mempool/src/sync.rs +++ b/crates/brk_mempool/src/sync.rs @@ -11,7 +11,7 @@ use brk_error::Result; use brk_rpc::Client; use brk_types::{MempoolEntryInfo, MempoolInfo, TxWithHex, Txid, TxidPrefix}; use derive_deref::Deref; -use log::{error, info}; +use log::{debug, error}; use parking_lot::{RwLock, RwLockReadGuard}; use rustc_hash::FxHashMap; @@ -225,7 +225,7 @@ impl MempoolInner { let i = Instant::now(); self.rebuild_projected_blocks(); - info!("mempool: rebuild_projected_blocks in {:?}", i.elapsed()); + debug!("mempool: rebuild_projected_blocks in {:?}", i.elapsed()); } /// Rebuild projected blocks snapshot. diff --git a/crates/brk_query/src/async.rs b/crates/brk_query/src/async.rs index 2d228b4ae..20cb202ae 100644 --- a/crates/brk_query/src/async.rs +++ b/crates/brk_query/src/async.rs @@ -6,8 +6,9 @@ use brk_indexer::Indexer; use brk_mempool::Mempool; use brk_reader::Reader; use brk_types::{ - Address, AddressStats, BlockInfo, BlockStatus, BlockTimestamp, DifficultyAdjustment, Height, - Index, IndexInfo, Limit, MempoolBlock, MempoolInfo, Metric, MetricCount, RecommendedFees, + Address, AddressStats, BlockInfo, BlockStatus, BlockTimestamp, DifficultyAdjustment, + HashrateSummary, Height, Index, IndexInfo, Limit, MempoolBlock, MempoolInfo, Metric, + MetricCount, PoolDetail, PoolInfo, PoolSlug, PoolsSummary, RecommendedFees, TimePeriod, Timestamp, Transaction, TreeNode, TxOutspend, TxStatus, Txid, TxidPath, Utxo, Vout, }; use tokio::task::spawn_blocking; @@ -117,7 +118,11 @@ impl AsyncQuery { spawn_blocking(move || query.get_block_txids(&hash)).await? } - pub async fn get_block_txs(&self, hash: String, start_index: usize) -> Result> { + pub async fn get_block_txs( + &self, + hash: String, + start_index: usize, + ) -> Result> { let query = self.0.clone(); spawn_blocking(move || query.get_block_txs(&hash, start_index)).await? } @@ -152,6 +157,70 @@ impl AsyncQuery { spawn_blocking(move || query.get_difficulty_adjustment()).await? } + pub async fn get_mining_pools(&self, time_period: TimePeriod) -> Result { + let query = self.0.clone(); + spawn_blocking(move || query.get_mining_pools(time_period)).await? + } + + pub async fn get_all_pools(&self) -> Result> { + Ok(self.0.get_all_pools()) + } + + pub async fn get_pool_detail(&self, slug: PoolSlug) -> Result { + let query = self.0.clone(); + spawn_blocking(move || query.get_pool_detail(slug)).await? + } + + pub async fn get_hashrate(&self, time_period: Option) -> Result { + let query = self.0.clone(); + spawn_blocking(move || query.get_hashrate(time_period)).await? + } + + pub async fn get_difficulty_adjustments( + &self, + time_period: Option, + ) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_difficulty_adjustments(time_period)).await? + } + + pub async fn get_block_fees( + &self, + time_period: TimePeriod, + ) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_block_fees(time_period)).await? + } + + pub async fn get_block_rewards( + &self, + time_period: TimePeriod, + ) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_block_rewards(time_period)).await? + } + + pub async fn get_block_fee_rates( + &self, + time_period: TimePeriod, + ) -> Result> { + let query = self.0.clone(); + spawn_blocking(move || query.get_block_fee_rates(time_period)).await? + } + + pub async fn get_block_sizes_weights( + &self, + time_period: TimePeriod, + ) -> Result { + let query = self.0.clone(); + spawn_blocking(move || query.get_block_sizes_weights(time_period)).await? + } + + pub async fn get_reward_stats(&self, block_count: usize) -> Result { + let query = self.0.clone(); + spawn_blocking(move || query.get_reward_stats(block_count)).await? + } + pub async fn match_metric(&self, metric: Metric, limit: Limit) -> Result> { let query = self.0.clone(); spawn_blocking(move || Ok(query.match_metric(&metric, limit))).await? diff --git a/crates/brk_query/src/chain/addr/txids.rs b/crates/brk_query/src/chain/addr/txids.rs index 66098c0bf..61efc29a5 100644 --- a/crates/brk_query/src/chain/addr/txids.rs +++ b/crates/brk_query/src/chain/addr/txids.rs @@ -41,13 +41,13 @@ pub fn get_address_txids( .rev() .filter(|(key, _): &(AddressIndexTxIndex, Unit)| { if let Some(after) = after_txindex { - TxIndex::from(key.txindex()) < after + key.txindex() < after } else { true } }) .take(limit) - .map(|(key, _)| TxIndex::from(key.txindex())) + .map(|(key, _)| key.txindex()) .collect(); let mut txindex_to_txid_iter = indexer.vecs.tx.txindex_to_txid.iter()?; diff --git a/crates/brk_query/src/chain/block/by_timestamp.rs b/crates/brk_query/src/chain/block/by_timestamp.rs index 1ad5d772c..95db42309 100644 --- a/crates/brk_query/src/chain/block/by_timestamp.rs +++ b/crates/brk_query/src/chain/block/by_timestamp.rs @@ -1,7 +1,7 @@ use brk_error::{Error, Result}; use brk_types::{BlockTimestamp, Date, DateIndex, Height, Timestamp}; use jiff::Timestamp as JiffTimestamp; -use vecdb::{AnyVec, GenericStoredVec, TypedVecIterator}; +use vecdb::{GenericStoredVec, TypedVecIterator}; use crate::Query; @@ -59,7 +59,12 @@ pub fn get_block_by_timestamp(timestamp: Timestamp, query: &Query) -> Result Result> { + let computer = query.computer(); + let current_height = query.get_height(); + let start = current_height + .to_usize() + .saturating_sub(time_period.block_count()); + + let iter = DateIndexIter::new(computer, start, current_height.to_usize()); + + let vecs = &computer.chain.indexes_to_fee_rate.dateindex; + let mut min = vecs.unwrap_min().iter(); + let mut pct10 = vecs.unwrap_pct10().iter(); + let mut pct25 = vecs.unwrap_pct25().iter(); + let mut median = vecs.unwrap_median().iter(); + let mut pct75 = vecs.unwrap_pct75().iter(); + let mut pct90 = vecs.unwrap_pct90().iter(); + let mut max = vecs.unwrap_max().iter(); + + Ok(iter.collect(|di, ts, h| { + Some(BlockFeeRatesEntry { + avg_height: h.into(), + timestamp: *ts as u32, + percentiles: FeeRatePercentiles::new( + min.get(di).unwrap_or_default(), + pct10.get(di).unwrap_or_default(), + pct25.get(di).unwrap_or_default(), + median.get(di).unwrap_or_default(), + pct75.get(di).unwrap_or_default(), + pct90.get(di).unwrap_or_default(), + max.get(di).unwrap_or_default(), + ), + }) + })) +} diff --git a/crates/brk_query/src/chain/mining/block_fees.rs b/crates/brk_query/src/chain/mining/block_fees.rs new file mode 100644 index 000000000..45077ffb0 --- /dev/null +++ b/crates/brk_query/src/chain/mining/block_fees.rs @@ -0,0 +1,32 @@ +use brk_error::Result; +use brk_types::{BlockFeesEntry, TimePeriod}; +use vecdb::{IterableVec, VecIndex}; + +use super::dateindex_iter::DateIndexIter; +use crate::Query; + +pub fn get_block_fees(time_period: TimePeriod, query: &Query) -> Result> { + let computer = query.computer(); + let current_height = query.get_height(); + let start = current_height + .to_usize() + .saturating_sub(time_period.block_count()); + + let iter = DateIndexIter::new(computer, start, current_height.to_usize()); + + let mut fees = computer + .chain + .indexes_to_fee + .sats + .dateindex + .unwrap_average() + .iter(); + + Ok(iter.collect(|di, ts, h| { + fees.get(di).map(|fee| BlockFeesEntry { + avg_height: h.into(), + timestamp: *ts as u32, + avg_fees: u64::from(*fee), + }) + })) +} diff --git a/crates/brk_query/src/chain/mining/block_rewards.rs b/crates/brk_query/src/chain/mining/block_rewards.rs new file mode 100644 index 000000000..6ae1024fb --- /dev/null +++ b/crates/brk_query/src/chain/mining/block_rewards.rs @@ -0,0 +1,33 @@ +use brk_error::Result; +use brk_types::{BlockRewardsEntry, TimePeriod}; +use vecdb::{IterableVec, VecIndex}; + +use super::dateindex_iter::DateIndexIter; +use crate::Query; + +pub fn get_block_rewards(time_period: TimePeriod, query: &Query) -> Result> { + let computer = query.computer(); + let current_height = query.get_height(); + let start = current_height + .to_usize() + .saturating_sub(time_period.block_count()); + + let iter = DateIndexIter::new(computer, start, current_height.to_usize()); + + // coinbase = subsidy + fees + let mut rewards = computer + .chain + .indexes_to_coinbase + .sats + .dateindex + .unwrap_average() + .iter(); + + Ok(iter.collect(|di, ts, h| { + rewards.get(di).map(|reward| BlockRewardsEntry { + avg_height: h.into(), + timestamp: *ts as u32, + avg_rewards: u64::from(*reward), + }) + })) +} diff --git a/crates/brk_query/src/chain/mining/block_sizes_weights.rs b/crates/brk_query/src/chain/mining/block_sizes_weights.rs new file mode 100644 index 000000000..518c9e76b --- /dev/null +++ b/crates/brk_query/src/chain/mining/block_sizes_weights.rs @@ -0,0 +1,62 @@ +use brk_error::Result; +use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod}; +use vecdb::{IterableVec, VecIndex}; + +use super::dateindex_iter::DateIndexIter; +use crate::Query; + +pub fn get_block_sizes_weights( + time_period: TimePeriod, + query: &Query, +) -> Result { + let computer = query.computer(); + let current_height = query.get_height(); + let start = current_height + .to_usize() + .saturating_sub(time_period.block_count()); + + let iter = DateIndexIter::new(computer, start, current_height.to_usize()); + + let mut sizes_vec = computer + .chain + .indexes_to_block_size + .dateindex + .unwrap_average() + .iter(); + let mut weights_vec = computer + .chain + .indexes_to_block_weight + .dateindex + .unwrap_average() + .iter(); + + let entries: Vec<_> = iter.collect(|di, ts, h| { + let size = sizes_vec.get(di).map(|s| u64::from(*s)); + let weight = weights_vec.get(di).map(|w| u64::from(*w)); + Some((h.into(), *ts as u32, size, weight)) + }); + + let sizes = entries + .iter() + .filter_map(|(h, ts, size, _)| { + size.map(|s| BlockSizeEntry { + avg_height: *h, + timestamp: *ts, + avg_size: s, + }) + }) + .collect(); + + let weights = entries + .iter() + .filter_map(|(h, ts, _, weight)| { + weight.map(|w| BlockWeightEntry { + avg_height: *h, + timestamp: *ts, + avg_weight: w, + }) + }) + .collect(); + + Ok(BlockSizesWeights { sizes, weights }) +} diff --git a/crates/brk_query/src/chain/mining/dateindex_iter.rs b/crates/brk_query/src/chain/mining/dateindex_iter.rs new file mode 100644 index 000000000..ba7155aed --- /dev/null +++ b/crates/brk_query/src/chain/mining/dateindex_iter.rs @@ -0,0 +1,67 @@ +use brk_computer::Computer; +use brk_types::{DateIndex, Height, Timestamp}; +use vecdb::{GenericStoredVec, IterableVec, VecIndex}; + +/// Helper for iterating over dateindex ranges with sampling. +pub struct DateIndexIter<'a> { + computer: &'a Computer, + start_di: DateIndex, + end_di: DateIndex, + step: usize, +} + +impl<'a> DateIndexIter<'a> { + pub fn new(computer: &'a Computer, start_height: usize, end_height: usize) -> Self { + let start_di = computer + .indexes + .height_to_dateindex + .read_once(Height::from(start_height)) + .unwrap_or_default(); + let end_di = computer + .indexes + .height_to_dateindex + .read_once(Height::from(end_height)) + .unwrap_or_default(); + + let total = end_di.to_usize().saturating_sub(start_di.to_usize()) + 1; + let step = (total / 200).max(1); + + Self { + computer, + start_di, + end_di, + step, + } + } + + /// Iterate and collect entries using the provided transform function. + pub fn collect(&self, mut transform: F) -> Vec + where + F: FnMut(DateIndex, Timestamp, Height) -> Option, + { + let total = self.end_di.to_usize().saturating_sub(self.start_di.to_usize()) + 1; + let mut timestamps = self + .computer + .chain + .timeindexes_to_timestamp + .dateindex_extra + .unwrap_first() + .iter(); + let mut heights = self.computer.indexes.dateindex_to_first_height.iter(); + + let mut entries = Vec::with_capacity(total / self.step + 1); + let mut i = self.start_di.to_usize(); + + while i <= self.end_di.to_usize() { + let di = DateIndex::from(i); + if let (Some(ts), Some(h)) = (timestamps.get(di), heights.get(di)) { + if let Some(entry) = transform(di, ts, h) { + entries.push(entry); + } + } + i += self.step; + } + + entries + } +} diff --git a/crates/brk_query/src/chain/mining/difficulty_adjustments.rs b/crates/brk_query/src/chain/mining/difficulty_adjustments.rs new file mode 100644 index 000000000..d35a3f13a --- /dev/null +++ b/crates/brk_query/src/chain/mining/difficulty_adjustments.rs @@ -0,0 +1,26 @@ +use brk_error::Result; +use brk_types::{DifficultyAdjustmentEntry, TimePeriod}; +use vecdb::VecIndex; + +use crate::Query; + +use super::epochs::iter_difficulty_epochs; + +/// Get historical difficulty adjustments. +pub fn get_difficulty_adjustments( + time_period: Option, + query: &Query, +) -> Result> { + let current_height = query.get_height(); + let end = current_height.to_usize(); + let start = match time_period { + Some(tp) => end.saturating_sub(tp.block_count()), + None => 0, + }; + + let mut entries = iter_difficulty_epochs(query.computer(), start, end); + + // Return in reverse chronological order (newest first) + entries.reverse(); + Ok(entries) +} diff --git a/crates/brk_query/src/chain/mining/epochs.rs b/crates/brk_query/src/chain/mining/epochs.rs new file mode 100644 index 000000000..3ed6bdd44 --- /dev/null +++ b/crates/brk_query/src/chain/mining/epochs.rs @@ -0,0 +1,63 @@ +use brk_computer::Computer; +use brk_types::{DifficultyAdjustmentEntry, DifficultyEpoch, Height}; +use vecdb::{GenericStoredVec, IterableVec, VecIndex}; + +/// Iterate over difficulty epochs within a height range. +pub fn iter_difficulty_epochs( + computer: &Computer, + start_height: usize, + end_height: usize, +) -> Vec { + let start_epoch = computer + .indexes + .height_to_difficultyepoch + .read_once(Height::from(start_height)) + .unwrap_or_default(); + let end_epoch = computer + .indexes + .height_to_difficultyepoch + .read_once(Height::from(end_height)) + .unwrap_or_default(); + + let mut epoch_to_height_iter = computer.indexes.difficultyepoch_to_first_height.iter(); + let mut epoch_to_timestamp_iter = computer.chain.difficultyepoch_to_timestamp.iter(); + let mut epoch_to_difficulty_iter = computer + .chain + .indexes_to_difficulty + .difficultyepoch + .unwrap_last() + .iter(); + + let mut results = Vec::with_capacity(end_epoch.to_usize() - start_epoch.to_usize() + 1); + let mut prev_difficulty: Option = None; + + for epoch_usize in start_epoch.to_usize()..=end_epoch.to_usize() { + let epoch = DifficultyEpoch::from(epoch_usize); + let epoch_height = epoch_to_height_iter.get(epoch).unwrap_or_default(); + + // Skip epochs before our start height but track difficulty + if epoch_height.to_usize() < start_height { + prev_difficulty = epoch_to_difficulty_iter.get(epoch).map(|d| *d); + continue; + } + + let epoch_timestamp = epoch_to_timestamp_iter.get(epoch).unwrap_or_default(); + let epoch_difficulty = *epoch_to_difficulty_iter.get(epoch).unwrap_or_default(); + + let change_percent = match prev_difficulty { + Some(prev) if prev > 0.0 => ((epoch_difficulty / prev) - 1.0) * 100.0, + _ => 0.0, + }; + + results.push(DifficultyAdjustmentEntry { + timestamp: epoch_timestamp, + height: epoch_height, + difficulty: epoch_difficulty, + change_percent, + }); + + prev_difficulty = Some(epoch_difficulty); + } + + results +} diff --git a/crates/brk_query/src/chain/mining/hashrate.rs b/crates/brk_query/src/chain/mining/hashrate.rs new file mode 100644 index 000000000..cd3787300 --- /dev/null +++ b/crates/brk_query/src/chain/mining/hashrate.rs @@ -0,0 +1,99 @@ +use brk_error::Result; +use brk_types::{DateIndex, DifficultyEntry, HashrateEntry, HashrateSummary, Height, TimePeriod}; +use vecdb::{GenericStoredVec, IterableVec, VecIndex}; + +use super::epochs::iter_difficulty_epochs; +use crate::Query; + +/// Get hashrate and difficulty data for a time period. +pub fn get_hashrate(time_period: Option, query: &Query) -> Result { + let indexer = query.indexer(); + let computer = query.computer(); + let current_height = query.get_height(); + + // Get current difficulty + let current_difficulty = *indexer + .vecs + .block + .height_to_difficulty + .read_once(current_height)?; + + // Get current hashrate + let current_dateindex = computer + .indexes + .height_to_dateindex + .read_once(current_height)?; + let current_hashrate = *computer + .chain + .indexes_to_hash_rate + .dateindex + .unwrap_last() + .read_once(current_dateindex)? as u128; + + // Calculate start height based on time period + let end = current_height.to_usize(); + let start = match time_period { + Some(tp) => end.saturating_sub(tp.block_count()), + None => 0, + }; + + // Get hashrate entries using iterators for efficiency + let start_dateindex = computer + .indexes + .height_to_dateindex + .read_once(Height::from(start))?; + let end_dateindex = current_dateindex; + + // Sample at regular intervals to avoid too many data points + let total_days = end_dateindex + .to_usize() + .saturating_sub(start_dateindex.to_usize()) + + 1; + let step = (total_days / 200).max(1); // Max ~200 data points + + // Create iterators for the loop + let mut hashrate_iter = computer + .chain + .indexes_to_hash_rate + .dateindex + .unwrap_last() + .iter(); + let mut timestamp_iter = computer + .chain + .timeindexes_to_timestamp + .dateindex_extra + .unwrap_first() + .iter(); + + let mut hashrates = Vec::with_capacity(total_days / step + 1); + let mut di = start_dateindex.to_usize(); + while di <= end_dateindex.to_usize() { + let dateindex = DateIndex::from(di); + if let (Some(hr), Some(timestamp)) = + (hashrate_iter.get(dateindex), timestamp_iter.get(dateindex)) + { + hashrates.push(HashrateEntry { + timestamp, + avg_hashrate: (*hr) as u128, + }); + } + di += step; + } + + // Get difficulty adjustments within the period + let difficulty: Vec = iter_difficulty_epochs(computer, start, end) + .into_iter() + .map(|e| DifficultyEntry { + timestamp: e.timestamp, + difficulty: e.difficulty, + height: e.height, + }) + .collect(); + + Ok(HashrateSummary { + hashrates, + difficulty, + current_hashrate, + current_difficulty, + }) +} diff --git a/crates/brk_query/src/chain/mining/mod.rs b/crates/brk_query/src/chain/mining/mod.rs index c2e5ec30a..dd1412bf1 100644 --- a/crates/brk_query/src/chain/mining/mod.rs +++ b/crates/brk_query/src/chain/mining/mod.rs @@ -1,3 +1,21 @@ +mod block_fee_rates; +mod block_fees; +mod block_rewards; +mod block_sizes_weights; +mod dateindex_iter; mod difficulty; +mod difficulty_adjustments; +mod epochs; +mod hashrate; +mod pools; +mod reward_stats; +pub use block_fee_rates::*; +pub use block_fees::*; +pub use block_rewards::*; +pub use block_sizes_weights::*; pub use difficulty::*; +pub use difficulty_adjustments::*; +pub use hashrate::*; +pub use pools::*; +pub use reward_stats::*; diff --git a/crates/brk_query/src/chain/mining/pools.rs b/crates/brk_query/src/chain/mining/pools.rs new file mode 100644 index 000000000..c3331dd78 --- /dev/null +++ b/crates/brk_query/src/chain/mining/pools.rs @@ -0,0 +1,172 @@ +use brk_error::{Error, Result}; +use brk_types::{ + Height, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo, PoolInfo, PoolSlug, + PoolStats, PoolsSummary, TimePeriod, pools, +}; +use vecdb::{AnyVec, GenericStoredVec, IterableVec, VecIndex}; + +use crate::Query; + +/// Get mining pool statistics for a time period using pre-computed cumulative counts. +pub fn get_mining_pools(time_period: TimePeriod, query: &Query) -> Result { + let computer = query.computer(); + let current_height = query.get_height(); + let end = current_height.to_usize(); + + // No blocks indexed yet + if computer.pools.height_to_pool.len() == 0 { + return Ok(PoolsSummary { + pools: vec![], + block_count: 0, + last_estimated_hashrate: 0, + }); + } + + // Calculate start height based on time period + let start = end.saturating_sub(time_period.block_count()); + + let pools = pools(); + let mut pool_data: Vec<(&'static brk_types::Pool, u32)> = Vec::new(); + + // For each pool, get cumulative count at end and start, subtract to get range count + for (pool_id, pool_vecs) in &computer.pools.vecs { + let mut cumulative = pool_vecs + .indexes_to_blocks_mined + .height_extra + .unwrap_cumulative() + .iter(); + + let count_at_end: u32 = *cumulative.get(current_height).unwrap_or_default(); + + let count_at_start: u32 = if start == 0 { + 0 + } else { + *cumulative.get(Height::from(start - 1)).unwrap_or_default() + }; + + let block_count = count_at_end.saturating_sub(count_at_start); + + // Only include pools that mined at least one block in the period + if block_count > 0 { + pool_data.push((pools.get(*pool_id), block_count)); + } + } + + // Sort by block count descending + pool_data.sort_by(|a, b| b.1.cmp(&a.1)); + + let total_blocks: u32 = pool_data.iter().map(|(_, count)| count).sum(); + + // Build stats with ranks + let pool_stats: Vec = pool_data + .into_iter() + .enumerate() + .map(|(idx, (pool, block_count))| { + let share = if total_blocks > 0 { + block_count as f64 / total_blocks as f64 + } else { + 0.0 + }; + PoolStats::new(pool, block_count, (idx + 1) as u32, share) + }) + .collect(); + + // TODO: Calculate actual hashrate from difficulty + let last_estimated_hashrate = 0u128; + + Ok(PoolsSummary { + pools: pool_stats, + block_count: total_blocks, + last_estimated_hashrate, + }) +} + +/// Get list of all known mining pools (no statistics). +pub fn get_all_pools() -> Vec { + pools().iter().map(PoolInfo::from).collect() +} + +/// Get detailed information about a specific pool by slug. +pub fn get_pool_detail(slug: PoolSlug, query: &Query) -> Result { + let computer = query.computer(); + let current_height = query.get_height(); + let end = current_height.to_usize(); + + let pools_list = pools(); + let pool = pools_list.get(slug); + + // Get pool vecs for this specific pool + let pool_vecs = computer + .pools + .vecs + .get(&slug) + .ok_or_else(|| Error::Str("Pool data not found"))?; + + let mut cumulative = pool_vecs + .indexes_to_blocks_mined + .height_extra + .unwrap_cumulative() + .iter(); + + // Get total blocks (all time) + let total_all: u32 = *cumulative.get(current_height).unwrap_or_default(); + + // Get blocks for 24h (144 blocks) + let start_24h = end.saturating_sub(144); + let count_before_24h: u32 = if start_24h == 0 { + 0 + } else { + *cumulative + .get(Height::from(start_24h - 1)) + .unwrap_or_default() + }; + let total_24h = total_all.saturating_sub(count_before_24h); + + // Get blocks for 1w (1008 blocks) + let start_1w = end.saturating_sub(1008); + let count_before_1w: u32 = if start_1w == 0 { + 0 + } else { + *cumulative + .get(Height::from(start_1w - 1)) + .unwrap_or_default() + }; + let total_1w = total_all.saturating_sub(count_before_1w); + + // Calculate total network blocks for share calculation + let network_blocks_all = (end + 1) as u32; + let network_blocks_24h = (end - start_24h + 1) as u32; + let network_blocks_1w = (end - start_1w + 1) as u32; + + let share_all = if network_blocks_all > 0 { + total_all as f64 / network_blocks_all as f64 + } else { + 0.0 + }; + let share_24h = if network_blocks_24h > 0 { + total_24h as f64 / network_blocks_24h as f64 + } else { + 0.0 + }; + let share_1w = if network_blocks_1w > 0 { + total_1w as f64 / network_blocks_1w as f64 + } else { + 0.0 + }; + + Ok(PoolDetail { + pool: PoolDetailInfo::from(pool), + block_count: PoolBlockCounts { + all: total_all, + day: total_24h, + week: total_1w, + }, + block_share: PoolBlockShares { + all: share_all, + day: share_24h, + week: share_1w, + }, + estimated_hashrate: 0, // TODO: Calculate from share and network hashrate + reported_hashrate: None, + }) +} diff --git a/crates/brk_query/src/chain/mining/reward_stats.rs b/crates/brk_query/src/chain/mining/reward_stats.rs new file mode 100644 index 000000000..55a079362 --- /dev/null +++ b/crates/brk_query/src/chain/mining/reward_stats.rs @@ -0,0 +1,58 @@ +use brk_error::Result; +use brk_types::{Height, RewardStats, Sats}; +use vecdb::{IterableVec, VecIndex}; + +use crate::Query; + +pub fn get_reward_stats(block_count: usize, query: &Query) -> Result { + let computer = query.computer(); + let current_height = query.get_height(); + + let end_block = current_height; + let start_block = Height::from(current_height.to_usize().saturating_sub(block_count - 1)); + + let mut coinbase_iter = computer + .chain + .indexes_to_coinbase + .sats + .height + .as_ref() + .unwrap() + .iter(); + let mut fee_iter = computer.chain.indexes_to_fee.sats.height.unwrap_sum().iter(); + let mut tx_count_iter = computer + .chain + .indexes_to_tx_count + .height + .as_ref() + .unwrap() + .iter(); + + let mut total_reward = Sats::ZERO; + let mut total_fee = Sats::ZERO; + let mut total_tx: u64 = 0; + + for height in start_block.to_usize()..=end_block.to_usize() { + let h = Height::from(height); + + if let Some(coinbase) = coinbase_iter.get(h) { + total_reward += Sats::from(u64::from(*coinbase)); + } + + if let Some(fee) = fee_iter.get(h) { + total_fee += Sats::from(u64::from(*fee)); + } + + if let Some(tx_count) = tx_count_iter.get(h) { + total_tx += u64::from(*tx_count); + } + } + + Ok(RewardStats { + start_block, + end_block, + total_reward, + total_fee, + total_tx, + }) +} diff --git a/crates/brk_query/src/chain/tx/outspend.rs b/crates/brk_query/src/chain/tx/outspend.rs index cba52206d..da753244a 100644 --- a/crates/brk_query/src/chain/tx/outspend.rs +++ b/crates/brk_query/src/chain/tx/outspend.rs @@ -47,7 +47,10 @@ pub fn get_tx_outspend( // Look up spend status let computer = query.computer(); - let txinindex = computer.stateful.txoutindex_to_txinindex.read_once(txoutindex)?; + let txinindex = computer + .stateful + .txoutindex_to_txinindex + .read_once(txoutindex)?; if txinindex == TxInIndex::UNSPENT { return Ok(TxOutspend::UNSPENT); @@ -119,10 +122,13 @@ pub fn get_tx_outspends(TxidPath { txid }: TxidPath, query: &Query) -> Result Result { let indexer = query.indexer(); - let computer = query.computer(); // Look up spending txindex directly - let spending_txindex = computer.indexes.txinindex_to_txindex.read_once(txinindex)?; + let spending_txindex = indexer + .vecs + .txin + .txinindex_to_txindex + .read_once(txinindex)?; // Calculate vin let spending_first_txinindex = indexer @@ -133,8 +139,16 @@ fn get_outspend_details(txinindex: TxInIndex, query: &Query) -> Result Result { + get_mining_pools(time_period, self) + } + + pub fn get_all_pools(&self) -> Vec { + get_all_pools() + } + + pub fn get_pool_detail(&self, slug: PoolSlug) -> Result { + get_pool_detail(slug, self) + } + + pub fn get_hashrate(&self, time_period: Option) -> Result { + get_hashrate(time_period, self) + } + + pub fn get_difficulty_adjustments( + &self, + time_period: Option, + ) -> Result> { + chain::get_difficulty_adjustments(time_period, self) + } + + pub fn get_block_fees(&self, time_period: TimePeriod) -> Result> { + chain::get_block_fees(time_period, self) + } + + pub fn get_block_rewards( + &self, + time_period: TimePeriod, + ) -> Result> { + chain::get_block_rewards(time_period, self) + } + + pub fn get_block_fee_rates( + &self, + time_period: TimePeriod, + ) -> Result> { + chain::get_block_fee_rates(time_period, self) + } + + pub fn get_block_sizes_weights( + &self, + time_period: TimePeriod, + ) -> Result { + chain::get_block_sizes_weights(time_period, self) + } + + pub fn get_reward_stats(&self, block_count: usize) -> Result { + chain::get_reward_stats(block_count, self) + } + pub fn match_metric(&self, metric: &Metric, limit: Limit) -> Vec<&'static str> { self.vecs().matches(metric, limit) } diff --git a/crates/brk_server/src/api/addresses/mod.rs b/crates/brk_server/src/api/addresses/mod.rs index 39e535ecc..19f83eb7a 100644 --- a/crates/brk_server/src/api/addresses/mod.rs +++ b/crates/brk_server/src/api/addresses/mod.rs @@ -118,21 +118,22 @@ impl AddressRoutes for ApiRouter { ), ) .api_route( - "/api/address/{address}/txs/chain/{after_txid}", + "/api/address/{address}/txs/chain", get_with(async | headers: HeaderMap, - Path((address, after_txid)): Path<(Address, Option)>, + Path(address): Path
, + Query(params): Query, State(state): State | { let etag = format!("{VERSION}-{}", state.get_height().await); if headers.has_etag(&etag) { return Response::new_not_modified(); } - state.get_address_txids(address, after_txid, 25).await.to_json_response(&etag) + state.get_address_txids(address, params.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.") + .description("Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid= for pagination.") .ok_response::>() .not_modified() .bad_request() diff --git a/crates/brk_server/src/api/mining/mod.rs b/crates/brk_server/src/api/mining/mod.rs index 50c44127f..0e23dcb5a 100644 --- a/crates/brk_server/src/api/mining/mod.rs +++ b/crates/brk_server/src/api/mining/mod.rs @@ -1,11 +1,15 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ - extract::State, + extract::{Path, State}, http::HeaderMap, response::{Redirect, Response}, routing::get, }; -use brk_types::DifficultyAdjustment; +use brk_types::{ + BlockCountPath, BlockFeeRatesEntry, BlockFeesEntry, BlockRewardsEntry, BlockSizesWeights, + DifficultyAdjustment, DifficultyAdjustmentEntry, HashrateSummary, PoolDetail, PoolInfo, + PoolSlugPath, PoolsSummary, RewardStats, TimePeriodPath, +}; use crate::{ VERSION, @@ -47,5 +51,279 @@ impl MiningRoutes for ApiRouter { }, ), ) + .api_route( + "/api/v1/mining/pools", + get_with( + async |headers: HeaderMap, State(state): State| { + let etag = format!("{VERSION}-pools"); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state.get_all_pools().await.to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("List all mining pools") + .description("Get list of all known mining pools with their identifiers.") + .ok_response::>() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/pools/:time_period", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-{}-{:?}", state.get_height().await, path.time_period); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_mining_pools(path.time_period) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Mining pool statistics") + .description("Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") + .ok_response::() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/pool/:slug", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-{}-{:?}", state.get_height().await, path.slug); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_pool_detail(path.slug) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Mining pool details") + .description("Get detailed information about a specific mining pool including block counts and shares for different time periods.") + .ok_response::() + .not_modified() + .not_found() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/hashrate", + get_with( + async |headers: HeaderMap, State(state): State| { + let etag = format!("{VERSION}-hashrate-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_hashrate(None) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Network hashrate (all time)") + .description("Get network hashrate and difficulty data for all time.") + .ok_response::() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/hashrate/:time_period", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-hashrate-{}-{:?}", state.get_height().await, path.time_period); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_hashrate(Some(path.time_period)) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Network hashrate") + .description("Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") + .ok_response::() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/difficulty-adjustments", + get_with( + async |headers: HeaderMap, State(state): State| { + let etag = format!("{VERSION}-diff-adj-{}", state.get_height().await); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_difficulty_adjustments(None) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Difficulty adjustments (all time)") + .description("Get historical difficulty adjustments. Returns array of [timestamp, height, difficulty, change_percent].") + .ok_response::>() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/difficulty-adjustments/:time_period", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-diff-adj-{}-{:?}", state.get_height().await, path.time_period); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_difficulty_adjustments(Some(path.time_period)) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Difficulty adjustments") + .description("Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y. Returns array of [timestamp, height, difficulty, change_percent].") + .ok_response::>() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/blocks/fees/:time_period", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-fees-{}-{:?}", state.get_height().await, path.time_period); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_block_fees(path.time_period) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Block fees") + .description("Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") + .ok_response::>() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/blocks/rewards/:time_period", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-rewards-{}-{:?}", state.get_height().await, path.time_period); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_block_rewards(path.time_period) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Block rewards") + .description("Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") + .ok_response::>() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/blocks/fee-rates/:time_period", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-feerates-{}-{:?}", state.get_height().await, path.time_period); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_block_fee_rates(path.time_period) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Block fee rates") + .description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") + .ok_response::>() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/blocks/sizes-weights/:time_period", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-sizes-{}-{:?}", state.get_height().await, path.time_period); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_block_sizes_weights(path.time_period) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Block sizes and weights") + .description("Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") + .ok_response::() + .not_modified() + .server_error() + }, + ), + ) + .api_route( + "/api/v1/mining/reward-stats/:block_count", + get_with( + async |headers: HeaderMap, Path(path): Path, State(state): State| { + let etag = format!("{VERSION}-reward-stats-{}-{}", state.get_height().await, path.block_count); + if headers.has_etag(&etag) { + return Response::new_not_modified(); + } + state + .get_reward_stats(path.block_count) + .await + .to_json_response(&etag) + }, + |op| { + op.mining_tag() + .summary("Mining reward statistics") + .description("Get mining reward statistics for the last N blocks including total rewards, fees, and transaction count.") + .ok_response::() + .not_modified() + .server_error() + }, + ), + ) } } diff --git a/crates/brk_types/src/blockcountpath.rs b/crates/brk_types/src/blockcountpath.rs new file mode 100644 index 000000000..c9a6102d2 --- /dev/null +++ b/crates/brk_types/src/blockcountpath.rs @@ -0,0 +1,9 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +#[derive(Deserialize, JsonSchema)] +pub struct BlockCountPath { + /// Number of blocks to include in the stats + #[schemars(example = 100)] + pub block_count: usize, +} diff --git a/crates/brk_types/src/blockfeesentry.rs b/crates/brk_types/src/blockfeesentry.rs new file mode 100644 index 000000000..9123bb1e9 --- /dev/null +++ b/crates/brk_types/src/blockfeesentry.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; +use serde::Serialize; + +/// A single block fees data point. +#[derive(Debug, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BlockFeesEntry { + pub avg_height: u32, + pub timestamp: u32, + pub avg_fees: u64, +} diff --git a/crates/brk_types/src/blockferatesentry.rs b/crates/brk_types/src/blockferatesentry.rs new file mode 100644 index 000000000..e72683178 --- /dev/null +++ b/crates/brk_types/src/blockferatesentry.rs @@ -0,0 +1,14 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use super::FeeRatePercentiles; + +/// A single block fee rates data point with percentiles. +#[derive(Debug, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BlockFeeRatesEntry { + pub avg_height: u32, + pub timestamp: u32, + #[serde(flatten)] + pub percentiles: FeeRatePercentiles, +} diff --git a/crates/brk_types/src/blockrewardsentry.rs b/crates/brk_types/src/blockrewardsentry.rs new file mode 100644 index 000000000..c7dd0e03e --- /dev/null +++ b/crates/brk_types/src/blockrewardsentry.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; +use serde::Serialize; + +/// A single block rewards data point. +#[derive(Debug, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BlockRewardsEntry { + pub avg_height: u32, + pub timestamp: u32, + pub avg_rewards: u64, +} diff --git a/crates/brk_types/src/blocksizeentry.rs b/crates/brk_types/src/blocksizeentry.rs new file mode 100644 index 000000000..8e3a2f3d3 --- /dev/null +++ b/crates/brk_types/src/blocksizeentry.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; +use serde::Serialize; + +/// A single block size data point. +#[derive(Debug, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BlockSizeEntry { + pub avg_height: u32, + pub timestamp: u32, + pub avg_size: u64, +} diff --git a/crates/brk_types/src/blocksizesweights.rs b/crates/brk_types/src/blocksizesweights.rs new file mode 100644 index 000000000..9b01410ca --- /dev/null +++ b/crates/brk_types/src/blocksizesweights.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use super::{BlockSizeEntry, BlockWeightEntry}; + +/// Combined block sizes and weights response. +#[derive(Debug, Serialize, JsonSchema)] +pub struct BlockSizesWeights { + pub sizes: Vec, + pub weights: Vec, +} diff --git a/crates/brk_types/src/blockweightentry.rs b/crates/brk_types/src/blockweightentry.rs new file mode 100644 index 000000000..0f12bbacb --- /dev/null +++ b/crates/brk_types/src/blockweightentry.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; +use serde::Serialize; + +/// A single block weight data point. +#[derive(Debug, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BlockWeightEntry { + pub avg_height: u32, + pub timestamp: u32, + pub avg_weight: u64, +} diff --git a/crates/brk_types/src/difficultyadjustmententry.rs b/crates/brk_types/src/difficultyadjustmententry.rs new file mode 100644 index 000000000..b21fa3ff2 --- /dev/null +++ b/crates/brk_types/src/difficultyadjustmententry.rs @@ -0,0 +1,29 @@ +use schemars::JsonSchema; +use serde::ser::SerializeTuple; +use serde::{Serialize, Serializer}; + +use crate::{Height, Timestamp}; + +/// A single difficulty adjustment entry. +/// Serializes as array: [timestamp, height, difficulty, change_percent] +#[derive(Debug, JsonSchema)] +pub struct DifficultyAdjustmentEntry { + pub timestamp: Timestamp, + pub height: Height, + pub difficulty: f64, + pub change_percent: f64, +} + +impl Serialize for DifficultyAdjustmentEntry { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(4)?; + tup.serialize_element(&self.timestamp)?; + tup.serialize_element(&self.height)?; + tup.serialize_element(&self.difficulty)?; + tup.serialize_element(&self.change_percent)?; + tup.end() + } +} diff --git a/crates/brk_types/src/difficultyentry.rs b/crates/brk_types/src/difficultyentry.rs new file mode 100644 index 000000000..fae0b3e4d --- /dev/null +++ b/crates/brk_types/src/difficultyentry.rs @@ -0,0 +1,15 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use super::{Height, Timestamp}; + +/// A single difficulty data point. +#[derive(Debug, Serialize, JsonSchema)] +pub struct DifficultyEntry { + /// Unix timestamp of the difficulty adjustment. + pub timestamp: Timestamp, + /// Difficulty value. + pub difficulty: f64, + /// Block height of the adjustment. + pub height: Height, +} diff --git a/crates/brk_types/src/feeratepercentiles.rs b/crates/brk_types/src/feeratepercentiles.rs new file mode 100644 index 000000000..19ae3ec31 --- /dev/null +++ b/crates/brk_types/src/feeratepercentiles.rs @@ -0,0 +1,71 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use super::FeeRate; + +/// Fee rate percentiles (min, 10%, 25%, 50%, 75%, 90%, max). +#[derive(Debug, Default, Clone, Copy, Serialize, JsonSchema)] +pub struct FeeRatePercentiles { + #[serde(rename = "avgFee_0")] + pub min: FeeRate, + #[serde(rename = "avgFee_10")] + pub pct10: FeeRate, + #[serde(rename = "avgFee_25")] + pub pct25: FeeRate, + #[serde(rename = "avgFee_50")] + pub median: FeeRate, + #[serde(rename = "avgFee_75")] + pub pct75: FeeRate, + #[serde(rename = "avgFee_90")] + pub pct90: FeeRate, + #[serde(rename = "avgFee_100")] + pub max: FeeRate, +} + +impl FeeRatePercentiles { + pub fn new( + min: FeeRate, + pct10: FeeRate, + pct25: FeeRate, + median: FeeRate, + pct75: FeeRate, + pct90: FeeRate, + max: FeeRate, + ) -> Self { + Self { + min, + pct10, + pct25, + median, + pct75, + pct90, + max, + } + } + + /// Convert to array format [min, 10%, 25%, 50%, 75%, 90%, max]. + pub fn to_array(&self) -> [FeeRate; 7] { + [ + self.min, + self.pct10, + self.pct25, + self.median, + self.pct75, + self.pct90, + self.max, + ] + } + + /// Create from array format [min, 10%, 25%, 50%, 75%, 90%, max]. + pub fn from_array(arr: [FeeRate; 7]) -> Self { + Self { + min: arr[0], + pct10: arr[1], + pct25: arr[2], + median: arr[3], + pct75: arr[4], + pct90: arr[5], + max: arr[6], + } + } +} diff --git a/crates/brk_types/src/hashrateentry.rs b/crates/brk_types/src/hashrateentry.rs new file mode 100644 index 000000000..13b2a5eba --- /dev/null +++ b/crates/brk_types/src/hashrateentry.rs @@ -0,0 +1,14 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use super::Timestamp; + +/// A single hashrate data point. +#[derive(Debug, Serialize, JsonSchema)] +pub struct HashrateEntry { + /// Unix timestamp. + pub timestamp: Timestamp, + /// Average hashrate (H/s). + #[serde(rename = "avgHashrate")] + pub avg_hashrate: u128, +} diff --git a/crates/brk_types/src/hashratesummary.rs b/crates/brk_types/src/hashratesummary.rs new file mode 100644 index 000000000..742dbc5e0 --- /dev/null +++ b/crates/brk_types/src/hashratesummary.rs @@ -0,0 +1,19 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use super::{DifficultyEntry, HashrateEntry}; + +/// Summary of network hashrate and difficulty data. +#[derive(Debug, Serialize, JsonSchema)] +pub struct HashrateSummary { + /// Historical hashrate data points. + pub hashrates: Vec, + /// Historical difficulty adjustments. + pub difficulty: Vec, + /// Current network hashrate (H/s). + #[serde(rename = "currentHashrate")] + pub current_hashrate: u128, + /// Current network difficulty. + #[serde(rename = "currentDifficulty")] + pub current_difficulty: f64, +} diff --git a/crates/brk_types/src/lib.rs b/crates/brk_types/src/lib.rs index b7b4b00c5..3fb0927a2 100644 --- a/crates/brk_types/src/lib.rs +++ b/crates/brk_types/src/lib.rs @@ -9,21 +9,28 @@ mod addresshash; mod addressindexoutpoint; mod addressindextxindex; mod addressmempoolstats; -mod addresstxidsparam; mod addressstats; +mod addresstxidsparam; mod addressvalidation; mod anyaddressindex; mod bitcoin; mod blkmetadata; mod blkposition; mod block; +mod blockcountpath; mod blockhash; mod blockhashpath; +mod blockhashprefix; mod blockhashstartindexpath; mod blockhashtxindexpath; -mod blockhashprefix; +mod blockfeesentry; +mod blockferatesentry; mod blockinfo; +mod blockrewardsentry; +mod blocksizeentry; +mod blocksizesweights; mod blockstatus; +mod blockweightentry; mod blocktimestamp; mod bytes; mod cents; @@ -31,14 +38,19 @@ mod date; mod dateindex; mod decadeindex; mod difficultyadjustment; +mod difficultyadjustmententry; +mod difficultyentry; mod difficultyepoch; mod dollars; mod emptyaddressdata; mod emptyaddressindex; mod emptyoutputindex; mod feerate; +mod feeratepercentiles; mod format; mod halvingepoch; +mod hashrateentry; +mod hashratesummary; mod health; mod height; mod heightpath; @@ -47,10 +59,10 @@ mod indexinfo; mod limit; mod loadedaddressdata; mod loadedaddressindex; -mod metric; -mod mempoolentryinfo; mod mempoolblock; +mod mempoolentryinfo; mod mempoolinfo; +mod metric; mod metriccount; mod metrics; mod monthindex; @@ -76,25 +88,31 @@ mod p2wpkhbytes; mod p2wshaddressindex; mod p2wshbytes; mod pool; -mod poolid; -mod poolsresponse; -mod poolstats; +mod pooldetail; +mod poolinfo; mod pools; +mod poolslug; +mod poolslugpath; +mod poolssummary; +mod poolstats; mod quarterindex; mod rawlocktime; mod recommendedfees; +mod rewardstats; mod sats; mod semesterindex; +mod startheightpath; mod stored_bool; mod stored_f32; mod stored_f64; mod stored_i16; -mod startheightpath; mod stored_string; mod stored_u16; mod stored_u32; mod stored_u64; mod stored_u8; +mod timeperiod; +mod timeperiodpath; mod timestamp; mod timestamppath; mod treenode; @@ -138,13 +156,20 @@ pub use bitcoin::*; pub use blkmetadata::*; pub use blkposition::*; pub use block::*; +pub use blockcountpath::*; pub use blockhash::*; pub use blockhashpath::*; +pub use blockhashprefix::*; pub use blockhashstartindexpath::*; pub use blockhashtxindexpath::*; -pub use blockhashprefix::*; +pub use blockfeesentry::*; +pub use blockferatesentry::*; pub use blockinfo::*; +pub use blockrewardsentry::*; +pub use blocksizeentry::*; +pub use blocksizesweights::*; pub use blockstatus::*; +pub use blockweightentry::*; pub use blocktimestamp::*; pub use bytes::*; pub use cents::*; @@ -152,14 +177,19 @@ pub use date::*; pub use dateindex::*; pub use decadeindex::*; pub use difficultyadjustment::*; +pub use difficultyadjustmententry::*; +pub use difficultyentry::*; pub use difficultyepoch::*; pub use dollars::*; pub use emptyaddressdata::*; pub use emptyaddressindex::*; pub use emptyoutputindex::*; pub use feerate::*; +pub use feeratepercentiles::*; pub use format::*; pub use halvingepoch::*; +pub use hashrateentry::*; +pub use hashratesummary::*; pub use health::*; pub use height::*; pub use heightpath::*; @@ -168,8 +198,8 @@ pub use indexinfo::*; pub use limit::*; pub use loadedaddressdata::*; pub use loadedaddressindex::*; -pub use mempoolentryinfo::*; pub use mempoolblock::*; +pub use mempoolentryinfo::*; pub use mempoolinfo::*; pub use metric::*; pub use metriccount::*; @@ -197,25 +227,31 @@ pub use p2wpkhbytes::*; pub use p2wshaddressindex::*; pub use p2wshbytes::*; pub use pool::*; -pub use poolid::*; +pub use pooldetail::*; +pub use poolinfo::*; pub use pools::*; -pub use poolsresponse::*; +pub use poolslug::*; +pub use poolslugpath::*; +pub use poolssummary::*; pub use poolstats::*; pub use quarterindex::*; pub use rawlocktime::*; pub use recommendedfees::*; +pub use rewardstats::*; pub use sats::*; pub use semesterindex::*; +pub use startheightpath::*; pub use stored_bool::*; pub use stored_f32::*; pub use stored_f64::*; pub use stored_i16::*; -pub use startheightpath::*; pub use stored_string::*; pub use stored_u8::*; pub use stored_u16::*; pub use stored_u32::*; pub use stored_u64::*; +pub use timeperiod::*; +pub use timeperiodpath::*; pub use timestamp::*; pub use timestamppath::*; pub use treenode::*; diff --git a/crates/brk_types/src/pool.rs b/crates/brk_types/src/pool.rs index 132f55104..a1ebf9f5d 100644 --- a/crates/brk_types/src/pool.rs +++ b/crates/brk_types/src/pool.rs @@ -1,13 +1,13 @@ use schemars::JsonSchema; use serde::Serialize; -use super::PoolId; +use super::PoolSlug; /// Mining pool information #[derive(Debug, Serialize, JsonSchema)] pub struct Pool { /// Unique pool identifier - pub id: PoolId, + pub slug: PoolSlug, /// Pool name pub name: &'static str, @@ -30,8 +30,14 @@ pub struct Pool { } impl Pool { - pub fn serialized_id(&self) -> String { - self.id.to_string() + /// Get slug of pool + pub fn slug(&self) -> PoolSlug { + self.slug + } + + /// Get the pool's unique numeric ID + pub fn unique_id(&self) -> u8 { + self.slug.into() } } @@ -39,7 +45,7 @@ impl From<(usize, JSONPool)> for Pool { #[inline] fn from((index, pool): (usize, JSONPool)) -> Self { Self { - id: (index as u8).into(), + slug: (index as u8).into(), name: pool.name, addresses: pool.addresses, tags_lowercase: pool diff --git a/crates/brk_types/src/pooldetail.rs b/crates/brk_types/src/pooldetail.rs new file mode 100644 index 000000000..8e4c937ed --- /dev/null +++ b/crates/brk_types/src/pooldetail.rs @@ -0,0 +1,92 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::{Pool, PoolSlug}; + +/// Detailed pool information with statistics across time periods +#[derive(Debug, Serialize, JsonSchema)] +pub struct PoolDetail { + /// Pool information + pub pool: PoolDetailInfo, + + /// Block counts for different time periods + #[serde(rename = "blockCount")] + pub block_count: PoolBlockCounts, + + /// Pool's share of total blocks for different time periods + #[serde(rename = "blockShare")] + pub block_share: PoolBlockShares, + + /// Estimated hashrate based on blocks mined + #[serde(rename = "estimatedHashrate")] + pub estimated_hashrate: u128, + + /// Self-reported hashrate (if available) + #[serde(rename = "reportedHashrate")] + pub reported_hashrate: Option, +} + +/// Pool information for detail view +#[derive(Debug, Serialize, JsonSchema)] +pub struct PoolDetailInfo { + /// Unique pool identifier + pub id: u8, + + /// Pool name + pub name: &'static str, + + /// Pool website URL + pub link: &'static str, + + /// Known payout addresses + pub addresses: Vec<&'static str>, + + /// Coinbase tag patterns (regexes) + pub regexes: Vec<&'static str>, + + /// URL-friendly pool identifier + pub slug: PoolSlug, +} + +impl From<&'static Pool> for PoolDetailInfo { + fn from(pool: &'static Pool) -> Self { + Self { + id: pool.unique_id(), + name: pool.name, + link: pool.link, + addresses: pool.addresses.to_vec(), + regexes: pool.tags.to_vec(), + slug: pool.slug(), + } + } +} + +/// Block counts for different time periods +#[derive(Debug, Serialize, JsonSchema)] +pub struct PoolBlockCounts { + /// Total blocks mined (all time) + pub all: u32, + + /// Blocks mined in last 24 hours + #[serde(rename = "24h")] + pub day: u32, + + /// Blocks mined in last week + #[serde(rename = "1w")] + pub week: u32, +} + +/// Pool's share of total blocks for different time periods +#[derive(Debug, Serialize, JsonSchema)] +pub struct PoolBlockShares { + /// Share of all blocks (0.0 - 1.0) + pub all: f64, + + /// Share of blocks in last 24 hours + #[serde(rename = "24h")] + pub day: f64, + + /// Share of blocks in last week + #[serde(rename = "1w")] + pub week: f64, +} diff --git a/crates/brk_types/src/poolinfo.rs b/crates/brk_types/src/poolinfo.rs new file mode 100644 index 000000000..bb15b5563 --- /dev/null +++ b/crates/brk_types/src/poolinfo.rs @@ -0,0 +1,27 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::{Pool, PoolSlug}; + +/// Basic pool information for listing all pools +#[derive(Debug, Serialize, JsonSchema)] +pub struct PoolInfo { + /// Pool name + pub name: &'static str, + + /// URL-friendly pool identifier + pub slug: PoolSlug, + + /// Unique numeric pool identifier + pub unique_id: u8, +} + +impl From<&'static Pool> for PoolInfo { + fn from(pool: &'static Pool) -> Self { + Self { + name: pool.name, + slug: pool.slug(), + unique_id: pool.unique_id(), + } + } +} diff --git a/crates/brk_types/src/pools.rs b/crates/brk_types/src/pools.rs index de261cba4..93ccbc28e 100644 --- a/crates/brk_types/src/pools.rs +++ b/crates/brk_types/src/pools.rs @@ -1,6 +1,6 @@ use std::{slice::Iter, sync::OnceLock}; -use crate::{JSONPool, PoolId}; +use crate::{JSONPool, PoolSlug}; use super::Pool; @@ -27,8 +27,8 @@ impl Pools { &self.0[0] } - pub fn get(&self, id: PoolId) -> &Pool { - let i: u8 = id.into(); + pub fn get(&self, slug: PoolSlug) -> &Pool { + let i: u8 = slug.into(); &self.0[i as usize] } diff --git a/crates/brk_types/src/poolid.rs b/crates/brk_types/src/poolslug.rs similarity index 98% rename from crates/brk_types/src/poolid.rs rename to crates/brk_types/src/poolslug.rs index 71a69374f..91018ea99 100644 --- a/crates/brk_types/src/poolid.rs +++ b/crates/brk_types/src/poolslug.rs @@ -25,7 +25,7 @@ use vecdb::{Bytes, Formattable}; #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] #[repr(u8)] -pub enum PoolId { +pub enum PoolSlug { #[default] Unknown, BlockFills, @@ -285,14 +285,14 @@ pub enum PoolId { Dummy255, } -impl Formattable for PoolId { +impl Formattable for PoolSlug { #[inline(always)] fn may_need_escaping() -> bool { false } } -impl Bytes for PoolId { +impl Bytes for PoolSlug { type Array = [u8; size_of::()]; #[inline] diff --git a/crates/brk_types/src/poolslugpath.rs b/crates/brk_types/src/poolslugpath.rs new file mode 100644 index 000000000..0d08d2e35 --- /dev/null +++ b/crates/brk_types/src/poolslugpath.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use super::PoolSlug; + +/// Path parameter for pool detail endpoint +#[derive(Deserialize, JsonSchema)] +pub struct PoolSlugPath { + /// Pool slug (e.g., "foundryusa", "f2pool", "antpool") + pub slug: PoolSlug, +} diff --git a/crates/brk_types/src/poolsresponse.rs b/crates/brk_types/src/poolssummary.rs similarity index 67% rename from crates/brk_types/src/poolsresponse.rs rename to crates/brk_types/src/poolssummary.rs index fe349b5f9..32b44061b 100644 --- a/crates/brk_types/src/poolsresponse.rs +++ b/crates/brk_types/src/poolssummary.rs @@ -5,11 +5,15 @@ use crate::PoolStats; /// Mining pools response for a time period #[derive(Debug, Serialize, JsonSchema)] -pub struct PoolsResponse { +pub struct PoolsSummary { /// List of pools sorted by block count descending pub pools: Vec, /// Total blocks in the time period #[serde(rename = "blockCount")] pub block_count: u32, + + /// Estimated network hashrate (hashes per second) + #[serde(rename = "lastEstimatedHashrate")] + pub last_estimated_hashrate: u128, } diff --git a/crates/brk_types/src/poolstats.rs b/crates/brk_types/src/poolstats.rs index 76dac7c1f..1e52d0a3d 100644 --- a/crates/brk_types/src/poolstats.rs +++ b/crates/brk_types/src/poolstats.rs @@ -1,19 +1,51 @@ use schemars::JsonSchema; use serde::Serialize; -use crate::Pool; +use crate::{Pool, PoolSlug}; /// Mining pool with block statistics for a time period #[derive(Debug, Serialize, JsonSchema)] pub struct PoolStats { - /// Pool information - #[serde(flatten)] - pub pool: &'static Pool, + /// Unique pool identifier + #[serde(rename = "poolId")] + pub pool_id: u8, + + /// Pool name + pub name: &'static str, + + /// Pool website URL + pub link: &'static str, /// Number of blocks mined in the time period #[serde(rename = "blockCount")] pub block_count: u32, + /// Pool ranking by block count (1 = most blocks) + pub rank: u32, + + /// Number of empty blocks mined + #[serde(rename = "emptyBlocks")] + pub empty_blocks: u32, + + /// URL-friendly pool identifier + pub slug: PoolSlug, + /// Pool's share of total blocks (0.0 - 1.0) pub share: f64, } + +impl PoolStats { + /// Create a new PoolStats from a Pool reference + pub fn new(pool: &'static Pool, block_count: u32, rank: u32, share: f64) -> Self { + Self { + pool_id: pool.unique_id(), + name: pool.name, + link: pool.link, + block_count, + rank, + empty_blocks: 0, // TODO: track empty blocks if needed + slug: pool.slug(), + share, + } + } +} diff --git a/crates/brk_types/src/rewardstats.rs b/crates/brk_types/src/rewardstats.rs new file mode 100644 index 000000000..9a614f6ce --- /dev/null +++ b/crates/brk_types/src/rewardstats.rs @@ -0,0 +1,31 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use super::{Height, Sats}; + +#[derive(Debug, Clone, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct RewardStats { + pub start_block: Height, + pub end_block: Height, + #[serde(serialize_with = "sats_as_string")] + pub total_reward: Sats, + #[serde(serialize_with = "sats_as_string")] + pub total_fee: Sats, + #[serde(serialize_with = "u64_as_string")] + pub total_tx: u64, +} + +fn sats_as_string(value: &Sats, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&value.to_string()) +} + +fn u64_as_string(value: &u64, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&value.to_string()) +} diff --git a/crates/brk_types/src/timeperiod.rs b/crates/brk_types/src/timeperiod.rs new file mode 100644 index 000000000..865d41ff8 --- /dev/null +++ b/crates/brk_types/src/timeperiod.rs @@ -0,0 +1,70 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Time period for mining statistics. +/// +/// Used to specify the lookback window for pool statistics, hashrate calculations, +/// and other time-based mining metrics. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum TimePeriod { + /// Last 24 hours (~144 blocks) + #[serde(rename = "24h")] + Day, + /// Last 3 days (~432 blocks) + #[serde(rename = "3d")] + ThreeDays, + /// Last week (~1008 blocks) + #[serde(rename = "1w")] + Week, + /// Last month (~4320 blocks) + #[serde(rename = "1m")] + Month, + /// Last 3 months (~12960 blocks) + #[serde(rename = "3m")] + ThreeMonths, + /// Last 6 months (~25920 blocks) + #[serde(rename = "6m")] + SixMonths, + /// Last year (~52560 blocks) + #[serde(rename = "1y")] + Year, + /// Last 2 years (~105120 blocks) + #[serde(rename = "2y")] + TwoYears, + /// Last 3 years (~157680 blocks) + #[serde(rename = "3y")] + ThreeYears, +} + +impl TimePeriod { + /// Approximate number of blocks for this time period (10 min per block average) + pub fn block_count(&self) -> usize { + match self { + TimePeriod::Day => 144, + TimePeriod::ThreeDays => 432, + TimePeriod::Week => 1008, + TimePeriod::Month => 4320, + TimePeriod::ThreeMonths => 12960, + TimePeriod::SixMonths => 25920, + TimePeriod::Year => 52560, + TimePeriod::TwoYears => 105120, + TimePeriod::ThreeYears => 157680, + } + } + + /// Parse from URL path segment + pub fn from_path(s: &str) -> Option { + match s { + "24h" => Some(TimePeriod::Day), + "3d" => Some(TimePeriod::ThreeDays), + "1w" => Some(TimePeriod::Week), + "1m" => Some(TimePeriod::Month), + "3m" => Some(TimePeriod::ThreeMonths), + "6m" => Some(TimePeriod::SixMonths), + "1y" => Some(TimePeriod::Year), + "2y" => Some(TimePeriod::TwoYears), + "3y" => Some(TimePeriod::ThreeYears), + _ => None, + } + } +} diff --git a/crates/brk_types/src/timeperiodpath.rs b/crates/brk_types/src/timeperiodpath.rs new file mode 100644 index 000000000..5f99d4247 --- /dev/null +++ b/crates/brk_types/src/timeperiodpath.rs @@ -0,0 +1,12 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use super::TimePeriod; + +/// Path parameter for mining pool statistics time period +#[derive(Deserialize, JsonSchema)] +pub struct TimePeriodPath { + /// Time period for statistics. + /// Valid values: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + pub time_period: TimePeriod, +} diff --git a/crates/brk_types/src/timestamp.rs b/crates/brk_types/src/timestamp.rs index c2b073d4a..321797957 100644 --- a/crates/brk_types/src/timestamp.rs +++ b/crates/brk_types/src/timestamp.rs @@ -11,6 +11,7 @@ use super::Date; /// Timestamp #[derive( Debug, + Default, Deref, Clone, Copy, diff --git a/docs/.gitignore b/docs/.gitignore index 2b89b9ddb..fa65f4f2d 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,2 +1,3 @@ OPENSATS*.md DUMP.md +*changes.md