mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snapshot
This commit is contained in:
@@ -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
|
||||
|
||||
207
crates/brk_oracle/examples/determinism.rs
Normal file
207
crates/brk_oracle/examples/determinism.rs
Normal file
@@ -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<Oracle>,
|
||||
converged_at: Option<usize>,
|
||||
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<TestRun> = (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!");
|
||||
}
|
||||
@@ -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<f64>,
|
||||
cursor: usize,
|
||||
filled: usize,
|
||||
ref_bin: f64,
|
||||
config: Config,
|
||||
weights: Vec<f64>,
|
||||
excluded_mask: u16,
|
||||
warmup: bool,
|
||||
}
|
||||
|
||||
impl Oracle {
|
||||
pub fn new(start_bin: f64, config: Config) -> Self {
|
||||
let weights: Vec<f64> = (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<f64> = (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<usize> {
|
||||
self.eligible_bin(sats, output_type)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn eligible_bin(&self, sats: Sats, output_type: OutputType) -> Option<usize> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<AppState> {
|
||||
.api_route(
|
||||
"/api/address/{address}",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<AddressParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/address/{address}/txs",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<AddressParam>,
|
||||
Query(params): Query<AddressTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/address/{address}/utxo",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<AddressParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/address/{address}/txs/mempool",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<AddressParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/address/{address}/txs/chain",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<AddressParam>,
|
||||
Query(params): Query<AddressTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/validate-address/{address}",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<ValidateAddressParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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()
|
||||
|
||||
@@ -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<AppState> {
|
||||
self.api_route(
|
||||
"/api/blocks",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/block/{hash}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/block/{hash}/status",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/block-height/{height}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/blocks/{height}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/block/{hash}/txids",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/block/{hash}/txs/{start_index}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashStartIndex>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/block/{hash}/txid/{index}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashTxIndex>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/timestamp/{timestamp}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<TimestampParam>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/block/{hash}/raw",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
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")
|
||||
|
||||
@@ -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<AppState> {
|
||||
.api_route(
|
||||
"/api/mempool/info",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), |q| q.mempool_info()).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/mempool/txids",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), |q| q.mempool_txids()).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/fees/recommended",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), |q| q.recommended_fees()).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/mempool/price",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state
|
||||
.server_cached_json(&headers, state.mempool_cache(), "price", |q| {
|
||||
q.live_price()
|
||||
})
|
||||
.await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/fees/mempool-blocks",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), |q| q.mempool_blocks()).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_blocks()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_mempool_blocks")
|
||||
|
||||
@@ -48,8 +48,8 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
self.api_route(
|
||||
"/api/metrics",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metrics_catalog().clone())).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
"/api/metrics/count",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/metrics/indexes",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/metrics/list",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Query(pagination): Query<Pagination>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/metrics/search/{metric}",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<MetricParam>,
|
||||
Query(query): Query<LimitParam>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/metric/{metric}",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<MetricParam>
|
||||
| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/metrics/cost-basis",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/metrics/cost-basis/{cohort}/dates",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(params): Path<CostBasisCohortParam>,
|
||||
State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/metrics/cost-basis/{cohort}/{date}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(params): Path<CostBasisParams>,
|
||||
Query(query): Query<CostBasisQuery>,
|
||||
State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Static, move |q| {
|
||||
.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
|
||||
q.cost_basis_formatted(
|
||||
¶ms.cohort,
|
||||
params.date,
|
||||
|
||||
@@ -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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/difficulty-adjustment",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustment()).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/pools",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
// 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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/pools/{time_period}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.mining_pools(path.time_period)).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/pool/{slug}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.pool_detail(path.slug)).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/hashrate",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, |q| q.hashrate(None)).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/hashrate/{time_period}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.hashrate(Some(path.time_period))).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/difficulty-adjustments",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustments(None)).await
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/difficulty-adjustments/{time_period}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.difficulty_adjustments(Some(path.time_period))).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/fees/{time_period}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_fees(path.time_period)).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/rewards/{time_period}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_rewards(path.time_period)).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/sizes-weights/{time_period}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_sizes_weights(path.time_period)).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/reward-stats/{block_count}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<BlockCountParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, move |q| q.reward_stats(path.block_count)).await
|
||||
async |uri: Uri, headers: HeaderMap, Path(path): Path<BlockCountParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.reward_stats(path.block_count)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_reward_stats")
|
||||
|
||||
@@ -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<AppState> {
|
||||
self.api_route(
|
||||
"/api/server/sync",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/api/server/disk",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
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<AppState> {
|
||||
.api_route(
|
||||
"/version",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Static, |_| {
|
||||
.cached_json(&headers, CacheStrategy::Static, &uri, |_| {
|
||||
Ok(env!("CARGO_PKG_VERSION"))
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -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<AppState> {
|
||||
"/api/tx/{txid}",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(txid): Path<TxidParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/tx/{txid}/status",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(txid): Path<TxidParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/tx/{txid}/hex",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(txid): Path<TxidParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/tx/{txid}/outspend/{vout}",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<TxidVout>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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<AppState> {
|
||||
"/api/tx/{txid}/outspends",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(txid): Path<TxidParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
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")
|
||||
|
||||
@@ -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<u8>, etag: &str) -> Self;
|
||||
fn new_bytes_with(status: StatusCode, value: Vec<u8>, etag: &str) -> Self;
|
||||
fn new_bytes_cached(value: Vec<u8>, params: &CacheParams) -> Self;
|
||||
}
|
||||
|
||||
impl ResponseExtended for Response<Body> {
|
||||
@@ -114,26 +112,4 @@ impl ResponseExtended for Response<Body> {
|
||||
}
|
||||
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<u8>, 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T, F>(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
strategy: CacheStrategy,
|
||||
f: F,
|
||||
) -> Response<Body>
|
||||
where
|
||||
T: Serialize + Send + 'static,
|
||||
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + 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::<T>::to_json_response(Err(e), params.etag_str()),
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON response with HTTP caching + server-side cache
|
||||
pub async fn server_cached_json<T, F>(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
strategy: CacheStrategy,
|
||||
cache_prefix: &str,
|
||||
uri: &Uri,
|
||||
f: F,
|
||||
) -> Response<Body>
|
||||
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<T, F>(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
strategy: CacheStrategy,
|
||||
uri: &Uri,
|
||||
f: F,
|
||||
) -> Response<Body>
|
||||
where
|
||||
T: AsRef<str> + Send + 'static,
|
||||
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + 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::<T>::to_text_response(Err(e), params.etag_str()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary response with HTTP + server-side caching
|
||||
pub async fn cached_bytes<T, F>(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
strategy: CacheStrategy,
|
||||
uri: &Uri,
|
||||
f: F,
|
||||
) -> Response<Body>
|
||||
where
|
||||
T: Into<Vec<u8>> + Send + 'static,
|
||||
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + 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::<T>::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<Output = brk_error::Result<Bytes>>,
|
||||
compute: impl Future<Output = brk_error::Result<Bytes>>,
|
||||
) -> brk_error::Result<Bytes> {
|
||||
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<T, F>(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
strategy: CacheStrategy,
|
||||
f: F,
|
||||
) -> Response<Body>
|
||||
where
|
||||
T: AsRef<str> + Send + 'static,
|
||||
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + 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::<T>::to_text_response(Err(e), params.etag_str()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary response with caching
|
||||
pub async fn cached_bytes<T, F>(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
strategy: CacheStrategy,
|
||||
f: F,
|
||||
) -> Response<Body>
|
||||
where
|
||||
T: Into<Vec<u8>> + Send + 'static,
|
||||
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + 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::<T>::to_bytes_response(Err(e), params.etag_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user