mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
global: snap
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ×tamps {
|
||||
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();
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+26
-10
@@ -135,14 +135,30 @@
|
||||
* @property {number} segwitTotalSize - Total size of segwit transactions in bytes
|
||||
* @property {Weight} segwitTotalWeight - Total weight of segwit transactions
|
||||
* @property {string} header - Raw 80-byte block header as hex
|
||||
* @property {number} utxoSetChange - UTXO set change (outputs created minus inputs spent)
|
||||
* @property {number} utxoSetSize - Total UTXO set size at this height
|
||||
* @property {number} utxoSetChange - 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.
|
||||
* @property {number} utxoSetSize - Total spendable UTXO set size at this height (excludes OP_RETURN and other unspendable outputs)
|
||||
* @property {Sats} totalInputAmt - Total input amount in satoshis
|
||||
* @property {number} virtualSize - Virtual size in vbytes
|
||||
* @property {?number=} firstSeen - Timestamp when the block was first seen (always null, not yet supported)
|
||||
* @property {string[]} orphans - Orphaned blocks (always empty)
|
||||
* @property {Dollars} price - USD price at block height
|
||||
*/
|
||||
/**
|
||||
* A single block fee rates data point with percentiles.
|
||||
*
|
||||
* @typedef {Object} BlockFeeRatesEntry
|
||||
* @property {Height} avgHeight - Average block height in this window
|
||||
* @property {Timestamp} timestamp - Unix timestamp at the window midpoint
|
||||
* @property {FeeRate} avgFee0
|
||||
* @property {FeeRate} avgFee10
|
||||
* @property {FeeRate} avgFee25
|
||||
* @property {FeeRate} avgFee50
|
||||
* @property {FeeRate} avgFee75
|
||||
* @property {FeeRate} avgFee90
|
||||
* @property {FeeRate} avgFee100
|
||||
*/
|
||||
/**
|
||||
* A single block fees data point.
|
||||
*
|
||||
@@ -359,9 +375,9 @@
|
||||
* @property {CpfpEntry[]} ancestors - Ancestor transactions in the CPFP chain
|
||||
* @property {(CpfpEntry|null)=} bestDescendant - Best (highest fee rate) descendant, if any
|
||||
* @property {CpfpEntry[]} descendants - Descendant transactions in the CPFP chain
|
||||
* @property {FeeRate} effectiveFeePerVsize - Effective fee rate considering CPFP relationships (sat/vB)
|
||||
* @property {Sats} fee - Transaction fee (sats)
|
||||
* @property {VSize} adjustedVsize - Adjusted virtual size (accounting for sigops)
|
||||
* @property {(FeeRate|null)=} effectiveFeePerVsize - Effective fee rate considering CPFP relationships (sat/vB)
|
||||
* @property {(Sats|null)=} fee - Transaction fee (sats)
|
||||
* @property {(VSize|null)=} adjustedVsize - Adjusted virtual size (accounting for sigops)
|
||||
*/
|
||||
/**
|
||||
* Data range with output format for API query parameters
|
||||
@@ -983,7 +999,7 @@
|
||||
* Used to specify the lookback window for pool statistics, hashrate calculations,
|
||||
* and other time-based mining series.
|
||||
*
|
||||
* @typedef {("24h"|"3d"|"1w"|"1m"|"3m"|"6m"|"1y"|"2y"|"3y")} TimePeriod
|
||||
* @typedef {("24h"|"3d"|"1w"|"1m"|"3m"|"6m"|"1y"|"2y"|"3y"|"all")} TimePeriod
|
||||
*/
|
||||
/**
|
||||
* @typedef {Object} TimePeriodParam
|
||||
@@ -6573,7 +6589,7 @@ function createTransferPattern(client, acc) {
|
||||
* @extends BrkClientBase
|
||||
*/
|
||||
class BrkClient extends BrkClientBase {
|
||||
VERSION = "v0.3.0-alpha.4";
|
||||
VERSION = "v0.3.0-alpha.5";
|
||||
|
||||
INDEXES = /** @type {const} */ ([
|
||||
"minute10",
|
||||
@@ -10363,16 +10379,16 @@ class BrkClient extends BrkClientBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`
|
||||
*
|
||||
* @param {TimePeriod} time_period
|
||||
* @returns {Promise<*>}
|
||||
* @returns {Promise<BlockFeeRatesEntry[]>}
|
||||
*/
|
||||
async getBlockFeeRates(time_period) {
|
||||
return this.getJson(`/api/v1/mining/blocks/fee-rates/${time_period}`);
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"url": "git+https://github.com/bitcoinresearchkit/brk.git"
|
||||
},
|
||||
"type": "module",
|
||||
"version": "0.3.0-alpha.4"
|
||||
"version": "0.3.0-alpha.5"
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ StoredU64 = int
|
||||
#
|
||||
# Used to specify the lookback window for pool statistics, hashrate calculations,
|
||||
# and other time-based mining series.
|
||||
TimePeriod = Literal["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y"]
|
||||
TimePeriod = Literal["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
# Index of the output being spent in the previous transaction
|
||||
Vout = int
|
||||
# Raw transaction version (i32) from Bitcoin protocol.
|
||||
@@ -354,8 +354,10 @@ class BlockExtras(TypedDict):
|
||||
segwitTotalSize: Total size of segwit transactions in bytes
|
||||
segwitTotalWeight: Total weight of segwit transactions
|
||||
header: Raw 80-byte block header as hex
|
||||
utxoSetChange: UTXO set change (outputs created minus inputs spent)
|
||||
utxoSetSize: Total UTXO set size at this height
|
||||
utxoSetChange: 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.
|
||||
utxoSetSize: Total spendable UTXO set size at this height (excludes OP_RETURN and other unspendable outputs)
|
||||
totalInputAmt: Total input amount in satoshis
|
||||
virtualSize: Virtual size in vbytes
|
||||
firstSeen: Timestamp when the block was first seen (always null, not yet supported)
|
||||
@@ -392,6 +394,24 @@ class BlockExtras(TypedDict):
|
||||
orphans: List[str]
|
||||
price: Dollars
|
||||
|
||||
class BlockFeeRatesEntry(TypedDict):
|
||||
"""
|
||||
A single block fee rates data point with percentiles.
|
||||
|
||||
Attributes:
|
||||
avgHeight: Average block height in this window
|
||||
timestamp: Unix timestamp at the window midpoint
|
||||
"""
|
||||
avgHeight: Height
|
||||
timestamp: Timestamp
|
||||
avgFee_0: FeeRate
|
||||
avgFee_10: FeeRate
|
||||
avgFee_25: FeeRate
|
||||
avgFee_50: FeeRate
|
||||
avgFee_75: FeeRate
|
||||
avgFee_90: FeeRate
|
||||
avgFee_100: FeeRate
|
||||
|
||||
class BlockFeesEntry(TypedDict):
|
||||
"""
|
||||
A single block fees data point.
|
||||
@@ -626,9 +646,9 @@ class CpfpInfo(TypedDict):
|
||||
ancestors: List[CpfpEntry]
|
||||
bestDescendant: Union[CpfpEntry, None]
|
||||
descendants: List[CpfpEntry]
|
||||
effectiveFeePerVsize: FeeRate
|
||||
fee: Sats
|
||||
adjustedVsize: VSize
|
||||
effectiveFeePerVsize: Union[FeeRate, None]
|
||||
fee: Union[Sats, None]
|
||||
adjustedVsize: Union[VSize, None]
|
||||
|
||||
class DataRangeFormat(TypedDict):
|
||||
"""
|
||||
@@ -6011,7 +6031,7 @@ class SeriesTree:
|
||||
class BrkClient(BrkClientBase):
|
||||
"""Main BRK client with series tree and API methods."""
|
||||
|
||||
VERSION = "v0.3.0-alpha.4"
|
||||
VERSION = "v0.3.0-alpha.5"
|
||||
|
||||
INDEXES = [
|
||||
"minute10",
|
||||
@@ -7777,15 +7797,15 @@ class BrkClient(BrkClientBase):
|
||||
path = f'/api/v1/historical-price{"?" + query if query else ""}'
|
||||
return self.get_json(path)
|
||||
|
||||
def get_block_fee_rates(self, time_period: TimePeriod) -> str:
|
||||
"""Block fee rates (WIP).
|
||||
def get_block_fee_rates(self, time_period: TimePeriod) -> List[BlockFeeRatesEntry]:
|
||||
"""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}`"""
|
||||
return self.get_text(f'/api/v1/mining/blocks/fee-rates/{time_period}')
|
||||
return self.get_json(f'/api/v1/mining/blocks/fee-rates/{time_period}')
|
||||
|
||||
def get_block_fees(self, time_period: TimePeriod) -> List[BlockFeesEntry]:
|
||||
"""Block fees.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "brk-client"
|
||||
version = "0.3.0-alpha.4"
|
||||
version = "0.3.0-alpha.5"
|
||||
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Compare brk output against bitcoin-cli to find discrepancies.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
BRK="http://localhost:3110"
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
warn=0
|
||||
|
||||
# Convert BTC decimal string to sats using python Decimal (no float loss)
|
||||
btc_to_sats() {
|
||||
python3 -c "from decimal import Decimal; print(int(Decimal('$1') * 100000000))"
|
||||
}
|
||||
|
||||
# Sum an array of BTC values to sats precisely
|
||||
sum_btc_values_to_sats() {
|
||||
python3 -c "
|
||||
import sys, json
|
||||
from decimal import Decimal
|
||||
vals = json.load(sys.stdin)
|
||||
print(int(sum(Decimal(str(v)) for v in vals) * 100000000))
|
||||
"
|
||||
}
|
||||
|
||||
compare() {
|
||||
local label="$1" btc_val="$2" brk_val="$3"
|
||||
if [[ "$btc_val" == "$brk_val" ]]; then
|
||||
printf "${GREEN} ✓ %-30s${NC} %s\n" "$label" "$btc_val"
|
||||
((pass++)) || true
|
||||
else
|
||||
printf "${RED} ✗ %-30s${NC} bitcoin-cli: %-20s brk: %-20s\n" "$label" "$btc_val" "$brk_val"
|
||||
((fail++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
compare_float() {
|
||||
local label="$1" btc_val="$2" brk_val="$3"
|
||||
local btc_short="${btc_val:0:15}" brk_short="${brk_val:0:15}"
|
||||
if [[ "$btc_short" == "$brk_short" ]]; then
|
||||
printf "${GREEN} ✓ %-30s${NC} %s\n" "$label" "$btc_val"
|
||||
((pass++)) || true
|
||||
else
|
||||
printf "${YELLOW} ~ %-30s${NC} bitcoin-cli: %-20s brk: %-20s\n" "$label" "$btc_val" "$brk_val"
|
||||
((warn++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
section() {
|
||||
printf "\n${BOLD}── %s ──${NC}\n" "$1"
|
||||
}
|
||||
|
||||
# ─── Get tip from brk ───
|
||||
brk_height=$(curl -sf "$BRK/api/blocks/tip/height")
|
||||
brk_hash=$(curl -sf "$BRK/api/blocks/tip/hash")
|
||||
btc_height=$(bitcoin-cli getblockcount)
|
||||
btc_hash=$(bitcoin-cli getbestblockhash)
|
||||
|
||||
section "Chain tip"
|
||||
compare "height" "$btc_height" "$brk_height"
|
||||
compare "hash" "$btc_hash" "$brk_hash"
|
||||
|
||||
# If tips differ, use the lower height for comparison
|
||||
if [[ "$btc_height" != "$brk_height" ]]; then
|
||||
printf "${YELLOW} Tips differ — comparing at brk height %s${NC}\n" "$brk_height"
|
||||
btc_hash=$(bitcoin-cli getblockhash "$brk_height")
|
||||
brk_hash=$(curl -sf "$BRK/api/block-height/$brk_height")
|
||||
compare "hash at $brk_height" "$btc_hash" "$brk_hash"
|
||||
fi
|
||||
|
||||
# ─── gettxoutsetinfo ───
|
||||
section "gettxoutsetinfo"
|
||||
printf " Running gettxoutsetinfo (this can take a while)...\n"
|
||||
txoutset=$(bitcoin-cli gettxoutsetinfo 2>/dev/null || echo '{}')
|
||||
|
||||
if [[ "$txoutset" != '{}' ]]; then
|
||||
btc_txouts=$(echo "$txoutset" | jq -r '.txouts')
|
||||
btc_total_amount=$(echo "$txoutset" | jq -r '.total_amount')
|
||||
btc_txoutset_height=$(echo "$txoutset" | jq -r '.height')
|
||||
|
||||
# Get brk utxoSetSize from block extras at that height
|
||||
txoutset_hash=$(bitcoin-cli getblockhash "$btc_txoutset_height")
|
||||
brk_block=$(curl -sf "$BRK/api/v1/block/$txoutset_hash")
|
||||
brk_utxo_set_size=$(echo "$brk_block" | jq -r '.extras.utxoSetSize')
|
||||
|
||||
compare "txouts (UTXO count)" "$btc_txouts" "$brk_utxo_set_size"
|
||||
|
||||
if [[ "$btc_txouts" != "$brk_utxo_set_size" ]]; then
|
||||
diff=$((brk_utxo_set_size - btc_txouts))
|
||||
printf "${RED} → delta: %d${NC}\n" "$diff"
|
||||
fi
|
||||
|
||||
btc_supply_sats=$(btc_to_sats "$btc_total_amount")
|
||||
printf " ${YELLOW}total_amount:${NC} bitcoin-cli: %s BTC (%s sats) — check brk supply series\n" "$btc_total_amount" "$btc_supply_sats"
|
||||
else
|
||||
printf " ${YELLOW}gettxoutsetinfo unavailable or timed out${NC}\n"
|
||||
fi
|
||||
|
||||
# ─── getblock comparison ───
|
||||
section "getblock vs /api/v1/block (at tip: $brk_hash)"
|
||||
|
||||
btc_block=$(bitcoin-cli getblock "$brk_hash")
|
||||
brk_v1=$(curl -sf "$BRK/api/v1/block/$brk_hash")
|
||||
|
||||
btc_size=$(echo "$btc_block" | jq -r '.size')
|
||||
btc_weight=$(echo "$btc_block" | jq -r '.weight')
|
||||
btc_nTx=$(echo "$btc_block" | jq -r '.nTx')
|
||||
btc_difficulty=$(echo "$btc_block" | jq -r '.difficulty')
|
||||
btc_version=$(echo "$btc_block" | jq -r '.version')
|
||||
# bitcoin-cli returns bits as hex string, brk as decimal — convert
|
||||
btc_bits_hex=$(echo "$btc_block" | jq -r '.bits')
|
||||
btc_bits=$(printf '%d' "0x$btc_bits_hex" 2>/dev/null || echo "$btc_bits_hex")
|
||||
btc_nonce=$(echo "$btc_block" | jq -r '.nonce')
|
||||
btc_timestamp=$(echo "$btc_block" | jq -r '.time')
|
||||
btc_mediantime=$(echo "$btc_block" | jq -r '.mediantime')
|
||||
btc_merkle=$(echo "$btc_block" | jq -r '.merkleroot')
|
||||
btc_prevhash=$(echo "$btc_block" | jq -r '.previousblockhash')
|
||||
|
||||
brk_size=$(echo "$brk_v1" | jq -r '.size')
|
||||
brk_weight=$(echo "$brk_v1" | jq -r '.weight')
|
||||
brk_nTx=$(echo "$brk_v1" | jq -r '.tx_count')
|
||||
brk_difficulty=$(echo "$brk_v1" | jq -r '.difficulty')
|
||||
brk_version=$(echo "$brk_v1" | jq -r '.version')
|
||||
brk_bits=$(echo "$brk_v1" | jq -r '.bits')
|
||||
brk_nonce=$(echo "$brk_v1" | jq -r '.nonce')
|
||||
brk_timestamp=$(echo "$brk_v1" | jq -r '.timestamp')
|
||||
brk_mediantime=$(echo "$brk_v1" | jq -r '.mediantime')
|
||||
brk_merkle=$(echo "$brk_v1" | jq -r '.merkle_root')
|
||||
brk_prevhash=$(echo "$brk_v1" | jq -r '.previousblockhash')
|
||||
|
||||
compare "size" "$btc_size" "$brk_size"
|
||||
compare "weight" "$btc_weight" "$brk_weight"
|
||||
compare "nTx / tx_count" "$btc_nTx" "$brk_nTx"
|
||||
compare_float "difficulty" "$btc_difficulty" "$brk_difficulty"
|
||||
compare "version" "$btc_version" "$brk_version"
|
||||
compare "bits" "$btc_bits" "$brk_bits"
|
||||
compare "nonce" "$btc_nonce" "$brk_nonce"
|
||||
compare "timestamp" "$btc_timestamp" "$brk_timestamp"
|
||||
compare "mediantime" "$btc_mediantime" "$brk_mediantime"
|
||||
compare "merkle_root" "$btc_merkle" "$brk_merkle"
|
||||
compare "previousblockhash" "$btc_prevhash" "$brk_prevhash"
|
||||
|
||||
# ─── Block extras sanity checks ───
|
||||
section "Block extras sanity (at tip)"
|
||||
|
||||
btc_block_v2=$(bitcoin-cli getblock "$brk_hash" 2 2>/dev/null || echo '{}')
|
||||
|
||||
if [[ "$btc_block_v2" != '{}' ]]; then
|
||||
btc_total_outputs=$(echo "$btc_block_v2" | jq '[.tx[].vout | length] | add')
|
||||
btc_total_inputs=$(echo "$btc_block_v2" | jq '[.tx[1:][].vin | length] | add // 0')
|
||||
|
||||
brk_total_outputs=$(echo "$brk_v1" | jq -r '.extras.totalOutputs')
|
||||
brk_total_inputs=$(echo "$brk_v1" | jq -r '.extras.totalInputs')
|
||||
|
||||
compare "totalOutputs" "$btc_total_outputs" "$brk_total_outputs"
|
||||
compare "totalInputs" "$btc_total_inputs" "$brk_total_inputs"
|
||||
|
||||
# totalOutputAmt excludes coinbase (matches mempool.space), so compare non-coinbase outputs only
|
||||
btc_total_output_amt=$(echo "$btc_block_v2" | jq '[.tx[1:][].vout[].value]' | sum_btc_values_to_sats)
|
||||
brk_total_output_amt=$(echo "$brk_v1" | jq -r '.extras.totalOutputAmt')
|
||||
compare "totalOutputAmt (sats)" "$btc_total_output_amt" "$brk_total_output_amt"
|
||||
|
||||
if [[ "$btc_total_output_amt" != "$brk_total_output_amt" ]]; then
|
||||
delta=$((brk_total_output_amt - btc_total_output_amt))
|
||||
printf "${RED} → delta: %d sats (%.8f BTC)${NC}\n" "$delta" "$(python3 -c "print($delta / 1e8)")"
|
||||
fi
|
||||
|
||||
# Reward = subsidy + fees. bitcoin-cli coinbase output sum = reward
|
||||
btc_coinbase_value=$(echo "$btc_block_v2" | jq '[.tx[0].vout[].value]' | sum_btc_values_to_sats)
|
||||
brk_reward=$(echo "$brk_v1" | jq -r '.extras.reward')
|
||||
compare "reward (coinbase sats)" "$btc_coinbase_value" "$brk_reward"
|
||||
|
||||
# Total input amount — needs verbosity 3 for prevout data
|
||||
btc_block_v3=$(bitcoin-cli getblock "$brk_hash" 3 2>/dev/null || echo '{}')
|
||||
if [[ "$btc_block_v3" != '{}' ]]; then
|
||||
btc_total_input_amt=$(echo "$btc_block_v3" | jq '[.tx[1:][].vin[].prevout.value]' | sum_btc_values_to_sats)
|
||||
brk_total_input_amt=$(echo "$brk_v1" | jq -r '.extras.totalInputAmt')
|
||||
compare "totalInputAmt (sats)" "$btc_total_input_amt" "$brk_total_input_amt"
|
||||
|
||||
if [[ "$btc_total_input_amt" != "$brk_total_input_amt" ]]; then
|
||||
delta=$((brk_total_input_amt - btc_total_input_amt))
|
||||
printf "${RED} → delta: %d sats (%.8f BTC)${NC}\n" "$delta" "$(python3 -c "print($delta / 1e8)")"
|
||||
fi
|
||||
|
||||
# fees = non-coinbase inputs - non-coinbase outputs
|
||||
btc_fees=$((btc_total_input_amt - btc_total_output_amt))
|
||||
brk_fees=$(echo "$brk_v1" | jq -r '.extras.totalFees')
|
||||
compare "totalFees (sats)" "$btc_fees" "$brk_fees"
|
||||
else
|
||||
printf " ${YELLOW}getblock verbosity 3 unavailable — skipping totalInputAmt${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf " ${YELLOW}getblock verbosity 2 unavailable${NC}\n"
|
||||
fi
|
||||
|
||||
# ─── getmempoolinfo ───
|
||||
section "getmempoolinfo vs /api/mempool"
|
||||
|
||||
btc_mempool=$(bitcoin-cli getmempoolinfo)
|
||||
brk_mempool=$(curl -sf "$BRK/api/mempool")
|
||||
|
||||
btc_mp_count=$(echo "$btc_mempool" | jq -r '.size')
|
||||
btc_mp_vsize=$(echo "$btc_mempool" | jq -r '.bytes')
|
||||
brk_mp_count=$(echo "$brk_mempool" | jq -r '.count')
|
||||
brk_mp_vsize=$(echo "$brk_mempool" | jq -r '.vsize')
|
||||
|
||||
printf " ${YELLOW}%-30s${NC} bitcoin-cli: %-12s brk: %-12s (live, may differ)\n" "tx count" "$btc_mp_count" "$brk_mp_count"
|
||||
printf " ${YELLOW}%-30s${NC} bitcoin-cli: %-12s brk: %-12s (live, may differ)\n" "vsize" "$btc_mp_vsize" "$brk_mp_vsize"
|
||||
|
||||
# ─── Difficulty adjustment ───
|
||||
section "Difficulty adjustment"
|
||||
|
||||
compare_float "current difficulty" "$(echo "$btc_block" | jq -r '.difficulty')" "$(echo "$brk_v1" | jq -r '.difficulty')"
|
||||
|
||||
# ─── Summary ───
|
||||
section "Summary"
|
||||
printf " ${GREEN}Passed: %d${NC} ${RED}Failed: %d${NC} ${YELLOW}Approximate: %d${NC}\n" "$pass" "$fail" "$warn"
|
||||
|
||||
if [[ $fail -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -180,21 +180,31 @@ function renderDetails(block) {
|
||||
["Pool", extras.pool.name],
|
||||
["Pool ID", extras.pool.id.toString()],
|
||||
["Pool Slug", extras.pool.slug],
|
||||
["Miner Names", extras.pool.minerNames || "N/A"],
|
||||
["Miner Names", extras.pool.minerNames?.join(", ") || "N/A"],
|
||||
["Reward", `${(extras.reward / 1e8).toFixed(8)} BTC`],
|
||||
["Total Fees", `${(extras.totalFees / 1e8).toFixed(8)} BTC`],
|
||||
["Median Fee Rate", `${formatFeeRate(extras.medianFee)} sat/vB`],
|
||||
["Avg Fee Rate", `${formatFeeRate(extras.avgFeeRate)} sat/vB`],
|
||||
["Avg Fee", `${extras.avgFee.toLocaleString()} sat`],
|
||||
["Median Fee", `${extras.medianFeeAmt.toLocaleString()} sat`],
|
||||
["Fee Range", extras.feeRange.map((f) => formatFeeRate(f)).join(", ") + " sat/vB"],
|
||||
["Fee Percentiles", extras.feePercentiles.map((f) => f.toLocaleString()).join(", ") + " sat"],
|
||||
[
|
||||
"Fee Range",
|
||||
extras.feeRange.map((f) => formatFeeRate(f)).join(", ") + " sat/vB",
|
||||
],
|
||||
[
|
||||
"Fee Percentiles",
|
||||
extras.feePercentiles.map((f) => f.toLocaleString()).join(", ") +
|
||||
" sat",
|
||||
],
|
||||
["Avg Tx Size", `${extras.avgTxSize.toLocaleString()} B`],
|
||||
["Virtual Size", `${extras.virtualSize.toLocaleString()} vB`],
|
||||
["Inputs", extras.totalInputs.toLocaleString()],
|
||||
["Outputs", extras.totalOutputs.toLocaleString()],
|
||||
["Total Input Amount", `${(extras.totalInputAmt / 1e8).toFixed(8)} BTC`],
|
||||
["Total Output Amount", `${(extras.totalOutputAmt / 1e8).toFixed(8)} BTC`],
|
||||
[
|
||||
"Total Output Amount",
|
||||
`${(extras.totalOutputAmt / 1e8).toFixed(8)} BTC`,
|
||||
],
|
||||
["UTXO Set Change", extras.utxoSetChange.toLocaleString()],
|
||||
["UTXO Set Size", extras.utxoSetSize.toLocaleString()],
|
||||
["SegWit Txs", extras.segwitTotalTxs.toLocaleString()],
|
||||
|
||||
@@ -47,7 +47,7 @@ a {
|
||||
}
|
||||
|
||||
aside {
|
||||
/*min-width: 0;*/
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
/*height: 100%;*/
|
||||
/*width: 100%;*/
|
||||
|
||||
@@ -25,7 +25,7 @@ main {
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
height: 21rem;
|
||||
height: 16rem;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
|
||||
@@ -4,26 +4,81 @@
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
overflow-y: auto;
|
||||
padding: var(--main-padding) 0;
|
||||
flex-direction: column;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--main-padding);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
transparent,
|
||||
var(--background-color)
|
||||
);
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 0;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--background-color)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
--cube: 4.5rem;
|
||||
|
||||
> * {
|
||||
padding: var(--main-padding);
|
||||
padding: 0 var(--main-padding);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: var(--main-padding);
|
||||
}
|
||||
}
|
||||
|
||||
#chain {
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: calc(var(--cube) * 0.75);
|
||||
--gap: 0.75;
|
||||
gap: calc(var(--cube) * var(--gap));
|
||||
margin-right: var(--cube);
|
||||
margin-top: calc(var(--cube) * -0.25);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
--gap: 1.25;
|
||||
flex-direction: row-reverse;
|
||||
height: 11.5rem;
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.cube {
|
||||
margin-top: -0.375rem;
|
||||
margin-left: calc(var(--cube) * -0.25);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
@@ -126,10 +181,13 @@
|
||||
|
||||
#block-details {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user