use std::cmp::Reverse; use brk_error::{Error, Result}; use brk_types::{ BlockInfoV1, Day1, Height, Pool, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo, PoolHashrateEntry, PoolInfo, PoolSlug, PoolStats, PoolsSummary, StoredF64, StoredU64, TimePeriod, pools, }; use vecdb::{AnyVec, ReadableVec, VecIndex}; use crate::Query; /// 7-day lookback for share computation (matching mempool.space) const LOOKBACK_DAYS: usize = 7; /// Weekly sample interval (matching mempool.space's 604800s interval) const SAMPLE_WEEKLY: usize = 7; /// Pre-read shared data for hashrate computation. struct HashrateSharedData { start_day: usize, end_day: usize, daily_hashrate: Vec>, first_heights: Vec, } impl Query { pub fn mining_pools(&self, time_period: TimePeriod) -> Result { let computer = self.computer(); let current_height = self.height(); // No blocks indexed yet if computer.pools.pool.len() == 0 { return Ok(PoolsSummary { pools: vec![], block_count: 0, last_estimated_hashrate: 0, last_estimated_hashrate3d: 0, last_estimated_hashrate1w: 0, }); } // Use timestamp-based lookback for accurate time boundaries let lookback = &computer.blocks.lookback; let start = match time_period { TimePeriod::Day => lookback._24h.collect_one(current_height), TimePeriod::ThreeDays => lookback._3d.collect_one(current_height), TimePeriod::Week => lookback._1w.collect_one(current_height), TimePeriod::Month => lookback._1m.collect_one(current_height), TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height), TimePeriod::SixMonths => lookback._6m.collect_one(current_height), TimePeriod::Year => lookback._1y.collect_one(current_height), TimePeriod::TwoYears => lookback._2y.collect_one(current_height), TimePeriod::ThreeYears => lookback._3y.collect_one(current_height), TimePeriod::All => None, } .unwrap_or_default() .to_usize(); let pools = pools(); let mut pool_data: Vec<(&'static Pool, u64)> = Vec::new(); // For each pool, get cumulative count at end and start, subtract to get range count for (pool_id, cumulative) in computer .pools .major .iter() .map(|(id, v)| (id, &v.blocks_mined.cumulative.height)) .chain( computer .pools .minor .iter() .map(|(id, v)| (id, &v.blocks_mined.cumulative.height)), ) { let count_at_end: u64 = *cumulative.collect_one(current_height).unwrap_or_default(); let count_at_start: u64 = if start == 0 { 0 } else { *cumulative .collect_one(Height::from(start - 1)) .unwrap_or_default() }; let block_count = count_at_end.saturating_sub(count_at_start); if block_count > 0 { pool_data.push((pools.get(*pool_id), block_count)); } } // Sort by block count descending pool_data.sort_by_key(|p| Reverse(p.1)); let total_blocks: u64 = 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(); let hashrate_at = |height: Height| -> u128 { let day = computer .indexes .height .day1 .collect_one(height) .unwrap_or_default(); computer .mining .hashrate .rate .base .day1 .collect_one(day) .flatten() .map(|v| *v as u128) .unwrap_or(0) }; let lookback = &computer.blocks.lookback; let last_estimated_hashrate = hashrate_at(current_height); let last_estimated_hashrate3d = hashrate_at(lookback._3d.collect_one(current_height).unwrap_or_default()); let last_estimated_hashrate1w = hashrate_at(lookback._1w.collect_one(current_height).unwrap_or_default()); Ok(PoolsSummary { pools: pool_stats, block_count: total_blocks, last_estimated_hashrate, last_estimated_hashrate3d, last_estimated_hashrate1w, }) } pub fn all_pools(&self) -> Vec { pools().iter().map(PoolInfo::from).collect() } pub fn pool_detail(&self, slug: PoolSlug) -> Result { let computer = self.computer(); let current_height = self.height(); let end = current_height.to_usize(); let pools_list = pools(); let pool = pools_list.get(slug); // Get cumulative blocks for this pool (works for both major and minor) let cumulative = computer .pools .major .get(&slug) .map(|v| &v.blocks_mined.cumulative.height) .or_else(|| { computer .pools .minor .get(&slug) .map(|v| &v.blocks_mined.cumulative.height) }) .ok_or_else(|| Error::NotFound("Pool data not found".into()))?; // Get total blocks (all time) let total_all: u64 = *cumulative.collect_one(current_height).unwrap_or_default(); // Use timestamp-based lookback for accurate time boundaries let lookback = &computer.blocks.lookback; let start_24h = lookback ._24h .collect_one(current_height) .unwrap_or_default() .to_usize(); let count_before_24h: u64 = if start_24h == 0 { 0 } else { *cumulative .collect_one(Height::from(start_24h - 1)) .unwrap_or_default() }; let total_24h = total_all.saturating_sub(count_before_24h); let start_1w = lookback ._1w .collect_one(current_height) .unwrap_or_default() .to_usize(); let count_before_1w: u64 = if start_1w == 0 { 0 } else { *cumulative .collect_one(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 u64; let network_blocks_24h = (end - start_24h + 1) as u64; let network_blocks_1w = (end - start_1w + 1) as u64; 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: { let day = computer .indexes .height .day1 .collect_one(current_height) .unwrap_or_default(); let network_hr = computer .mining .hashrate .rate .base .day1 .collect_one(day) .flatten() .map(|v| *v as u128) .unwrap_or(0); (share_24h * network_hr as f64) as u128 }, reported_hashrate: None, total_reward: computer .pools .major .get(&slug) .and_then(|v| v.rewards.cumulative.sats.height.collect_one(current_height)), }) } pub fn pool_blocks( &self, slug: PoolSlug, start_height: Option, ) -> Result> { let computer = self.computer(); let max_height = self.height().to_usize(); let start = start_height.map(|h| h.to_usize()).unwrap_or(max_height); let end = start.min(computer.pools.pool.len().saturating_sub(1)); const POOL_BLOCKS_LIMIT: usize = 100; let heights: Vec = computer .pools .pool_heights .read() .get(&slug) .map(|pool_heights| { let pos = pool_heights.partition_point(|h| h.to_usize() <= end); let start = pos.saturating_sub(POOL_BLOCKS_LIMIT); pool_heights[start..pos] .iter() .rev() .map(|h| h.to_usize()) .collect() }) .unwrap_or_default(); // Group consecutive descending heights into ranges for batch reads let mut blocks = Vec::with_capacity(heights.len()); let mut i = 0; while i < heights.len() { let hi = heights[i]; while i + 1 < heights.len() && heights[i + 1] + 1 == heights[i] { i += 1; } if let Ok(mut v) = self.blocks_v1_range(heights[i], hi + 1) { blocks.append(&mut v); } i += 1; } Ok(blocks) } pub fn pool_hashrate(&self, slug: PoolSlug) -> Result> { let pool_name = pools().get(slug).name.to_string(); let shared = self.hashrate_shared_data(0)?; let pool_cum = self.pool_daily_cumulative(slug, shared.start_day, shared.end_day)?; Ok(Self::compute_hashrate_entries( &shared, &pool_cum, &pool_name, SAMPLE_WEEKLY, )) } pub fn pools_hashrate( &self, time_period: Option, ) -> Result> { let start_height = match time_period { Some(tp) => { let lookback = &self.computer().blocks.lookback; let current_height = self.height(); match tp { TimePeriod::Day => lookback._24h.collect_one(current_height), TimePeriod::ThreeDays => lookback._3d.collect_one(current_height), TimePeriod::Week => lookback._1w.collect_one(current_height), TimePeriod::Month => lookback._1m.collect_one(current_height), TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height), TimePeriod::SixMonths => lookback._6m.collect_one(current_height), TimePeriod::Year => lookback._1y.collect_one(current_height), TimePeriod::TwoYears => lookback._2y.collect_one(current_height), TimePeriod::ThreeYears => lookback._3y.collect_one(current_height), TimePeriod::All => None, } .unwrap_or_default() .to_usize() } None => 0, }; let shared = self.hashrate_shared_data(start_height)?; let pools_list = pools(); let mut entries = Vec::new(); for pool in pools_list.iter() { let Ok(pool_cum) = self.pool_daily_cumulative(pool.slug, shared.start_day, shared.end_day) else { continue; }; entries.extend(Self::compute_hashrate_entries( &shared, &pool_cum, pool.name, SAMPLE_WEEKLY, )); } Ok(entries) } /// Shared data needed for hashrate computation (read once, reuse across pools). fn hashrate_shared_data(&self, start_height: usize) -> Result { let computer = self.computer(); let current_height = self.height(); let start_day = computer .indexes .height .day1 .collect_one_at(start_height) .unwrap_or_default() .to_usize(); let end_day = computer .indexes .height .day1 .collect_one(current_height) .unwrap_or_default() .to_usize() + 1; let daily_hashrate = computer .mining .hashrate .rate .base .day1 .collect_range_at(start_day, end_day); let first_heights = computer .indexes .day1 .first_height .collect_range_at(start_day, end_day); Ok(HashrateSharedData { start_day, end_day, daily_hashrate, first_heights, }) } /// Read daily cumulative blocks mined for a pool. fn pool_daily_cumulative( &self, slug: PoolSlug, start_day: usize, end_day: usize, ) -> Result>> { let computer = self.computer(); computer .pools .major .get(&slug) .map(|v| { v.base .blocks_mined .cumulative .day1 .collect_range_at(start_day, end_day) }) .or_else(|| { computer.pools.minor.get(&slug).map(|v| { v.blocks_mined .cumulative .day1 .collect_range_at(start_day, end_day) }) }) .ok_or_else(|| Error::NotFound("Pool not found".into())) } /// Compute hashrate entries from daily cumulative blocks + shared data. /// Uses 7-day windowed share: pool_blocks_in_week / total_blocks_in_week. fn compute_hashrate_entries( shared: &HashrateSharedData, pool_cum: &[Option], pool_name: &str, sample_days: usize, ) -> Vec { let total = pool_cum.len(); if total <= LOOKBACK_DAYS { return vec![]; } let mut entries = Vec::new(); let mut i = LOOKBACK_DAYS; while i < total { if let (Some(cum_now), Some(cum_prev)) = (pool_cum[i], pool_cum[i - LOOKBACK_DAYS]) { let pool_blocks = (*cum_now).saturating_sub(*cum_prev); if pool_blocks > 0 { let h_now = shared.first_heights[i].to_usize(); let h_prev = shared.first_heights[i - LOOKBACK_DAYS].to_usize(); let total_blocks = h_now.saturating_sub(h_prev); if total_blocks > 0 && let Some(hr) = shared.daily_hashrate[i].as_ref() { let network_hr = **hr; let share = pool_blocks as f64 / total_blocks as f64; let day = Day1::from(shared.start_day + i); entries.push(PoolHashrateEntry { timestamp: day.to_timestamp(), avg_hashrate: (network_hr * share) as u128, share, pool_name: pool_name.to_string(), }); } } } i += sample_days; } entries } }