mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
website: snap
This commit is contained in:
@@ -8198,7 +8198,7 @@ pub struct BrkClient {
|
||||
|
||||
impl BrkClient {
|
||||
/// Client version.
|
||||
pub const VERSION: &'static str = "v0.3.0-alpha.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<String>) -> Self {
|
||||
|
||||
@@ -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<Option<Entry>>,
|
||||
prefix_to_idx: FxHashMap<TxidPrefix, TxIndex>,
|
||||
parent_to_children: FxHashMap<TxidPrefix, SmallVec<[TxidPrefix; 2]>>,
|
||||
free_slots: Vec<TxIndex>,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Vec<u8>> {
|
||||
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<TxOutspend> {
|
||||
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<Vec<TxOutspend>> {
|
||||
// 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<TxOutspend> {
|
||||
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<Transaction> {
|
||||
|
||||
@@ -106,6 +106,7 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
.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::<Vec<Txid>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
|
||||
@@ -149,7 +149,7 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
.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::<Vec<u8>>()
|
||||
.binary_response()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
|
||||
@@ -29,6 +29,7 @@ impl FeesRoutes for ApiRouter<AppState> {
|
||||
.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::<Vec<MempoolBlock>>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -49,6 +50,7 @@ impl FeesRoutes for ApiRouter<AppState> {
|
||||
.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::<RecommendedFees>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -69,6 +71,7 @@ impl FeesRoutes for ApiRouter<AppState> {
|
||||
.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::<RecommendedFees>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -55,6 +55,7 @@ impl GeneralRoutes for ApiRouter<AppState> {
|
||||
.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::<Prices>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -27,6 +27,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
|
||||
.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::<MempoolInfo>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -45,6 +46,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
|
||||
.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::<Vec<Txid>>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -63,6 +65,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
|
||||
.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::<Vec<MempoolRecentTx>>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -85,6 +88,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
|
||||
plus mempool.",
|
||||
)
|
||||
.json_response::<Dollars>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -299,6 +299,7 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
.summary("Block fee rates")
|
||||
.description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
|
||||
.json_response::<Vec<BlockFeeRatesEntry>>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -34,6 +34,7 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
.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::<CpfpInfo>()
|
||||
.not_modified()
|
||||
.not_found()
|
||||
.server_error(),
|
||||
),
|
||||
@@ -203,7 +204,7 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
.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::<Vec<u8>>()
|
||||
.binary_response()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
@@ -248,6 +249,7 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
.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::<Vec<u64>>()
|
||||
.not_modified()
|
||||
.server_error(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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<AppState> {
|
||||
"Returns the single most recent value for a series, unwrapped (not inside a SeriesData object)."
|
||||
)
|
||||
.json_response::<serde_json::Value>()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
@@ -284,6 +284,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
|
||||
.summary("Get series data length")
|
||||
.description("Returns the total number of data points for a series at the given index.")
|
||||
.json_response::<usize>()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
@@ -306,6 +307,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
|
||||
.summary("Get series version")
|
||||
.description("Returns the current version of a series. Changes when the series data is updated.")
|
||||
.json_response::<brk_types::Version>()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
@@ -343,6 +345,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
|
||||
.summary("Available cost basis cohorts")
|
||||
.description("List available cohorts for cost basis distribution.")
|
||||
.json_response::<Vec<String>>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -366,6 +369,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
|
||||
.summary("Available cost basis dates")
|
||||
.description("List available dates for a cohort's cost basis distribution.")
|
||||
.json_response::<Vec<Date>>()
|
||||
.not_modified()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
@@ -401,6 +405,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
|
||||
- `value`: supply (default, in BTC), realized (USD), unrealized (USD)",
|
||||
)
|
||||
.json_response::<CostBasisFormatted>()
|
||||
.not_modified()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
|
||||
@@ -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<String>,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl ResponseExtended for Response<Body> {
|
||||
|
||||
fn new_not_modified_with(params: &CacheParams) -> Response<Body> {
|
||||
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<T>(value: T, params: &CacheParams) -> Self
|
||||
@@ -51,7 +51,7 @@ impl ResponseExtended for Response<Body> {
|
||||
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<Body> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<u8>, _>(|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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user