From e91f1386b1ba941bf977fff81e04423e66be7839 Mon Sep 17 00:00:00 2001 From: nym21 Date: Mon, 6 Apr 2026 22:30:02 +0200 Subject: [PATCH] website: snap --- crates/brk_client/src/lib.rs | 2 +- crates/brk_mempool/src/entry_pool.rs | 28 + crates/brk_query/src/impl/mempool.rs | 5 +- crates/brk_query/src/impl/mining/pools.rs | 13 +- crates/brk_query/src/impl/tx.rs | 156 +++-- .../brk_server/src/api/mempool_space/addrs.rs | 1 + .../src/api/mempool_space/blocks.rs | 2 +- .../brk_server/src/api/mempool_space/fees.rs | 3 + .../src/api/mempool_space/general.rs | 1 + .../src/api/mempool_space/mempool.rs | 4 + .../src/api/mempool_space/mining.rs | 1 + .../src/api/mempool_space/transactions.rs | 4 +- crates/brk_server/src/api/series/mod.rs | 9 +- crates/brk_server/src/cache.rs | 60 +- crates/brk_server/src/extended/response.rs | 6 +- .../src/extended/transform_operation.rs | 6 + crates/brk_server/src/state.rs | 2 +- modules/brk-client/index.js | 2 +- modules/brk-client/package.json | 2 +- packages/brk_client/brk_client/__init__.py | 2 +- packages/brk_client/pyproject.toml | 2 +- website/index.html | 3 +- website/scripts/_types.js | 3 +- website/scripts/main.js | 75 +- website/scripts/options/full.js | 48 +- website/scripts/options/partial.js | 14 +- website/scripts/options/types.js | 20 +- website/scripts/panes/_table.js | 433 ------------ website/scripts/panes/chart.js | 33 +- website/scripts/panes/explorer.js | 662 +++++++++++------- website/scripts/utils/elements.js | 1 + website/scripts/utils/format.js | 11 +- website/styles/elements.css | 16 +- website/styles/main.css | 6 +- website/styles/panes/explorer.css | 131 ++++ 35 files changed, 872 insertions(+), 895 deletions(-) delete mode 100644 website/scripts/panes/_table.js diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index c1d85c89f..d56746aa1 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -8198,7 +8198,7 @@ pub struct BrkClient { impl BrkClient { /// Client version. - pub const VERSION: &'static str = "v0.3.0-alpha.5"; + pub const VERSION: &'static str = "v0.3.0-alpha.6"; /// Create a new client with the given base URL. pub fn new(base_url: impl Into) -> Self { diff --git a/crates/brk_mempool/src/entry_pool.rs b/crates/brk_mempool/src/entry_pool.rs index 2b4ca84ea..df00c8e2a 100644 --- a/crates/brk_mempool/src/entry_pool.rs +++ b/crates/brk_mempool/src/entry_pool.rs @@ -1,5 +1,6 @@ use brk_types::TxidPrefix; use rustc_hash::FxHashMap; +use smallvec::SmallVec; use crate::{entry::Entry, types::TxIndex}; @@ -11,12 +12,20 @@ use crate::{entry::Entry, types::TxIndex}; pub struct EntryPool { entries: Vec>, prefix_to_idx: FxHashMap, + parent_to_children: FxHashMap>, free_slots: Vec, } impl EntryPool { /// Insert an entry, returning its index. pub fn insert(&mut self, prefix: TxidPrefix, entry: Entry) -> TxIndex { + for parent in &entry.depends { + self.parent_to_children + .entry(*parent) + .or_default() + .push(prefix); + } + let idx = match self.free_slots.pop() { Some(idx) => { self.entries[idx.as_usize()] = Some(entry); @@ -39,9 +48,28 @@ impl EntryPool { self.entries.get(idx.as_usize())?.as_ref() } + /// Get direct children of a transaction (txs that depend on it). + pub fn children(&self, prefix: &TxidPrefix) -> &[TxidPrefix] { + self.parent_to_children + .get(prefix) + .map(SmallVec::as_slice) + .unwrap_or_default() + } + /// Remove an entry by its txid prefix. pub fn remove(&mut self, prefix: &TxidPrefix) { if let Some(idx) = self.prefix_to_idx.remove(prefix) { + if let Some(entry) = self.entries.get(idx.as_usize()).and_then(|e| e.as_ref()) { + for parent in &entry.depends { + if let Some(children) = self.parent_to_children.get_mut(parent) { + children.retain(|c| c != prefix); + if children.is_empty() { + self.parent_to_children.remove(parent); + } + } + } + } + self.parent_to_children.remove(prefix); if let Some(slot) = self.entries.get_mut(idx.as_usize()) { *slot = None; } diff --git a/crates/brk_query/src/impl/mempool.rs b/crates/brk_query/src/impl/mempool.rs index 0d60ca64f..8f6bc8193 100644 --- a/crates/brk_query/src/impl/mempool.rs +++ b/crates/brk_query/src/impl/mempool.rs @@ -74,10 +74,9 @@ impl Query { } } - // Descendants: find entries that depend on this tx's prefix let mut descendants = Vec::new(); - for e in entries.entries().iter().flatten() { - if e.depends.contains(&prefix) { + for child_prefix in entries.children(&prefix) { + if let Some(e) = entries.get(child_prefix) { descendants.push(CpfpEntry { txid: e.txid.clone(), weight: Weight::from(e.vsize), diff --git a/crates/brk_query/src/impl/mining/pools.rs b/crates/brk_query/src/impl/mining/pools.rs index 7a1255944..a334dcb02 100644 --- a/crates/brk_query/src/impl/mining/pools.rs +++ b/crates/brk_query/src/impl/mining/pools.rs @@ -292,7 +292,6 @@ impl Query { let max_height = self.height().to_usize(); let start = start_height.map(|h| h.to_usize()).unwrap_or(max_height); - // BytesVec reader gives O(1) mmap reads — efficient for backward scan let reader = computer.pools.pool.reader(); let end = start.min(reader.len().saturating_sub(1)); @@ -307,12 +306,20 @@ impl Query { } } + // Group consecutive descending heights into ranges for batch reads let mut blocks = Vec::with_capacity(heights.len()); - for h in heights { - if let Ok(mut v) = self.blocks_v1_range(h, h + 1) { + let mut i = 0; + while i < heights.len() { + let hi = heights[i]; + while i + 1 < heights.len() && heights[i + 1] + 1 == heights[i] { + i += 1; + } + if let Ok(mut v) = self.blocks_v1_range(heights[i], hi + 1) { blocks.append(&mut v); } + i += 1; } + Ok(blocks) } diff --git a/crates/brk_query/src/impl/tx.rs b/crates/brk_query/src/impl/tx.rs index 695e0ab12..2ae14f336 100644 --- a/crates/brk_query/src/impl/tx.rs +++ b/crates/brk_query/src/impl/tx.rs @@ -1,8 +1,8 @@ -use bitcoin::hex::DisplayHex; +use bitcoin::hex::{DisplayHex, FromHex}; use brk_error::{Error, Result}; use brk_types::{ - BlockHash, Height, MerkleProof, Timestamp, Transaction, TxInIndex, TxIndex, TxOutspend, - TxStatus, Txid, TxidPrefix, Vin, Vout, + BlockHash, Height, MerkleProof, Timestamp, Transaction, TxInIndex, TxIndex, TxOutIndex, + TxOutspend, TxStatus, Txid, TxidPrefix, Vin, Vout, }; use vecdb::{ReadableVec, VecIndex}; @@ -71,6 +71,13 @@ impl Query { } pub fn transaction_raw(&self, txid: &Txid) -> Result> { + if let Some(mempool) = self.mempool() + && let Some(tx_with_hex) = mempool.get_txs().get(txid) + { + return Vec::from_hex(tx_with_hex.hex()) + .map_err(|_| Error::Parse("Failed to decode mempool tx hex".into())); + } + let prefix = TxidPrefix::from(txid); let indexer = self.indexer(); let Ok(Some(tx_index)) = indexer @@ -108,65 +115,40 @@ impl Query { } pub fn outspend(&self, txid: &Txid, vout: Vout) -> Result { - let all = self.outspends(txid)?; - Ok(all - .into_iter() - .nth(usize::from(vout)) - .unwrap_or(TxOutspend::UNSPENT)) + if self.mempool().is_some_and(|m| m.get_txs().contains_key(txid)) { + return Ok(TxOutspend::UNSPENT); + } + let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?; + if usize::from(vout) >= output_count { + return Ok(TxOutspend::UNSPENT); + } + self.resolve_outspend(first_txout + vout) } pub fn outspends(&self, txid: &Txid) -> Result> { - // Mempool outputs are unspent in on-chain terms if let Some(mempool) = self.mempool() && let Some(tx_with_hex) = mempool.get_txs().get(txid) { - let output_count = tx_with_hex.tx().output.len(); - return Ok(vec![TxOutspend::UNSPENT; output_count]); + return Ok(vec![TxOutspend::UNSPENT; tx_with_hex.tx().output.len()]); } + let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?; - // Look up confirmed transaction - let prefix = TxidPrefix::from(txid); let indexer = self.indexer(); - let Ok(Some(tx_index)) = indexer - .stores - .txid_prefix_to_tx_index - .get(&prefix) - .map(|opt| opt.map(|cow| cow.into_owned())) - else { - return Err(Error::UnknownTxid); - }; - - // Get output range - let first_txout_index = indexer - .vecs - .transactions - .first_txout_index - .read_once(tx_index)?; - let next_first_txout_index = indexer - .vecs - .transactions - .first_txout_index - .read_once(tx_index.incremented())?; - let output_count = usize::from(next_first_txout_index) - usize::from(first_txout_index); - - // Get spend status for each output - let computer = self.computer(); - let txin_index_reader = computer.outputs.spent.txin_index.reader(); + let txin_index_reader = self.computer().outputs.spent.txin_index.reader(); let txid_reader = indexer.vecs.transactions.txid.reader(); - // Cursors for PcoVec reads — buffer chunks so nearby indices share decompression + // Cursors buffer chunks so nearby indices share decompression let mut input_tx_cursor = indexer.vecs.inputs.tx_index.cursor(); let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor(); let mut height_cursor = indexer.vecs.transactions.height.cursor(); let mut block_ts_cursor = indexer.vecs.blocks.timestamp.cursor(); - // Block info cache — spending txs in the same block share block hash/time + // Spending txs in the same block share block hash/time let mut cached_block: Option<(Height, BlockHash, Timestamp)> = None; let mut outspends = Vec::with_capacity(output_count); for i in 0..output_count { - let txout_index = first_txout_index + Vout::from(i); - let txin_index = txin_index_reader.get(usize::from(txout_index)); + let txin_index = txin_index_reader.get(usize::from(first_txout + Vout::from(i))); if txin_index == TxInIndex::UNSPENT { outspends.push(TxOutspend::UNSPENT); @@ -174,9 +156,9 @@ impl Query { } let spending_tx_index = input_tx_cursor.get(usize::from(txin_index)).unwrap(); - let spending_first_txin_index = + let spending_first_txin = first_txin_cursor.get(spending_tx_index.to_usize()).unwrap(); - let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin_index)); + let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin)); let spending_txid = txid_reader.get(spending_tx_index.to_usize()); let spending_height = height_cursor.get(spending_tx_index.to_usize()).unwrap(); @@ -207,6 +189,92 @@ impl Query { Ok(outspends) } + /// Resolve txid to (tx_index, first_txout_index, output_count). + fn resolve_tx_outputs(&self, txid: &Txid) -> Result<(TxIndex, TxOutIndex, usize)> { + let prefix = TxidPrefix::from(txid); + let indexer = self.indexer(); + let tx_index: TxIndex = indexer + .stores + .txid_prefix_to_tx_index + .get(&prefix)? + .map(|cow| cow.into_owned()) + .ok_or(Error::UnknownTxid)?; + let first = indexer + .vecs + .transactions + .first_txout_index + .read_once(tx_index)?; + let next = indexer + .vecs + .transactions + .first_txout_index + .read_once(tx_index.incremented())?; + Ok((tx_index, first, usize::from(next) - usize::from(first))) + } + + /// Resolve spend status for a single output. + fn resolve_outspend(&self, txout_index: TxOutIndex) -> Result { + let indexer = self.indexer(); + let txin_index = self + .computer() + .outputs + .spent + .txin_index + .reader() + .get(usize::from(txout_index)); + + if txin_index == TxInIndex::UNSPENT { + return Ok(TxOutspend::UNSPENT); + } + + let spending_tx_index = indexer + .vecs + .inputs + .tx_index + .collect_one_at(usize::from(txin_index)) + .unwrap(); + let spending_first_txin = indexer + .vecs + .transactions + .first_txin_index + .collect_one(spending_tx_index) + .unwrap(); + let spending_height = indexer + .vecs + .transactions + .height + .collect_one(spending_tx_index) + .unwrap(); + + Ok(TxOutspend { + spent: true, + txid: Some( + indexer + .vecs + .transactions + .txid + .reader() + .get(spending_tx_index.to_usize()), + ), + vin: Some(Vin::from( + usize::from(txin_index) - usize::from(spending_first_txin), + )), + status: Some(TxStatus { + confirmed: true, + block_height: Some(spending_height), + block_hash: Some(indexer.vecs.blocks.blockhash.read_once(spending_height)?), + block_time: Some( + indexer + .vecs + .blocks + .timestamp + .collect_one(spending_height) + .unwrap(), + ), + }), + }) + } + // === Helper methods === pub fn transaction_by_index(&self, tx_index: TxIndex) -> Result { diff --git a/crates/brk_server/src/api/mempool_space/addrs.rs b/crates/brk_server/src/api/mempool_space/addrs.rs index 499ae5f18..341e86778 100644 --- a/crates/brk_server/src/api/mempool_space/addrs.rs +++ b/crates/brk_server/src/api/mempool_space/addrs.rs @@ -106,6 +106,7 @@ impl AddrRoutes for ApiRouter { .summary("Address mempool transactions") .description("Get unconfirmed transaction IDs for an address from the mempool (up to 50).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-mempool)*") .json_response::>() + .not_modified() .bad_request() .not_found() .server_error() diff --git a/crates/brk_server/src/api/mempool_space/blocks.rs b/crates/brk_server/src/api/mempool_space/blocks.rs index 193eab65e..f8e1379ad 100644 --- a/crates/brk_server/src/api/mempool_space/blocks.rs +++ b/crates/brk_server/src/api/mempool_space/blocks.rs @@ -149,7 +149,7 @@ impl BlockRoutes for ApiRouter { .description( "Returns the raw block data in binary format.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-raw)*", ) - .json_response::>() + .binary_response() .not_modified() .bad_request() .not_found() diff --git a/crates/brk_server/src/api/mempool_space/fees.rs b/crates/brk_server/src/api/mempool_space/fees.rs index 753db7546..92be703e0 100644 --- a/crates/brk_server/src/api/mempool_space/fees.rs +++ b/crates/brk_server/src/api/mempool_space/fees.rs @@ -29,6 +29,7 @@ impl FeesRoutes for ApiRouter { .summary("Projected mempool blocks") .description("Get projected blocks from the mempool for fee estimation.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*") .json_response::>() + .not_modified() .server_error() }, ), @@ -49,6 +50,7 @@ impl FeesRoutes for ApiRouter { .summary("Recommended fees") .description("Get recommended fee rates for different confirmation targets.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*") .json_response::() + .not_modified() .server_error() }, ), @@ -69,6 +71,7 @@ impl FeesRoutes for ApiRouter { .summary("Precise recommended fees") .description("Get recommended fee rates with up to 3 decimal places.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*") .json_response::() + .not_modified() .server_error() }, ), diff --git a/crates/brk_server/src/api/mempool_space/general.rs b/crates/brk_server/src/api/mempool_space/general.rs index 30fea0901..24c3d7403 100644 --- a/crates/brk_server/src/api/mempool_space/general.rs +++ b/crates/brk_server/src/api/mempool_space/general.rs @@ -55,6 +55,7 @@ impl GeneralRoutes for ApiRouter { .summary("Current BTC price") .description("Returns bitcoin latest price (on-chain derived, USD only).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)*") .json_response::() + .not_modified() .server_error() }, ), diff --git a/crates/brk_server/src/api/mempool_space/mempool.rs b/crates/brk_server/src/api/mempool_space/mempool.rs index 73ddacb4d..352f9725e 100644 --- a/crates/brk_server/src/api/mempool_space/mempool.rs +++ b/crates/brk_server/src/api/mempool_space/mempool.rs @@ -27,6 +27,7 @@ impl MempoolRoutes for ApiRouter { .summary("Mempool statistics") .description("Get current mempool statistics including transaction count, total vsize, total fees, and fee histogram.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool)*") .json_response::() + .not_modified() .server_error() }, ), @@ -45,6 +46,7 @@ impl MempoolRoutes for ApiRouter { .summary("Mempool transaction IDs") .description("Get all transaction IDs currently in the mempool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-transaction-ids)*") .json_response::>() + .not_modified() .server_error() }, ), @@ -63,6 +65,7 @@ impl MempoolRoutes for ApiRouter { .summary("Recent mempool transactions") .description("Get the last 10 transactions to enter the mempool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-recent)*") .json_response::>() + .not_modified() .server_error() }, ), @@ -85,6 +88,7 @@ impl MempoolRoutes for ApiRouter { plus mempool.", ) .json_response::() + .not_modified() .server_error() }, ), diff --git a/crates/brk_server/src/api/mempool_space/mining.rs b/crates/brk_server/src/api/mempool_space/mining.rs index 2c8fc2869..17d83d7a8 100644 --- a/crates/brk_server/src/api/mempool_space/mining.rs +++ b/crates/brk_server/src/api/mempool_space/mining.rs @@ -299,6 +299,7 @@ impl MiningRoutes for ApiRouter { .summary("Block fee rates") .description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*") .json_response::>() + .not_modified() .server_error() }, ), diff --git a/crates/brk_server/src/api/mempool_space/transactions.rs b/crates/brk_server/src/api/mempool_space/transactions.rs index 4ad215eae..afd3e20b9 100644 --- a/crates/brk_server/src/api/mempool_space/transactions.rs +++ b/crates/brk_server/src/api/mempool_space/transactions.rs @@ -34,6 +34,7 @@ impl TxRoutes for ApiRouter { .summary("CPFP info") .description("Returns ancestors and descendants for a CPFP transaction.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)*") .json_response::() + .not_modified() .not_found() .server_error(), ), @@ -203,7 +204,7 @@ impl TxRoutes for ApiRouter { .transactions_tag() .summary("Transaction raw") .description("Returns a transaction as binary data.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-raw)*") - .json_response::>() + .binary_response() .not_modified() .bad_request() .not_found() @@ -248,6 +249,7 @@ impl TxRoutes for ApiRouter { .summary("Transaction first-seen times") .description("Returns timestamps when transactions were first seen in the mempool. Returns 0 for mined or unknown transactions.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-times)*") .json_response::>() + .not_modified() .server_error(), ), ) diff --git a/crates/brk_server/src/api/series/mod.rs b/crates/brk_server/src/api/series/mod.rs index 9bae9d0eb..883a1c5d9 100644 --- a/crates/brk_server/src/api/series/mod.rs +++ b/crates/brk_server/src/api/series/mod.rs @@ -15,6 +15,7 @@ use brk_types::{ use crate::{ CacheStrategy, + cache::CACHE_CONTROL, extended::TransformResponseExtended, params::{CostBasisCohortParam, CostBasisParams, CostBasisQuery, SeriesParam}, }; @@ -29,8 +30,6 @@ pub mod legacy; const MAX_WEIGHT: usize = 4 * 8 * 10_000; /// Maximum allowed request weight for localhost (50MB) const MAX_WEIGHT_LOCALHOST: usize = 50 * 1_000_000; -/// Cache control header for series data responses -const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate"; /// Returns the max weight for a request based on the client address. /// Localhost requests get a generous limit, external requests get a stricter one. @@ -262,6 +261,7 @@ impl ApiSeriesRoutes for ApiRouter { "Returns the single most recent value for a series, unwrapped (not inside a SeriesData object)." ) .json_response::() + .not_modified() .not_found(), ), ) @@ -284,6 +284,7 @@ impl ApiSeriesRoutes for ApiRouter { .summary("Get series data length") .description("Returns the total number of data points for a series at the given index.") .json_response::() + .not_modified() .not_found(), ), ) @@ -306,6 +307,7 @@ impl ApiSeriesRoutes for ApiRouter { .summary("Get series version") .description("Returns the current version of a series. Changes when the series data is updated.") .json_response::() + .not_modified() .not_found(), ), ) @@ -343,6 +345,7 @@ impl ApiSeriesRoutes for ApiRouter { .summary("Available cost basis cohorts") .description("List available cohorts for cost basis distribution.") .json_response::>() + .not_modified() .server_error() }, ), @@ -366,6 +369,7 @@ impl ApiSeriesRoutes for ApiRouter { .summary("Available cost basis dates") .description("List available dates for a cohort's cost basis distribution.") .json_response::>() + .not_modified() .not_found() .server_error() }, @@ -401,6 +405,7 @@ impl ApiSeriesRoutes for ApiRouter { - `value`: supply (default, in BTC), realized (USD), unrealized (USD)", ) .json_response::() + .not_modified() .not_found() .server_error() }, diff --git a/crates/brk_server/src/cache.rs b/crates/brk_server/src/cache.rs index c4b96323b..835f1309d 100644 --- a/crates/brk_server/src/cache.rs +++ b/crates/brk_server/src/cache.rs @@ -26,25 +26,47 @@ pub enum CacheStrategy { MempoolHash(u64), } +pub(crate) const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate"; + /// Resolved cache parameters pub struct CacheParams { pub etag: Option, - pub cache_control: String, + pub cache_control: &'static str, } impl CacheParams { - pub fn immutable(version: Version) -> Self { + pub fn tip(tip: BlockHashPrefix) -> Self { Self { - etag: Some(format!("i{version}")), - cache_control: "public, max-age=1, must-revalidate".into(), + etag: Some(format!("t{:x}", *tip)), + cache_control: CACHE_CONTROL, + } + } + + pub fn immutable(version: Version) -> Self { + Self { + etag: Some(format!("i{version}")), + cache_control: CACHE_CONTROL, + } + } + + pub fn block_bound(version: Version, prefix: BlockHashPrefix) -> Self { + Self { + etag: Some(format!("b{version}-{:x}", *prefix)), + cache_control: CACHE_CONTROL, } } - /// Cache params using CARGO_PKG_VERSION as etag (for openapi.json etc.) pub fn static_version() -> Self { Self { etag: Some(format!("s{VERSION}")), - cache_control: "public, max-age=1, must-revalidate".into(), + cache_control: CACHE_CONTROL, + } + } + + pub fn mempool_hash(hash: u64) -> Self { + Self { + etag: Some(format!("m{hash:x}")), + cache_control: CACHE_CONTROL, } } @@ -59,28 +81,12 @@ impl CacheParams { } pub fn resolve(strategy: &CacheStrategy, tip: impl FnOnce() -> BlockHashPrefix) -> Self { - let cache_control = "public, max-age=1, must-revalidate".into(); match strategy { - CacheStrategy::Tip => Self { - etag: Some(format!("t{:x}", *tip())), - cache_control, - }, - CacheStrategy::Immutable(v) => Self { - etag: Some(format!("i{v}")), - cache_control, - }, - CacheStrategy::BlockBound(v, prefix) => Self { - etag: Some(format!("b{v}-{:x}", **prefix)), - cache_control, - }, - CacheStrategy::Static => Self { - etag: Some(format!("s{VERSION}")), - cache_control, - }, - CacheStrategy::MempoolHash(hash) => Self { - etag: Some(format!("m{hash:x}")), - cache_control, - }, + CacheStrategy::Tip => Self::tip(tip()), + CacheStrategy::Immutable(v) => Self::immutable(*v), + CacheStrategy::BlockBound(v, prefix) => Self::block_bound(*v, *prefix), + CacheStrategy::Static => Self::static_version(), + CacheStrategy::MempoolHash(hash) => Self::mempool_hash(*hash), } } } diff --git a/crates/brk_server/src/extended/response.rs b/crates/brk_server/src/extended/response.rs index 82b2635c4..a1d5bf816 100644 --- a/crates/brk_server/src/extended/response.rs +++ b/crates/brk_server/src/extended/response.rs @@ -40,7 +40,7 @@ impl ResponseExtended for Response { fn new_not_modified_with(params: &CacheParams) -> Response { let etag = Etag::from(params.etag_str()); - Self::new_not_modified(&etag, ¶ms.cache_control) + Self::new_not_modified(&etag, params.cache_control) } fn new_json_cached(value: T, params: &CacheParams) -> Self @@ -51,7 +51,7 @@ impl ResponseExtended for Response { let mut response = Response::builder().body(bytes.into()).unwrap(); let headers = response.headers_mut(); headers.insert_content_type_application_json(); - headers.insert_cache_control(¶ms.cache_control); + headers.insert_cache_control(params.cache_control); if let Some(etag) = ¶ms.etag { headers.insert_etag(etag); } @@ -83,7 +83,7 @@ impl ResponseExtended for Response { let h = response.headers_mut(); h.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); h.insert(header::CONTENT_ENCODING, content_encoding.parse().unwrap()); - h.insert_cache_control(¶ms.cache_control); + h.insert_cache_control(params.cache_control); if let Some(etag) = ¶ms.etag { h.insert_etag(etag); } diff --git a/crates/brk_server/src/extended/transform_operation.rs b/crates/brk_server/src/extended/transform_operation.rs index aa323f895..6053f3912 100644 --- a/crates/brk_server/src/extended/transform_operation.rs +++ b/crates/brk_server/src/extended/transform_operation.rs @@ -31,6 +31,8 @@ pub trait TransformResponseExtended<'t> { F: FnOnce(TransformResponse<'_, R>) -> TransformResponse<'_, R>; /// 200 with text/plain content type fn text_response(self) -> Self; + /// 200 with application/octet-stream content type + fn binary_response(self) -> Self; /// 200 with text/csv content type (adds CSV as alternative response format) fn csv_response(self) -> Self; /// 400 @@ -108,6 +110,10 @@ impl<'t> TransformResponseExtended<'t> for TransformOperation<'t> { self.response_with::<200, String, _>(|res| res.description("Successful response")) } + fn binary_response(self) -> Self { + self.response_with::<200, Vec, _>(|res| res.description("Raw binary data")) + } + fn csv_response(mut self) -> Self { // Add text/csv content type to existing 200 response if let Some(responses) = &mut self.inner_mut().responses diff --git a/crates/brk_server/src/state.rs b/crates/brk_server/src/state.rs index 3a32ed901..047366a65 100644 --- a/crates/brk_server/src/state.rs +++ b/crates/brk_server/src/state.rs @@ -165,7 +165,7 @@ impl AppState { let mut response = Response::new(Body::from(bytes)); let h = response.headers_mut(); h.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type)); - h.insert_cache_control(¶ms.cache_control); + h.insert_cache_control(params.cache_control); h.insert_content_encoding(encoding); if let Some(etag) = ¶ms.etag { h.insert_etag(etag); diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 3edaadf79..2503c5f40 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -6590,7 +6590,7 @@ function createTransferPattern(client, acc) { * @extends BrkClientBase */ class BrkClient extends BrkClientBase { - VERSION = "v0.3.0-alpha.5"; + VERSION = "v0.3.0-alpha.6"; INDEXES = /** @type {const} */ ([ "minute10", diff --git a/modules/brk-client/package.json b/modules/brk-client/package.json index f62fb909d..ed349d4e4 100644 --- a/modules/brk-client/package.json +++ b/modules/brk-client/package.json @@ -40,5 +40,5 @@ "url": "git+https://github.com/bitcoinresearchkit/brk.git" }, "type": "module", - "version": "0.3.0-alpha.5" + "version": "0.3.0-alpha.6" } diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index d95bab88b..a7a3e1ebe 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -6033,7 +6033,7 @@ class SeriesTree: class BrkClient(BrkClientBase): """Main BRK client with series tree and API methods.""" - VERSION = "v0.3.0-alpha.5" + VERSION = "v0.3.0-alpha.6" INDEXES = [ "minute10", diff --git a/packages/brk_client/pyproject.toml b/packages/brk_client/pyproject.toml index 76984d947..3be3b4f1f 100644 --- a/packages/brk_client/pyproject.toml +++ b/packages/brk_client/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "brk-client" -version = "0.3.0-alpha.5" +version = "0.3.0-alpha.6" description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index" readme = "README.md" requires-python = ">=3.9" diff --git a/website/index.html b/website/index.html index 665657c89..b8b9bdeda 100644 --- a/website/index.html +++ b/website/index.html @@ -114,7 +114,7 @@