global: snap

This commit is contained in:
nym21
2026-04-04 18:19:11 +02:00
parent 5340cc288e
commit 62f51761ee
23 changed files with 492 additions and 126 deletions
+5 -5
View File
@@ -8198,7 +8198,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.3.0-alpha.4";
pub const VERSION: &'static str = "v0.3.0-alpha.5";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {
@@ -8907,15 +8907,15 @@ impl BrkClient {
self.base.get_json(&path)
}
/// Block fee rates (WIP)
/// Block fee rates
///
/// **Work in progress.** 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
/// 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
///
/// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*
///
/// Endpoint: `GET /api/v1/mining/blocks/fee-rates/{time_period}`
pub fn get_block_fee_rates(&self, time_period: TimePeriod) -> Result<String> {
self.base.get_text(&format!("/api/v1/mining/blocks/fee-rates/{time_period}"))
pub fn get_block_fee_rates(&self, time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
self.base.get_json(&format!("/api/v1/mining/blocks/fee-rates/{time_period}"))
}
/// Block fees
+6 -8
View File
@@ -206,13 +206,12 @@ impl Query {
.total
.sum
.collect_range_at(begin, end);
let utxo_begin = begin.saturating_sub(1);
let utxo_set_sizes = computer
.outputs
.count
.unspent
.height
.collect_range_at(utxo_begin, end);
.collect_range_at(begin, end);
let input_volumes = computer
.transactions
.volume
@@ -279,9 +278,6 @@ impl Query {
let subsidy = subsidy_sats[i];
let total_inputs = (*input_counts[i]).saturating_sub(1);
let total_outputs = *output_counts[i];
let utxo_idx = begin + i - utxo_begin;
let utxo_set_size = *utxo_set_sizes[utxo_idx];
let prev_utxo_set_size = if utxo_idx > 0 { *utxo_set_sizes[utxo_idx - 1] } else { 0 };
let vsize = weight.to_vbytes_ceil();
let total_fees_u64 = u64::from(total_fees);
let non_coinbase = tx_count.saturating_sub(1) as u64;
@@ -383,8 +379,8 @@ impl Query {
segwit_total_size: *segwit_sizes[i],
segwit_total_weight: segwit_weights[i],
header: raw_header.to_lower_hex_string(),
utxo_set_change: utxo_set_size as i64 - prev_utxo_set_size as i64,
utxo_set_size,
utxo_set_change: total_outputs as i64 - total_inputs as i64,
utxo_set_size: *utxo_set_sizes[i],
total_input_amt,
virtual_size: vsize as f64,
price: prices[i],
@@ -552,7 +548,9 @@ impl Query {
.ok()
.map(|a| a.to_string())
})
.collect();
.collect::<Vec<_>>();
let mut coinbase_addresses = coinbase_addresses;
coinbase_addresses.dedup();
let coinbase_address = coinbase_addresses.first().cloned();
let coinbase_signature = tx
+6 -6
View File
@@ -56,9 +56,9 @@ impl Query {
let entries = mempool.get_entries();
let prefix = TxidPrefix::from(txid);
let entry = entries
.get(&prefix)
.ok_or(Error::NotFound("Transaction not in mempool".into()))?;
let Some(entry) = entries.get(&prefix) else {
return Ok(CpfpInfo::default());
};
// Ancestors: walk up the depends chain
let mut ancestors = Vec::new();
@@ -101,9 +101,9 @@ impl Query {
ancestors,
best_descendant,
descendants,
effective_fee_per_vsize,
fee: entry.fee,
adjusted_vsize: entry.vsize,
effective_fee_per_vsize: Some(effective_fee_per_vsize),
fee: Some(entry.fee),
adjusted_vsize: Some(entry.vsize),
})
}
@@ -1,59 +1,57 @@
// TODO: INCOMPLETE - indexes_to_fee_rate.day1 doesn't have percentile fields
// because from_tx_index.rs calls remove_percentiles() before creating day1.
// Need to either:
// 1. Use .height instead and convert height to day1 for iteration
// 2. Fix from_tx_index.rs to preserve percentiles for day1
// 3. Create a separate day1 computation path with percentiles
#![allow(dead_code)]
use brk_error::Result;
use brk_types::{
BlockFeeRatesEntry,
// FeeRatePercentiles,
TimePeriod,
};
// use vecdb::{IterableVec, VecIndex};
use brk_types::{BlockFeeRatesEntry, FeeRate, FeeRatePercentiles, TimePeriod};
use vecdb::ReadableVec;
use super::block_window::BlockWindow;
use crate::Query;
impl Query {
pub fn block_fee_rates(&self, _time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
// Disabled until percentile data is available at day1 level
Ok(Vec::new())
pub fn block_fee_rates(&self, time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
let bw = BlockWindow::new(self, time_period);
let computer = self.computer();
let frd = &computer.transactions.fees.effective_fee_rate.distribution.block;
// Original implementation:
// let computer = self.computer();
// let current_height = self.height();
// let start = current_height
// .to_usize()
// .saturating_sub(time_period.block_count());
//
// let iter = Day1Iter::new(computer, start, current_height.to_usize());
//
// let vecs = &computer.transactions.transaction.indexes_to_fee_rate.day1;
// 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,
// timestamp: ts,
// 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(),
// ),
// })
// }))
let min = frd.min.height.collect_range_at(bw.start, bw.end);
let pct10 = frd.pct10.height.collect_range_at(bw.start, bw.end);
let pct25 = frd.pct25.height.collect_range_at(bw.start, bw.end);
let median = frd.median.height.collect_range_at(bw.start, bw.end);
let pct75 = frd.pct75.height.collect_range_at(bw.start, bw.end);
let pct90 = frd.pct90.height.collect_range_at(bw.start, bw.end);
let max = frd.max.height.collect_range_at(bw.start, bw.end);
let timestamps = bw.timestamps(self);
let mut results = Vec::with_capacity(timestamps.len());
let mut pos = 0;
let total = min.len();
for ts in &timestamps {
let window_end = (pos + bw.window).min(total);
let count = window_end - pos;
if count > 0 {
let mid = (pos + window_end) / 2;
let avg = |vals: &[FeeRate]| -> FeeRate {
let sum: f64 = vals[pos..window_end].iter().map(|f| f64::from(*f)).sum();
FeeRate::new(sum / count as f64)
};
results.push(BlockFeeRatesEntry {
avg_height: brk_types::Height::from(bw.start + mid),
timestamp: *ts,
percentiles: FeeRatePercentiles::new(
avg(&min),
avg(&pct10),
avg(&pct25),
avg(&median),
avg(&pct75),
avg(&pct90),
avg(&max),
),
});
}
pos = window_end;
}
Ok(results)
}
}
@@ -12,6 +12,7 @@ fn block_window(period: TimePeriod) -> usize {
TimePeriod::SixMonths => 18,
TimePeriod::Year | TimePeriod::TwoYears => 48,
TimePeriod::ThreeYears => 72,
TimePeriod::All => 144,
}
}
@@ -49,6 +50,7 @@ impl BlockWindow {
TimePeriod::Year => cached._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();
+21 -1
View File
@@ -65,6 +65,7 @@ impl Query {
.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();
@@ -254,7 +255,25 @@ impl Query {
day: share_24h,
week: share_1w,
},
estimated_hashrate: 0, // TODO: Calculate from share and network hashrate
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,
})
}
@@ -338,6 +357,7 @@ impl Query {
.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()
+3 -2
View File
@@ -109,9 +109,10 @@ impl Query {
pub fn outspend(&self, txid: &Txid, vout: Vout) -> Result<TxOutspend> {
let all = self.outspends(txid)?;
all.into_iter()
Ok(all
.into_iter()
.nth(usize::from(vout))
.ok_or(Error::OutOfRange("Output index out of range".into()))
.unwrap_or(TxOutspend::UNSPENT))
}
pub fn outspends(&self, txid: &Txid) -> Result<Vec<TxOutspend>> {
@@ -2,16 +2,17 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::{HeaderMap, Uri},
response::{IntoResponse, Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{
BlockFeesEntry, BlockInfoV1, BlockRewardsEntry, BlockSizesWeights, DifficultyAdjustmentEntry,
HashrateSummary, PoolDetail, PoolHashrateEntry, PoolInfo, PoolsSummary, RewardStats,
BlockFeeRatesEntry, BlockFeesEntry, BlockInfoV1, BlockRewardsEntry, BlockSizesWeights,
DifficultyAdjustmentEntry, HashrateSummary, PoolDetail, PoolHashrateEntry, PoolInfo,
PoolsSummary, RewardStats,
};
use crate::{
AppState, CacheStrategy, Error,
AppState, CacheStrategy,
extended::TransformResponseExtended,
params::{BlockCountParam, PoolSlugAndHeightParam, PoolSlugParam, TimePeriodParam},
};
@@ -289,14 +290,15 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/blocks/fee-rates/{time_period}",
get_with(
async |Path(_path): Path<TimePeriodParam>| -> Response {
Error::not_implemented("Fee rate percentiles are not yet available").into_response()
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_fee_rates(path.time_period)).await
},
|op| {
op.id("get_block_fee_rates")
.mining_tag()
.summary("Block fee rates (WIP)")
.description("**Work in progress.** 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\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
.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\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
.json_response::<Vec<BlockFeeRatesEntry>>()
.server_error()
},
),
+4 -2
View File
@@ -91,11 +91,13 @@ pub struct BlockExtras {
/// Raw 80-byte block header as hex
pub header: String,
/// UTXO set change (outputs created minus inputs spent)
/// UTXO set change (total outputs - total inputs, includes unspendable like OP_RETURN).
/// Note: intentionally differs from utxo_set_size diff which excludes unspendable outputs.
/// Matches mempool.space/bitcoin-cli behavior.
#[serde(rename = "utxoSetChange")]
pub utxo_set_change: i64,
/// Total UTXO set size at this height
/// Total spendable UTXO set size at this height (excludes OP_RETURN and other unspendable outputs)
#[serde(rename = "utxoSetSize")]
pub utxo_set_size: u64,
@@ -1,12 +1,12 @@
use schemars::JsonSchema;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::{Height, Timestamp};
use super::FeeRatePercentiles;
/// A single block fee rates data point with percentiles.
#[derive(Debug, Serialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BlockFeeRatesEntry {
/// Average block height in this window
+10 -4
View File
@@ -4,23 +4,29 @@ use serde::{Deserialize, Serialize};
use crate::{FeeRate, Sats, Txid, VSize, Weight};
/// CPFP (Child Pays For Parent) information for a transaction
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CpfpInfo {
/// Ancestor transactions in the CPFP chain
pub ancestors: Vec<CpfpEntry>,
/// Best (highest fee rate) descendant, if any
#[serde(skip_serializing_if = "Option::is_none")]
pub best_descendant: Option<CpfpEntry>,
/// Descendant transactions in the CPFP chain
#[serde(skip_serializing_if = "Vec::is_empty")]
pub descendants: Vec<CpfpEntry>,
/// Effective fee rate considering CPFP relationships (sat/vB)
pub effective_fee_per_vsize: FeeRate,
#[serde(skip_serializing_if = "Option::is_none")]
pub effective_fee_per_vsize: Option<FeeRate>,
/// Transaction fee (sats)
pub fee: Sats,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee: Option<Sats>,
/// Adjusted virtual size (accounting for sigops)
pub adjusted_vsize: VSize,
#[serde(skip_serializing_if = "Option::is_none")]
pub adjusted_vsize: Option<VSize>,
}
/// A transaction in a CPFP relationship
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CpfpEntry {
+2 -2
View File
@@ -1,10 +1,10 @@
use schemars::JsonSchema;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use super::FeeRate;
/// Fee rate percentiles (min, 10%, 25%, 50%, 75%, 90%, max).
#[derive(Debug, Default, Clone, Copy, Serialize, JsonSchema)]
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct FeeRatePercentiles {
#[serde(rename = "avgFee_0")]
pub min: FeeRate,
+5
View File
@@ -28,6 +28,8 @@ pub enum TimePeriod {
TwoYears,
#[serde(rename = "3y")]
ThreeYears,
#[serde(rename = "all")]
All,
}
impl TimePeriod {
@@ -43,6 +45,7 @@ impl TimePeriod {
TimePeriod::Year => 52560,
TimePeriod::TwoYears => 105120,
TimePeriod::ThreeYears => 157680,
TimePeriod::All => usize::MAX,
}
}
@@ -58,6 +61,7 @@ impl TimePeriod {
"1y" => Some(TimePeriod::Year),
"2y" => Some(TimePeriod::TwoYears),
"3y" => Some(TimePeriod::ThreeYears),
"all" => Some(TimePeriod::All),
_ => None,
}
}
@@ -75,6 +79,7 @@ impl fmt::Display for TimePeriod {
TimePeriod::Year => write!(f, "1y"),
TimePeriod::TwoYears => write!(f, "2y"),
TimePeriod::ThreeYears => write!(f, "3y"),
TimePeriod::All => write!(f, "all"),
}
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ impl Serialize for TxIn {
String::new()
};
let has_inner_redeem = !inner_redeem.is_empty();
let has_inner_redeem = is_p2sh && !self.is_coinbase;
let has_inner_witness = !inner_witness.is_empty();
let field_count =
7 + has_witness as usize + has_inner_redeem as usize + has_inner_witness as usize;