diff --git a/crates/brk_computer/src/price/oracle/compute.rs b/crates/brk_computer/src/price/oracle/compute.rs index 9a4f3da02..8d976b30b 100644 --- a/crates/brk_computer/src/price/oracle/compute.rs +++ b/crates/brk_computer/src/price/oracle/compute.rs @@ -2,7 +2,7 @@ use std::ops::Range; use brk_error::Result; use brk_indexer::Indexer; -use brk_oracle::{Config, Oracle, START_HEIGHT, bin_to_cents, cents_to_bin}; +use brk_oracle::{Config, NUM_BINS, Oracle, START_HEIGHT, bin_to_cents, cents_to_bin}; use brk_types::{ CentsUnsigned, Close, DateIndex, Height, High, Low, OHLCCentsUnsigned, OHLCDollars, Open, OutputType, Sats, TxIndex, TxOutIndex, @@ -358,7 +358,6 @@ impl Vecs { let mut value_iter = indexer.vecs.outputs.value.into_iter(); let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter(); - let mut block_outputs: Vec<(Sats, OutputType)> = Vec::new(); let mut ref_bins = Vec::with_capacity(range.len()); for h in range { @@ -382,15 +381,16 @@ impl Vecs { .unwrap_or(TxOutIndex::from(total_outputs)) .to_usize(); - block_outputs.clear(); + let mut hist = [0u32; NUM_BINS]; for i in out_start..out_end { - block_outputs.push(( - value_iter.get_at_unwrap(i), - outputtype_iter.get_at_unwrap(i), - )); + let sats: Sats = value_iter.get_at_unwrap(i); + let output_type: OutputType = outputtype_iter.get_at_unwrap(i); + if let Some(bin) = oracle.output_to_bin(sats, output_type) { + hist[bin] += 1; + } } - ref_bins.push(oracle.process_outputs(block_outputs.iter().copied())); + ref_bins.push(oracle.process_histogram(&hist)); } ref_bins diff --git a/crates/brk_oracle/examples/determinism.rs b/crates/brk_oracle/examples/determinism.rs new file mode 100644 index 000000000..303e950ba --- /dev/null +++ b/crates/brk_oracle/examples/determinism.rs @@ -0,0 +1,207 @@ +//! Verify oracle determinism: oracles started from different heights converge +//! to identical ref_bin values after the ring buffer fills. +//! +//! Creates a reference oracle at height 575k and test oracles every 1000 blocks +//! up to 630k. After window_size blocks, each test oracle should produce the +//! same ref_bin as the reference, proving the truncated EMA provides +//! start-point independence. +//! +//! Run with: cargo run -p brk_oracle --example determinism --release + +use std::path::PathBuf; + +use brk_indexer::Indexer; +use brk_oracle::{Config, NUM_BINS, Oracle, PRICES, START_HEIGHT, cents_to_bin, sats_to_bin}; +use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; +use vecdb::{AnyVec, VecIndex, VecIterator}; + +fn seed_bin(height: usize) -> f64 { + let price: f64 = PRICES + .lines() + .nth(height - 1) + .expect("prices.txt too short") + .parse() + .expect("Failed to parse seed price"); + cents_to_bin(price * 100.0) +} + +struct TestRun { + start_height: usize, + oracle: Option, + converged_at: Option, + diverged_after: bool, +} + +fn main() { + let data_dir = std::env::var("BRK_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap(); + PathBuf::from(home).join(".brk") + }); + + let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer"); + let total_heights = indexer.vecs.blocks.timestamp.len(); + + let config = Config::default(); + let window_size = config.window_size; + + let total_txs = indexer.vecs.transactions.height.len(); + let total_outputs = indexer.vecs.outputs.value.len(); + + let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter(); + let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter(); + let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter(); + let mut value_iter = indexer.vecs.outputs.value.into_iter(); + let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter(); + + let ref_config = Config::default(); + + // Reference oracle at 575k. + let ref_start = START_HEIGHT; + let mut ref_oracle = Oracle::new(seed_bin(ref_start), Config::default()); + + // Test oracles every 1000 blocks from 576k to 630k. + let mut runs: Vec = (576_000..=630_000) + .step_by(1000) + .map(|h| TestRun { + start_height: h, + oracle: None, + converged_at: None, + diverged_after: false, + }) + .collect(); + + let last_start = runs.last().map(|r| r.start_height).unwrap_or(ref_start); + // Process enough blocks for all oracles to converge + verification margin. + let end_height = (last_start + window_size + 100).min(total_heights); + + for h in START_HEIGHT..end_height { + let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h); + let next_first_txindex = first_txindex_iter + .get_at(h + 1) + .unwrap_or(TxIndex::from(total_txs)); + + let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() { + first_txoutindex_iter + .get_at_unwrap(first_txindex.to_usize() + 1) + .to_usize() + } else { + out_first_iter + .get_at(h + 1) + .unwrap_or(TxOutIndex::from(total_outputs)) + .to_usize() + }; + let out_end = out_first_iter + .get_at(h + 1) + .unwrap_or(TxOutIndex::from(total_outputs)) + .to_usize(); + + let mut hist = [0u32; NUM_BINS]; + for i in out_start..out_end { + let sats: Sats = value_iter.get_at_unwrap(i); + let output_type: OutputType = outputtype_iter.get_at_unwrap(i); + if ref_config.excluded_output_types.contains(&output_type) { + continue; + } + if *sats < ref_config.min_sats + || (ref_config.exclude_common_round_values && sats.is_common_round_value()) + { + continue; + } + if let Some(bin) = sats_to_bin(sats) { + hist[bin] += 1; + } + } + + let ref_bin = ref_oracle.process_histogram(&hist); + + for run in &mut runs { + if h < run.start_height { + continue; + } + if run.oracle.is_none() { + run.oracle = Some(Oracle::new(seed_bin(run.start_height), Config::default())); + } + let test_bin = run.oracle.as_mut().unwrap().process_histogram(&hist); + + if run.converged_at.is_some() { + if test_bin != ref_bin { + run.diverged_after = true; + } + } else if test_bin == ref_bin { + run.converged_at = Some(h); + } + } + } + + // Print results. + println!(); + println!( + "{:<12} {:>16} {:>8}", + "Start", "Converged at", "Blocks" + ); + println!("{}", "-".repeat(40)); + + let mut max_blocks = 0usize; + let mut failed = Vec::new(); + let mut diverged = Vec::new(); + + for run in &runs { + if let Some(converged) = run.converged_at { + let blocks = converged - run.start_height; + if blocks > max_blocks { + max_blocks = blocks; + } + println!( + "{:<12} {:>16} {:>8}", + run.start_height, converged, blocks + ); + if run.diverged_after { + diverged.push(run.start_height); + } + } else { + println!("{:<12} {:>16} {:>8}", run.start_height, "NEVER", "-"); + failed.push(run.start_height); + } + } + + println!(); + println!( + "{}/{} converged, max {} blocks to converge (window_size={})", + runs.len() - failed.len(), + runs.len(), + max_blocks, + window_size, + ); + + if !diverged.is_empty() { + println!("DIVERGED after convergence: {:?}", diverged); + } + if !failed.is_empty() { + println!("NEVER converged: {:?}", failed); + } + + // Assertions. + assert!( + failed.is_empty(), + "{} oracles never converged: {:?}", + failed.len(), + failed + ); + assert!( + diverged.is_empty(), + "{} oracles diverged after convergence: {:?}", + diverged.len(), + diverged + ); + assert!( + max_blocks <= window_size * 2, + "Convergence took {} blocks, expected <= {} (2 * window_size)", + max_blocks, + window_size * 2 + ); + + println!(); + println!("All assertions passed!"); +} diff --git a/crates/brk_oracle/src/lib.rs b/crates/brk_oracle/src/lib.rs index c67fc5917..db7a81115 100644 --- a/crates/brk_oracle/src/lib.rs +++ b/crates/brk_oracle/src/lib.rs @@ -132,6 +132,7 @@ fn find_best_bin( best_bin as f64 + sub_bin } +#[derive(Clone)] pub struct Config { /// EMA decay: 2/(N+1) where N is span in blocks. 2/7 = 6-block span. pub alpha: f64, @@ -162,29 +163,39 @@ impl Default for Config { } } +#[derive(Clone)] pub struct Oracle { histograms: Vec<[u32; NUM_BINS]>, ema: Box<[f64; NUM_BINS]>, - weights: Vec, cursor: usize, filled: usize, ref_bin: f64, config: Config, + weights: Vec, + excluded_mask: u16, + warmup: bool, } impl Oracle { pub fn new(start_bin: f64, config: Config) -> Self { - let weights: Vec = (0..config.window_size) - .map(|age| config.alpha * (1.0 - config.alpha).powi(age as i32)) - .collect(); let window_size = config.window_size; + let decay = 1.0 - config.alpha; + let weights: Vec = (0..window_size) + .map(|i| config.alpha * decay.powi(i as i32)) + .collect(); + let excluded_mask = config + .excluded_output_types + .iter() + .fold(0u16, |mask, ot| mask | (1 << *ot as u8)); Self { histograms: vec![[0u32; NUM_BINS]; window_size], ema: Box::new([0.0; NUM_BINS]), - weights, cursor: 0, filled: 0, ref_bin: start_bin, + weights, + excluded_mask, + warmup: false, config, } } @@ -215,7 +226,10 @@ impl Oracle { /// ref_bin is anchored to the checkpoint regardless of warmup drift. pub fn from_checkpoint(ref_bin: f64, config: Config, fill: impl FnOnce(&mut Self)) -> Self { let mut oracle = Self::new(ref_bin, config); + oracle.warmup = true; fill(&mut oracle); + oracle.warmup = false; + oracle.recompute_ema(); oracle.ref_bin = ref_bin; oracle } @@ -236,12 +250,19 @@ impl Oracle { self.price_cents().into() } + #[inline(always)] + pub fn output_to_bin(&self, sats: Sats, output_type: OutputType) -> Option { + self.eligible_bin(sats, output_type) + } + #[inline(always)] fn eligible_bin(&self, sats: Sats, output_type: OutputType) -> Option { - if self.config.excluded_output_types.contains(&output_type) { + if self.excluded_mask & (1 << output_type as u8) != 0 { return None; } - if *sats < self.config.min_sats || (self.config.exclude_common_round_values && sats.is_common_round_value()) { + if *sats < self.config.min_sats + || (self.config.exclude_common_round_values && sats.is_common_round_value()) + { return None; } sats_to_bin(sats) @@ -254,24 +275,28 @@ impl Oracle { self.filled += 1; } - self.recompute_ema(); + if !self.warmup { + self.recompute_ema(); - self.ref_bin = find_best_bin( - &self.ema, - self.ref_bin, - self.config.search_below, - self.config.search_above, - ); + self.ref_bin = find_best_bin( + &self.ema, + self.ref_bin, + self.config.search_below, + self.config.search_above, + ); + } self.ref_bin } fn recompute_ema(&mut self) { self.ema.fill(0.0); for age in 0..self.filled { - let idx = (self.cursor + self.config.window_size - 1 - age) % self.config.window_size; + let idx = + (self.cursor + self.config.window_size - 1 - age) % self.config.window_size; let weight = self.weights[age]; + let h = &self.histograms[idx]; for bin in 0..NUM_BINS { - self.ema[bin] += weight * self.histograms[idx][bin] as f64; + self.ema[bin] += weight * h[bin] as f64; } } } diff --git a/crates/brk_query/src/impl/price.rs b/crates/brk_query/src/impl/price.rs index 5220733e7..9bc73bbe6 100644 --- a/crates/brk_query/src/impl/price.rs +++ b/crates/brk_query/src/impl/price.rs @@ -16,12 +16,11 @@ impl Query { if let Some(mempool) = self.mempool() { let txs = mempool.get_txs(); - let mempool_outputs: Vec<_> = txs - .values() - .flat_map(|tx| &tx.tx().output) - .map(|txout| (txout.value, txout.type_())) - .collect(); - oracle.process_outputs(mempool_outputs.into_iter()); + oracle.process_outputs( + txs.values() + .flat_map(|tx| &tx.tx().output) + .map(|txout| (txout.value, txout.type_())), + ); } Ok(oracle.price_dollars()) diff --git a/crates/brk_server/src/api/addresses/mod.rs b/crates/brk_server/src/api/addresses/mod.rs index b3afe34ad..306d70e91 100644 --- a/crates/brk_server/src/api/addresses/mod.rs +++ b/crates/brk_server/src/api/addresses/mod.rs @@ -1,7 +1,7 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, Query, State}, - http::HeaderMap, + http::{HeaderMap, Uri}, response::Redirect, routing::get, }; @@ -26,11 +26,12 @@ impl AddressRoutes for ApiRouter { .api_route( "/api/address/{address}", get_with(async | + uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.address(path.address)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address(path.address)).await }, |op| op .id("get_address") .addresses_tag() @@ -46,12 +47,13 @@ impl AddressRoutes for ApiRouter { .api_route( "/api/address/{address}/txs", get_with(async | + uri: Uri, headers: HeaderMap, Path(path): Path, Query(params): Query, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(path.address, params.after_txid, params.limit)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, params.limit)).await }, |op| op .id("get_address_txs") .addresses_tag() @@ -67,11 +69,12 @@ impl AddressRoutes for ApiRouter { .api_route( "/api/address/{address}/utxo", get_with(async | + uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_utxos(path.address)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_utxos(path.address)).await }, |op| op .id("get_address_utxos") .addresses_tag() @@ -87,12 +90,13 @@ impl AddressRoutes for ApiRouter { .api_route( "/api/address/{address}/txs/mempool", get_with(async | + uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State | { let hash = state.sync(|q| q.address_mempool_hash(&path.address)); - state.cached_json(&headers, CacheStrategy::MempoolHash(hash), move |q| q.address_mempool_txids(path.address)).await + state.cached_json(&headers, CacheStrategy::MempoolHash(hash), &uri, move |q| q.address_mempool_txids(path.address)).await }, |op| op .id("get_address_mempool_txs") .addresses_tag() @@ -107,12 +111,13 @@ impl AddressRoutes for ApiRouter { .api_route( "/api/address/{address}/txs/chain", get_with(async | + uri: Uri, headers: HeaderMap, Path(path): Path, Query(params): Query, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(path.address, params.after_txid, 25)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, 25)).await }, |op| op .id("get_address_confirmed_txs") .addresses_tag() @@ -128,11 +133,12 @@ impl AddressRoutes for ApiRouter { .api_route( "/api/v1/validate-address/{address}", get_with(async | + uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Static, move |_q| Ok(AddressValidation::from_address(&path.address))).await + state.cached_json(&headers, CacheStrategy::Static, &uri, move |_q| Ok(AddressValidation::from_address(&path.address))).await }, |op| op .id("validate_address") .addresses_tag() diff --git a/crates/brk_server/src/api/blocks/mod.rs b/crates/brk_server/src/api/blocks/mod.rs index d56414993..1c357cf6f 100644 --- a/crates/brk_server/src/api/blocks/mod.rs +++ b/crates/brk_server/src/api/blocks/mod.rs @@ -1,7 +1,7 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, State}, - http::HeaderMap, + http::{HeaderMap, Uri}, }; use brk_query::BLOCK_TXS_PAGE_SIZE; use brk_types::{ @@ -22,9 +22,9 @@ impl BlockRoutes for ApiRouter { self.api_route( "/api/blocks", get_with( - async |headers: HeaderMap, State(state): State| { + async |uri: Uri, headers: HeaderMap, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(None)) + .cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(None)) .await }, |op| { @@ -41,10 +41,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/block/{hash}", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, move |q| q.block(&path.hash)).await + state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block(&path.hash)).await }, |op| { op.id("get_block") @@ -64,10 +65,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/block/{hash}/status", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_status(&path.hash)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_status(&path.hash)).await }, |op| { op.id("get_block_status") @@ -87,10 +89,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/block-height/{height}", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_height(path.height)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_height(path.height)).await }, |op| { op.id("get_block_by_height") @@ -110,10 +113,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/blocks/{height}", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(Some(path.height))).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await }, |op| { op.id("get_blocks_from_height") @@ -132,10 +136,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/block/{hash}/txids", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txids(&path.hash)).await + state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txids(&path.hash)).await }, |op| { op.id("get_block_txids") @@ -155,10 +160,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/block/{hash}/txs/{start_index}", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txs(&path.hash, path.start_index)).await + state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txs(&path.hash, path.start_index)).await }, |op| { op.id("get_block_txs") @@ -179,10 +185,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/block/{hash}/txid/{index}", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_text(&headers, CacheStrategy::Static, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await + state.cached_text(&headers, CacheStrategy::Static, &uri, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await }, |op| { op.id("get_block_txid") @@ -202,10 +209,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/v1/mining/blocks/timestamp/{timestamp}", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_timestamp(path.timestamp)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await }, |op| { op.id("get_block_by_timestamp") @@ -223,10 +231,11 @@ impl BlockRoutes for ApiRouter { .api_route( "/api/block/{hash}/raw", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_bytes(&headers, CacheStrategy::Static, move |q| q.block_raw(&path.hash)).await + state.cached_bytes(&headers, CacheStrategy::Static, &uri, move |q| q.block_raw(&path.hash)).await }, |op| { op.id("get_block_raw") diff --git a/crates/brk_server/src/api/mempool/mod.rs b/crates/brk_server/src/api/mempool/mod.rs index c553afcd2..24feb88a8 100644 --- a/crates/brk_server/src/api/mempool/mod.rs +++ b/crates/brk_server/src/api/mempool/mod.rs @@ -1,5 +1,5 @@ use aide::axum::{ApiRouter, routing::get_with}; -use axum::{extract::State, http::HeaderMap, response::Redirect, routing::get}; +use axum::{extract::State, http::{HeaderMap, Uri}, response::Redirect, routing::get}; use brk_types::{Dollars, MempoolBlock, MempoolInfo, RecommendedFees, Txid}; use crate::extended::TransformResponseExtended; @@ -17,8 +17,8 @@ impl MempoolRoutes for ApiRouter { .api_route( "/api/mempool/info", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, state.mempool_cache(), |q| q.mempool_info()).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_info()).await }, |op| { op.id("get_mempool") @@ -33,8 +33,8 @@ impl MempoolRoutes for ApiRouter { .api_route( "/api/mempool/txids", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, state.mempool_cache(), |q| q.mempool_txids()).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_txids()).await }, |op| { op.id("get_mempool_txids") @@ -49,8 +49,8 @@ impl MempoolRoutes for ApiRouter { .api_route( "/api/v1/fees/recommended", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, state.mempool_cache(), |q| q.recommended_fees()).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.recommended_fees()).await }, |op| { op.id("get_recommended_fees") @@ -65,12 +65,8 @@ impl MempoolRoutes for ApiRouter { .api_route( "/api/mempool/price", get_with( - async |headers: HeaderMap, State(state): State| { - state - .server_cached_json(&headers, state.mempool_cache(), "price", |q| { - q.live_price() - }) - .await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.live_price()).await }, |op| { op.id("get_live_price") @@ -89,8 +85,8 @@ impl MempoolRoutes for ApiRouter { .api_route( "/api/v1/fees/mempool-blocks", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, state.mempool_cache(), |q| q.mempool_blocks()).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_blocks()).await }, |op| { op.id("get_mempool_blocks") diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index cf1b45b01..15973fbb0 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -48,8 +48,8 @@ impl ApiMetricsRoutes for ApiRouter { self.api_route( "/api/metrics", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metrics_catalog().clone())).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metrics_catalog().clone())).await }, |op| op .id("get_metrics_tree") @@ -67,10 +67,11 @@ impl ApiMetricsRoutes for ApiRouter { "/api/metrics/count", get_with( async | + uri: Uri, headers: HeaderMap, State(state): State | { - state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metric_count())).await + state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metric_count())).await }, |op| op .id("get_metrics_count") @@ -85,10 +86,11 @@ impl ApiMetricsRoutes for ApiRouter { "/api/metrics/indexes", get_with( async | + uri: Uri, headers: HeaderMap, State(state): State | { - state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.indexes().to_vec())).await + state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.indexes().to_vec())).await }, |op| op .id("get_indexes") @@ -105,11 +107,12 @@ impl ApiMetricsRoutes for ApiRouter { "/api/metrics/list", get_with( async | + uri: Uri, headers: HeaderMap, State(state): State, Query(pagination): Query | { - state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.metrics(pagination))).await + state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.metrics(pagination))).await }, |op| op .id("list_metrics") @@ -124,12 +127,13 @@ impl ApiMetricsRoutes for ApiRouter { "/api/metrics/search/{metric}", get_with( async | + uri: Uri, headers: HeaderMap, State(state): State, Path(path): Path, Query(query): Query | { - state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.match_metric(&path.metric, query.limit))).await + state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.match_metric(&path.metric, query.limit))).await }, |op| op .id("search_metrics") @@ -145,11 +149,12 @@ impl ApiMetricsRoutes for ApiRouter { "/api/metric/{metric}", get_with( async | + uri: Uri, headers: HeaderMap, State(state): State, Path(path): Path | { - state.cached_json(&headers, CacheStrategy::Static, move |q| { + state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| { if let Some(indexes) = q.metric_to_indexes(path.metric.clone()) { return Ok(indexes.clone()) } @@ -296,9 +301,9 @@ impl ApiMetricsRoutes for ApiRouter { .api_route( "/api/metrics/cost-basis", get_with( - async |headers: HeaderMap, State(state): State| { + async |uri: Uri, headers: HeaderMap, State(state): State| { state - .cached_json(&headers, CacheStrategy::Static, |q| q.cost_basis_cohorts()) + .cached_json(&headers, CacheStrategy::Static, &uri, |q| q.cost_basis_cohorts()) .await }, |op| { @@ -314,11 +319,12 @@ impl ApiMetricsRoutes for ApiRouter { .api_route( "/api/metrics/cost-basis/{cohort}/dates", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(params): Path, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, move |q| { + .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { q.cost_basis_dates(¶ms.cohort) }) .await @@ -337,12 +343,13 @@ impl ApiMetricsRoutes for ApiRouter { .api_route( "/api/metrics/cost-basis/{cohort}/{date}", get_with( - async |headers: HeaderMap, + async |uri: Uri, + headers: HeaderMap, Path(params): Path, Query(query): Query, State(state): State| { state - .cached_json(&headers, CacheStrategy::Static, move |q| { + .cached_json(&headers, CacheStrategy::Static, &uri, move |q| { q.cost_basis_formatted( ¶ms.cohort, params.date, diff --git a/crates/brk_server/src/api/mining/mod.rs b/crates/brk_server/src/api/mining/mod.rs index c88565510..33157ef7c 100644 --- a/crates/brk_server/src/api/mining/mod.rs +++ b/crates/brk_server/src/api/mining/mod.rs @@ -1,7 +1,7 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, State}, - http::HeaderMap, + http::{HeaderMap, Uri}, response::Redirect, routing::get, }; @@ -28,8 +28,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/difficulty-adjustment", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustment()).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.difficulty_adjustment()).await }, |op| { op.id("get_difficulty_adjustment") @@ -45,9 +45,9 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/pools", get_with( - async |headers: HeaderMap, State(state): State| { + async |uri: Uri, headers: HeaderMap, State(state): State| { // Pool list is static, only changes on code update - state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.all_pools())).await + state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.all_pools())).await }, |op| { op.id("get_pools") @@ -63,8 +63,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/pools/{time_period}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.mining_pools(path.time_period)).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.mining_pools(path.time_period)).await }, |op| { op.id("get_pool_stats") @@ -80,8 +80,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/pool/{slug}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.pool_detail(path.slug)).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_detail(path.slug)).await }, |op| { op.id("get_pool") @@ -98,8 +98,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/hashrate", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, |q| q.hashrate(None)).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.hashrate(None)).await }, |op| { op.id("get_hashrate") @@ -115,8 +115,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/hashrate/{time_period}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.hashrate(Some(path.time_period))).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.hashrate(Some(path.time_period))).await }, |op| { op.id("get_hashrate_by_period") @@ -132,8 +132,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/difficulty-adjustments", get_with( - async |headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustments(None)).await + async |uri: Uri, headers: HeaderMap, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.difficulty_adjustments(None)).await }, |op| { op.id("get_difficulty_adjustments") @@ -149,8 +149,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/difficulty-adjustments/{time_period}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.difficulty_adjustments(Some(path.time_period))).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.difficulty_adjustments(Some(path.time_period))).await }, |op| { op.id("get_difficulty_adjustments_by_period") @@ -166,8 +166,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/blocks/fees/{time_period}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_fees(path.time_period)).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_fees(path.time_period)).await }, |op| { op.id("get_block_fees") @@ -183,8 +183,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/blocks/rewards/{time_period}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_rewards(path.time_period)).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_rewards(path.time_period)).await }, |op| { op.id("get_block_rewards") @@ -218,8 +218,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/blocks/sizes-weights/{time_period}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_sizes_weights(path.time_period)).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_sizes_weights(path.time_period)).await }, |op| { op.id("get_block_sizes_weights") @@ -235,8 +235,8 @@ impl MiningRoutes for ApiRouter { .api_route( "/api/v1/mining/reward-stats/{block_count}", get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.reward_stats(path.block_count)).await + async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.reward_stats(path.block_count)).await }, |op| { op.id("get_reward_stats") diff --git a/crates/brk_server/src/api/server/mod.rs b/crates/brk_server/src/api/server/mod.rs index 917b9f8fb..118ed44c7 100644 --- a/crates/brk_server/src/api/server/mod.rs +++ b/crates/brk_server/src/api/server/mod.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, fs, path}; use aide::axum::{ApiRouter, routing::get_with}; -use axum::{extract::State, http::HeaderMap}; +use axum::{extract::State, http::{HeaderMap, Uri}}; use brk_types::{DiskUsage, Health, Height, SyncStatus}; use vecdb::GenericStoredVec; @@ -18,11 +18,11 @@ impl ServerRoutes for ApiRouter { self.api_route( "/api/server/sync", get_with( - async |headers: HeaderMap, State(state): State| { + async |uri: Uri, headers: HeaderMap, State(state): State| { let tip_height = state.client.get_last_height(); state - .cached_json(&headers, CacheStrategy::Height, move |q| { + .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { let indexed_height = q.height(); let tip_height = tip_height?; let blocks_behind = Height::from(tip_height.saturating_sub(*indexed_height)); @@ -59,10 +59,10 @@ impl ServerRoutes for ApiRouter { .api_route( "/api/server/disk", get_with( - async |headers: HeaderMap, State(state): State| { + async |uri: Uri, headers: HeaderMap, State(state): State| { let brk_path = state.data_path.clone(); state - .cached_json(&headers, CacheStrategy::Height, move |q| { + .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { let brk_bytes = dir_size(&brk_path)?; let bitcoin_bytes = dir_size(q.blocks_dir())?; Ok(DiskUsage::new(brk_bytes, bitcoin_bytes)) @@ -106,9 +106,9 @@ impl ServerRoutes for ApiRouter { .api_route( "/version", get_with( - async |headers: HeaderMap, State(state): State| { + async |uri: Uri, headers: HeaderMap, State(state): State| { state - .cached_json(&headers, CacheStrategy::Static, |_| { + .cached_json(&headers, CacheStrategy::Static, &uri, |_| { Ok(env!("CARGO_PKG_VERSION")) }) .await diff --git a/crates/brk_server/src/api/transactions/mod.rs b/crates/brk_server/src/api/transactions/mod.rs index f6e2c44fa..2a6ea7b13 100644 --- a/crates/brk_server/src/api/transactions/mod.rs +++ b/crates/brk_server/src/api/transactions/mod.rs @@ -1,7 +1,7 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, State}, - http::HeaderMap, + http::{HeaderMap, Uri}, response::Redirect, routing::get, }; @@ -24,11 +24,12 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}", get_with( async | + uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction(txid)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction(txid)).await }, |op| op .id("get_tx") @@ -48,11 +49,12 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}/status", get_with( async | + uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction_status(txid)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_status(txid)).await }, |op| op .id("get_tx_status") @@ -72,11 +74,12 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}/hex", get_with( async | + uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State | { - state.cached_text(&headers, CacheStrategy::Height, move |q| q.transaction_hex(txid)).await + state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_hex(txid)).await }, |op| op .id("get_tx_hex") @@ -96,12 +99,13 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}/outspend/{vout}", get_with( async | + uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State | { let txid = TxidParam { txid: path.txid }; - state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspend(txid, path.vout)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspend(txid, path.vout)).await }, |op| op .id("get_tx_outspend") @@ -121,11 +125,12 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}/outspends", get_with( async | + uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspends(txid)).await + state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspends(txid)).await }, |op| op .id("get_tx_outspends") diff --git a/crates/brk_server/src/extended/response.rs b/crates/brk_server/src/extended/response.rs index dec423dc7..1743178a7 100644 --- a/crates/brk_server/src/extended/response.rs +++ b/crates/brk_server/src/extended/response.rs @@ -27,10 +27,8 @@ where T: Serialize; fn new_text(value: &str, etag: &str) -> Self; fn new_text_with(status: StatusCode, value: &str, etag: &str) -> Self; - fn new_text_cached(value: &str, params: &CacheParams) -> Self; fn new_bytes(value: Vec, etag: &str) -> Self; fn new_bytes_with(status: StatusCode, value: Vec, etag: &str) -> Self; - fn new_bytes_cached(value: Vec, params: &CacheParams) -> Self; } impl ResponseExtended for Response { @@ -114,26 +112,4 @@ impl ResponseExtended for Response { } Self::new_json_cached(value, ¶ms) } - - fn new_text_cached(value: &str, params: &CacheParams) -> Self { - let mut response = Response::builder().body(value.to_string().into()).unwrap(); - let headers = response.headers_mut(); - headers.insert_content_type_text_plain(); - headers.insert_cache_control(¶ms.cache_control); - if let Some(etag) = ¶ms.etag { - headers.insert_etag(etag); - } - response - } - - fn new_bytes_cached(value: Vec, params: &CacheParams) -> Self { - let mut response = Response::builder().body(value.into()).unwrap(); - let headers = response.headers_mut(); - headers.insert_content_type_octet_stream(); - headers.insert_cache_control(¶ms.cache_control); - if let Some(etag) = ¶ms.etag { - headers.insert_etag(etag); - } - response - } } diff --git a/crates/brk_server/src/state.rs b/crates/brk_server/src/state.rs index 80216ca10..dca043e28 100644 --- a/crates/brk_server/src/state.rs +++ b/crates/brk_server/src/state.rs @@ -1,15 +1,15 @@ -use std::{path::PathBuf, sync::Arc, time::Instant}; +use std::{future::Future, path::PathBuf, sync::Arc, time::{Duration, Instant}}; use derive_more::Deref; use axum::{ body::{Body, Bytes}, - http::{HeaderMap, Response}, + http::{HeaderMap, Response, Uri}, }; use brk_query::AsyncQuery; use brk_rpc::Client; use jiff::Timestamp; -use quick_cache::sync::Cache; +use quick_cache::sync::{Cache, GuardResult}; use serde::Serialize; use crate::{ @@ -35,33 +35,12 @@ impl AppState { CacheStrategy::MempoolHash(hash) } - /// JSON response with caching + /// JSON response with HTTP + server-side caching pub async fn cached_json( &self, headers: &HeaderMap, strategy: CacheStrategy, - f: F, - ) -> Response - where - T: Serialize + Send + 'static, - F: FnOnce(&brk_query::Query) -> brk_error::Result + Send + 'static, - { - let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into())); - if params.matches_etag(headers) { - return ResponseExtended::new_not_modified(); - } - match self.run(f).await { - Ok(value) => ResponseExtended::new_json_cached(&value, ¶ms), - Err(e) => ResultExtended::::to_json_response(Err(e), params.etag_str()), - } - } - - /// JSON response with HTTP caching + server-side cache - pub async fn server_cached_json( - &self, - headers: &HeaderMap, - strategy: CacheStrategy, - cache_prefix: &str, + uri: &Uri, f: F, ) -> Response where @@ -73,9 +52,9 @@ impl AppState { return ResponseExtended::new_not_modified(); } - let cache_key = format!("{cache_prefix}-{}", params.etag_str()); + let full_key = format!("{}-{}", uri, params.etag_str()); let result = self - .get_or_insert(&cache_key, async move { + .get_or_insert(&full_key, async move { let value = self.run(f).await?; Ok(serde_json::to_vec(&value).unwrap().into()) }) @@ -96,18 +75,95 @@ impl AppState { } } + /// Text response with HTTP + server-side caching + pub async fn cached_text( + &self, + headers: &HeaderMap, + strategy: CacheStrategy, + uri: &Uri, + f: F, + ) -> Response + where + T: AsRef + Send + 'static, + F: FnOnce(&brk_query::Query) -> brk_error::Result + Send + 'static, + { + let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into())); + if params.matches_etag(headers) { + return ResponseExtended::new_not_modified(); + } + + let full_key = format!("{}-{}", uri, params.etag_str()); + let result = self + .get_or_insert(&full_key, async move { + let value = self.run(f).await?; + Ok(Bytes::from(value.as_ref().to_owned())) + }) + .await; + + match result { + Ok(bytes) => { + let mut response = Response::new(Body::from(bytes)); + let h = response.headers_mut(); + h.insert_content_type_text_plain(); + h.insert_cache_control(¶ms.cache_control); + if let Some(etag) = ¶ms.etag { + h.insert_etag(etag); + } + response + } + Err(e) => ResultExtended::::to_text_response(Err(e), params.etag_str()), + } + } + + /// Binary response with HTTP + server-side caching + pub async fn cached_bytes( + &self, + headers: &HeaderMap, + strategy: CacheStrategy, + uri: &Uri, + f: F, + ) -> Response + where + T: Into> + Send + 'static, + F: FnOnce(&brk_query::Query) -> brk_error::Result + Send + 'static, + { + let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into())); + if params.matches_etag(headers) { + return ResponseExtended::new_not_modified(); + } + + let full_key = format!("{}-{}", uri, params.etag_str()); + let result = self + .get_or_insert(&full_key, async move { + let value = self.run(f).await?; + Ok(Bytes::from(value.into())) + }) + .await; + + match result { + Ok(bytes) => { + let mut response = Response::new(Body::from(bytes)); + let h = response.headers_mut(); + h.insert_content_type_octet_stream(); + h.insert_cache_control(¶ms.cache_control); + if let Some(etag) = ¶ms.etag { + h.insert_etag(etag); + } + response + } + Err(e) => ResultExtended::::to_bytes_response(Err(e), params.etag_str()), + } + } + /// Check server-side cache, compute on miss pub async fn get_or_insert( &self, cache_key: &str, - compute: impl std::future::Future>, + compute: impl Future>, ) -> brk_error::Result { - use quick_cache::sync::GuardResult; - - let guard_res = self.cache.get_value_or_guard( - cache_key, - Some(std::time::Duration::from_millis(50)), - ); + let guard_res = self + .cache + .get_value_or_guard(cache_key, Some(Duration::from_millis(50))); if let GuardResult::Value(bytes) = guard_res { return Ok(bytes); @@ -121,46 +177,4 @@ impl AppState { Ok(bytes) } - - /// Text response with caching - pub async fn cached_text( - &self, - headers: &HeaderMap, - strategy: CacheStrategy, - f: F, - ) -> Response - where - T: AsRef + Send + 'static, - F: FnOnce(&brk_query::Query) -> brk_error::Result + Send + 'static, - { - let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into())); - if params.matches_etag(headers) { - return ResponseExtended::new_not_modified(); - } - match self.run(f).await { - Ok(value) => ResponseExtended::new_text_cached(value.as_ref(), ¶ms), - Err(e) => ResultExtended::::to_text_response(Err(e), params.etag_str()), - } - } - - /// Binary response with caching - pub async fn cached_bytes( - &self, - headers: &HeaderMap, - strategy: CacheStrategy, - f: F, - ) -> Response - where - T: Into> + Send + 'static, - F: FnOnce(&brk_query::Query) -> brk_error::Result + Send + 'static, - { - let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into())); - if params.matches_etag(headers) { - return ResponseExtended::new_not_modified(); - } - match self.run(f).await { - Ok(value) => ResponseExtended::new_bytes_cached(value.into(), ¶ms), - Err(e) => ResultExtended::::to_bytes_response(Err(e), params.etag_str()), - } - } } diff --git a/crates/brk_types/src/sats.rs b/crates/brk_types/src/sats.rs index a7cf8927a..868838729 100644 --- a/crates/brk_types/src/sats.rs +++ b/crates/brk_types/src/sats.rs @@ -83,45 +83,14 @@ impl Sats { if self.0 == 0 { return false; } - let log = (self.0 as f64).log10(); - let magnitude = 10.0_f64.powf(log.floor()); - let leading = (self.0 as f64 / magnitude).round() as u64; + let mag = 10u64.pow(self.0.ilog10()); + let leading = (self.0 + mag / 2) / mag; if !matches!(leading, 1 | 2 | 3 | 5 | 6 | 10) { return false; } - let round_val = leading as f64 * magnitude; - (self.0 as f64 - round_val).abs() <= round_val * 0.001 + let round_val = leading * mag; + self.0.abs_diff(round_val) * 1000 <= round_val } - - // pub fn is_common_round_value(&self) -> bool { - // const ROUND_SATS: [u64; 19] = [ - // 1_000, // 1k sats - // 10_000, // 10k sats - // 20_000, // 20k sats - // 30_000, // 30k sats - // 50_000, // 50k sats - // 100_000, // 100k sats (0.001 BTC) - // 200_000, // 200k sats - // 300_000, // 300k sats - // 500_000, // 500k sats - // 1_000_000, // 0.01 BTC - // 2_000_000, // 0.02 BTC - // 3_000_000, // 0.03 BTC - // 5_000_000, // 0.05 BTC - // 10_000_000, // 0.1 BTC - // 20_000_000, // 0.2 BTC - // 30_000_000, // 0.3 BTC - // 50_000_000, // 0.5 BTC - // 100_000_000, // 1 BTC - // 1_000_000_000, // 10 BTC - // ]; - // const TOLERANCE: f64 = 0.001; // 0.1% - // - // let v = self.0 as f64; - // ROUND_SATS - // .iter() - // .any(|&r| (v - r as f64).abs() <= r as f64 * TOLERANCE) - // } } impl Add for Sats { diff --git a/website/scripts/main.js b/website/scripts/main.js index 6be9df045..2a445151f 100644 --- a/website/scripts/main.js +++ b/website/scripts/main.js @@ -9,6 +9,7 @@ import { } from "./panes/chart.js"; import { initSearch } from "./panes/search.js"; import { replaceHistory } from "./utils/url.js"; +import { idle } from "./utils/timing.js"; import { removeStored, writeToStorage } from "./utils/storage.js"; import { asideElement, @@ -194,7 +195,7 @@ function initSelected() { } initSelected(); -requestIdleCallback(() => options.setParent(navElement)); +idle(() => options.setParent(navElement)); onFirstIntersection(navElement, () => { options.setParent(navElement); diff --git a/website/scripts/options/full.js b/website/scripts/options/full.js index fae11f490..917451eba 100644 --- a/website/scripts/options/full.js +++ b/website/scripts/options/full.js @@ -389,10 +389,6 @@ export function initOptions() { const onSelectedPath = isOnSelectedPath(node.path); - if (onSelectedPath) { - li.dataset.highlight = ""; - } - if (node.type === "group") { const details = window.document.createElement("details"); details.dataset.name = node.serName; @@ -439,6 +435,7 @@ export function initOptions() { if (parentEl) return; parentEl = el; buildTreeDOM(processedTree, el); + updateHighlight(selected.value); } if (!selected.value) { diff --git a/website/scripts/utils/timing.js b/website/scripts/utils/timing.js index 574183af5..48ffeaf9b 100644 --- a/website/scripts/utils/timing.js +++ b/website/scripts/utils/timing.js @@ -11,6 +11,15 @@ export function next() { return sleep(0); } +/** + * @param {() => void} callback + */ +export function idle(callback) { + ("requestIdleCallback" in window ? requestIdleCallback : setTimeout)( + callback, + ); +} + /** * * @template {(...args: any[]) => any} F