server: snapshot

This commit is contained in:
nym21
2025-12-15 16:32:45 +01:00
parent 882a3525af
commit 825a4a77c0
100 changed files with 2677 additions and 3438 deletions
Generated
+1 -1
View File
@@ -1200,9 +1200,9 @@ dependencies = [
"brk_error",
"brk_fetcher",
"brk_indexer",
"brk_iterator",
"brk_logger",
"brk_mcp",
"brk_mempool",
"brk_query",
"brk_reader",
"brk_rpc",
+1 -1
View File
@@ -42,7 +42,7 @@ impl Vecs {
indexes: &indexes::Vecs,
price: Option<&price::Vecs>,
) -> Result<Self> {
let suffix = |s: &str| format!("{}_{s}", slug.to_string());
let suffix = |s: &str| format!("{}_{s}", slug);
let compute_dollars = price.is_some();
let version = parent_version + Version::ZERO;
+16 -11
View File
@@ -1,6 +1,6 @@
#![doc = include_str!("../README.md")]
use brk_query::{AsyncQuery, PaginatedIndexParam, PaginationParam, Params};
use brk_query::{AsyncQuery, MetricSelection, Pagination, PaginationIndex};
use brk_rmcp::{
ErrorData as McpError, RoleServer, ServerHandler,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
@@ -36,7 +36,7 @@ Get the count of unique metrics.
async fn get_metric_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: distinct_metric_count");
Ok(CallToolResult::success(vec![
Content::json(self.query.distinct_metric_count().await).unwrap(),
Content::json(self.query.sync(|q| q.distinct_metric_count())).unwrap(),
]))
}
@@ -46,7 +46,7 @@ Get the count of all metrics. (distinct metrics multiplied by the number of inde
async fn get_vec_count(&self) -> Result<CallToolResult, McpError> {
info!("mcp: total_metric_count");
Ok(CallToolResult::success(vec![
Content::json(self.query.total_metric_count().await).unwrap(),
Content::json(self.query.sync(|q| q.total_metric_count())).unwrap(),
]))
}
@@ -56,7 +56,7 @@ Get the list of all existing indexes and their accepted variants.
async fn get_indexes(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_indexes");
Ok(CallToolResult::success(vec![
Content::json(self.query.get_indexes().await).unwrap(),
Content::json(self.query.inner().get_indexes()).unwrap(),
]))
}
@@ -67,11 +67,11 @@ If the `page` param is omitted, it will default to the first page.
")]
async fn get_vecids(
&self,
Parameters(pagination): Parameters<PaginationParam>,
Parameters(pagination): Parameters<Pagination>,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_metrics");
Ok(CallToolResult::success(vec![
Content::json(self.query.get_metrics(pagination).await).unwrap(),
Content::json(self.query.sync(|q| q.get_metrics(pagination))).unwrap(),
]))
}
@@ -82,11 +82,16 @@ If the `page` param is omitted, it will default to the first page.
")]
async fn get_index_to_vecids(
&self,
Parameters(paginated_index): Parameters<PaginatedIndexParam>,
Parameters(paginated_index): Parameters<PaginationIndex>,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_index_to_vecids");
let result = self
.query
.inner()
.get_index_to_vecids(paginated_index)
.unwrap_or_default();
Ok(CallToolResult::success(vec![
Content::json(self.query.get_index_to_vecids(paginated_index).await).unwrap(),
Content::json(result).unwrap(),
]))
}
@@ -100,7 +105,7 @@ The list will be empty if the vec id isn't correct.
) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecid_to_indexes");
Ok(CallToolResult::success(vec![
Content::json(self.query.metric_to_indexes(metric).await).unwrap(),
Content::json(self.query.inner().metric_to_indexes(metric)).unwrap(),
]))
}
@@ -113,11 +118,11 @@ The response's format will depend on the given parameters, it will be:
")]
async fn get_vecs(
&self,
Parameters(params): Parameters<Params>,
Parameters(params): Parameters<MetricSelection>,
) -> Result<CallToolResult, McpError> {
info!("mcp: get_vecs");
Ok(CallToolResult::success(vec![Content::text(
match self.query.search_and_format(params).await {
match self.query.run(move |q| q.search_and_format(params)).await {
Ok(output) => output.to_string(),
Err(e) => format!("Error:\n{e}"),
},
+6 -6
View File
@@ -3,10 +3,10 @@ use std::{env, fs, path::Path};
use brk_computer::Computer;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_query::{Params, ParamsOpt, Query};
use brk_query::Query;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_types::Index;
use brk_types::{DataRangeFormat, Index, MetricSelection};
use vecdb::Exit;
pub fn main() -> Result<()> {
@@ -38,15 +38,15 @@ pub fn main() -> Result<()> {
let query = Query::build(&reader, &indexer, &computer, None);
dbg!(query.search_and_format(Params {
dbg!(query.search_and_format(MetricSelection {
index: Index::Height,
metrics: vec!["date"].into(),
rest: ParamsOpt::default().set_from(-1),
range: DataRangeFormat::default().set_from(-1),
})?);
dbg!(query.search_and_format(Params {
dbg!(query.search_and_format(MetricSelection {
index: Index::Height,
metrics: vec!["date", "timestamp"].into(),
rest: ParamsOpt::default().set_from(-10).set_count(5),
range: DataRangeFormat::default().set_from(-10).set_count(5),
})?);
Ok(())
+31 -287
View File
@@ -1,22 +1,11 @@
use std::collections::BTreeMap;
use brk_computer::Computer;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_mempool::Mempool;
use brk_reader::Reader;
use brk_types::{
Address, AddressStats, BlockInfo, BlockStatus, BlockTimestamp, DifficultyAdjustment,
HashrateSummary, Height, Index, IndexInfo, Limit, MempoolBlock, MempoolInfo, Metric,
MetricCount, PoolDetail, PoolInfo, PoolSlug, PoolsSummary, RecommendedFees, TimePeriod,
Timestamp, Transaction, TreeNode, TxOutspend, TxStatus, Txid, TxidPath, Utxo, Vout,
};
use tokio::task::spawn_blocking;
use crate::{
Output, PaginatedIndexParam, PaginatedMetrics, PaginationParam, Params, Query,
vecs::{IndexToVec, MetricToVec, Vecs},
};
use crate::Query;
#[derive(Clone)]
pub struct AsyncQuery(Query);
@@ -31,282 +20,37 @@ impl AsyncQuery {
Self(Query::build(reader, indexer, computer, mempool))
}
/// Run a blocking query operation on a spawn_blocking thread.
/// Use this for I/O-heavy or CPU-intensive operations.
///
/// # Example
/// ```ignore
/// let address_stats = query.run(move |q| q.address(address)).await?;
/// ```
pub async fn run<F, T>(&self, f: F) -> Result<T>
where
F: FnOnce(&Query) -> Result<T> + Send + 'static,
T: Send + 'static,
{
let query = self.0.clone();
spawn_blocking(move || f(&query)).await?
}
/// Run a cheap sync operation directly without spawn_blocking.
/// Use this for simple accessors that don't do I/O.
///
/// # Example
/// ```ignore
/// let height = query.sync(|q| q.height());
/// ```
pub fn sync<F, T>(&self, f: F) -> T
where
F: FnOnce(&Query) -> T,
{
f(&self.0)
}
pub fn inner(&self) -> &Query {
&self.0
}
pub async fn get_height(&self) -> Height {
self.0.get_height()
}
pub async fn get_address(&self, address: Address) -> Result<AddressStats> {
let query = self.0.clone();
spawn_blocking(move || query.get_address(address)).await?
}
pub async fn get_address_txids(
&self,
address: Address,
after_txid: Option<Txid>,
limit: usize,
) -> Result<Vec<Txid>> {
let query = self.0.clone();
spawn_blocking(move || query.get_address_txids(address, after_txid, limit)).await?
}
pub async fn get_address_utxos(&self, address: Address) -> Result<Vec<Utxo>> {
let query = self.0.clone();
spawn_blocking(move || query.get_address_utxos(address)).await?
}
pub async fn get_address_mempool_txids(&self, address: Address) -> Result<Vec<Txid>> {
let query = self.0.clone();
spawn_blocking(move || query.get_address_mempool_txids(address)).await?
}
pub async fn get_transaction(&self, txid: TxidPath) -> Result<Transaction> {
let query = self.0.clone();
spawn_blocking(move || query.get_transaction(txid)).await?
}
pub async fn get_transaction_status(&self, txid: TxidPath) -> Result<TxStatus> {
let query = self.0.clone();
spawn_blocking(move || query.get_transaction_status(txid)).await?
}
pub async fn get_transaction_hex(&self, txid: TxidPath) -> Result<String> {
let query = self.0.clone();
spawn_blocking(move || query.get_transaction_hex(txid)).await?
}
pub async fn get_tx_outspend(&self, txid: TxidPath, vout: Vout) -> Result<TxOutspend> {
let query = self.0.clone();
spawn_blocking(move || query.get_tx_outspend(txid, vout)).await?
}
pub async fn get_tx_outspends(&self, txid: TxidPath) -> Result<Vec<TxOutspend>> {
let query = self.0.clone();
spawn_blocking(move || query.get_tx_outspends(txid)).await?
}
pub async fn get_block(&self, hash: String) -> Result<BlockInfo> {
let query = self.0.clone();
spawn_blocking(move || query.get_block(&hash)).await?
}
pub async fn get_block_by_height(&self, height: Height) -> Result<BlockInfo> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_by_height(height)).await?
}
pub async fn get_block_by_timestamp(&self, timestamp: Timestamp) -> Result<BlockTimestamp> {
self.0.get_block_by_timestamp(timestamp)
}
pub async fn get_block_status(&self, hash: String) -> Result<BlockStatus> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_status(&hash)).await?
}
pub async fn get_blocks(&self, start_height: Option<Height>) -> Result<Vec<BlockInfo>> {
let query = self.0.clone();
spawn_blocking(move || query.get_blocks(start_height)).await?
}
pub async fn get_block_txids(&self, hash: String) -> Result<Vec<Txid>> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_txids(&hash)).await?
}
pub async fn get_block_txs(
&self,
hash: String,
start_index: usize,
) -> Result<Vec<Transaction>> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_txs(&hash, start_index)).await?
}
pub async fn get_block_txid_at_index(&self, hash: String, index: usize) -> Result<Txid> {
self.0.get_block_txid_at_index(&hash, index)
}
pub async fn get_block_raw(&self, hash: String) -> Result<Vec<u8>> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_raw(&hash)).await?
}
pub async fn get_mempool_info(&self) -> Result<MempoolInfo> {
self.0.get_mempool_info()
}
pub async fn get_mempool_txids(&self) -> Result<Vec<Txid>> {
self.0.get_mempool_txids()
}
pub async fn get_recommended_fees(&self) -> Result<RecommendedFees> {
self.0.get_recommended_fees()
}
pub async fn get_mempool_blocks(&self) -> Result<Vec<MempoolBlock>> {
self.0.get_mempool_blocks()
}
pub async fn get_difficulty_adjustment(&self) -> Result<DifficultyAdjustment> {
let query = self.0.clone();
spawn_blocking(move || query.get_difficulty_adjustment()).await?
}
pub async fn get_mining_pools(&self, time_period: TimePeriod) -> Result<PoolsSummary> {
let query = self.0.clone();
spawn_blocking(move || query.get_mining_pools(time_period)).await?
}
pub async fn get_all_pools(&self) -> Result<Vec<PoolInfo>> {
Ok(self.0.get_all_pools())
}
pub async fn get_pool_detail(&self, slug: PoolSlug) -> Result<PoolDetail> {
let query = self.0.clone();
spawn_blocking(move || query.get_pool_detail(slug)).await?
}
pub async fn get_hashrate(&self, time_period: Option<TimePeriod>) -> Result<HashrateSummary> {
let query = self.0.clone();
spawn_blocking(move || query.get_hashrate(time_period)).await?
}
pub async fn get_difficulty_adjustments(
&self,
time_period: Option<TimePeriod>,
) -> Result<Vec<brk_types::DifficultyAdjustmentEntry>> {
let query = self.0.clone();
spawn_blocking(move || query.get_difficulty_adjustments(time_period)).await?
}
pub async fn get_block_fees(
&self,
time_period: TimePeriod,
) -> Result<Vec<brk_types::BlockFeesEntry>> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_fees(time_period)).await?
}
pub async fn get_block_rewards(
&self,
time_period: TimePeriod,
) -> Result<Vec<brk_types::BlockRewardsEntry>> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_rewards(time_period)).await?
}
pub async fn get_block_fee_rates(
&self,
time_period: TimePeriod,
) -> Result<Vec<brk_types::BlockFeeRatesEntry>> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_fee_rates(time_period)).await?
}
pub async fn get_block_sizes_weights(
&self,
time_period: TimePeriod,
) -> Result<brk_types::BlockSizesWeights> {
let query = self.0.clone();
spawn_blocking(move || query.get_block_sizes_weights(time_period)).await?
}
pub async fn get_reward_stats(&self, block_count: usize) -> Result<brk_types::RewardStats> {
let query = self.0.clone();
spawn_blocking(move || query.get_reward_stats(block_count)).await?
}
pub async fn match_metric(&self, metric: Metric, limit: Limit) -> Result<Vec<&'static str>> {
let query = self.0.clone();
spawn_blocking(move || Ok(query.match_metric(&metric, limit))).await?
}
// pub async fn search_metric_with_index(
// &self,
// metric: &str,
// index: Index,
// // params: &Params,
// ) -> Result<Vec<(String, &&dyn AnyExportableVec)>> {
// let query = self.0.clone();
// spawn_blocking(move || query.search_metric_with_index(metric, index)).await?
// }
// pub async fn format(
// &self,
// metrics: Vec<(String, &&dyn AnyExportableVec)>,
// params: &ParamsOpt,
// ) -> Result<Output> {
// let query = self.0.clone();
// spawn_blocking(move || query.format(metrics, params)).await?
// }
pub async fn search_and_format(&self, params: Params) -> Result<Output> {
let query = self.0.clone();
spawn_blocking(move || query.search_and_format(params)).await?
}
pub async fn metric_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> {
self.0.metric_to_index_to_vec()
}
pub async fn index_to_metric_to_vec(&self) -> &BTreeMap<Index, MetricToVec<'_>> {
self.0.index_to_metric_to_vec()
}
pub async fn metric_count(&self) -> MetricCount {
self.0.metric_count()
}
pub async fn distinct_metric_count(&self) -> usize {
self.0.distinct_metric_count()
}
pub async fn total_metric_count(&self) -> usize {
self.0.total_metric_count()
}
pub async fn get_indexes(&self) -> &[IndexInfo] {
self.0.get_indexes()
}
pub async fn get_metrics(&self, pagination: PaginationParam) -> PaginatedMetrics {
self.0.get_metrics(pagination)
}
pub async fn get_metrics_catalog(&self) -> &TreeNode {
self.0.get_metrics_catalog()
}
pub async fn get_index_to_vecids(&self, paginated_index: PaginatedIndexParam) -> Vec<&str> {
self.0.get_index_to_vecids(paginated_index)
}
pub async fn metric_to_indexes(&self, metric: Metric) -> Option<&Vec<Index>> {
self.0.metric_to_indexes(metric)
}
#[inline]
pub async fn reader(&self) -> &Reader {
self.0.reader()
}
#[inline]
pub async fn indexer(&self) -> &Indexer {
self.0.indexer()
}
#[inline]
pub async fn computer(&self) -> &Computer {
self.0.computer()
}
#[inline]
pub async fn vecs(&self) -> &'static Vecs<'static> {
self.0.vecs()
}
}
-87
View File
@@ -1,87 +0,0 @@
use std::str::FromStr;
use bitcoin::{Network, PublicKey, ScriptBuf};
use brk_error::{Error, Result};
use brk_types::{
Address, AddressBytes, AddressChainStats, AddressHash, AddressStats, AnyAddressDataIndexEnum,
OutputType,
};
use vecdb::TypedVecIterator;
use crate::Query;
pub fn get_address(Address { address }: Address, query: &Query) -> Result<AddressStats> {
let indexer = query.indexer();
let computer = query.computer();
let stores = &indexer.stores;
let script = if let Ok(address) = bitcoin::Address::from_str(&address) {
if !address.is_valid_for_network(Network::Bitcoin) {
return Err(Error::InvalidNetwork);
}
let address = address.assume_checked();
address.script_pubkey()
} else if let Ok(pubkey) = PublicKey::from_str(&address) {
ScriptBuf::new_p2pk(&pubkey)
} else {
return Err(Error::InvalidAddress);
};
let outputtype = OutputType::from(&script);
let Ok(bytes) = AddressBytes::try_from((&script, outputtype)) else {
return Err(Error::Str("Failed to convert the address to bytes"));
};
let addresstype = outputtype;
let hash = AddressHash::from(&bytes);
let Ok(Some(type_index)) = stores
.addresstype_to_addresshash_to_addressindex
.get(addresstype)
.unwrap()
.get(&hash)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownAddress);
};
let any_address_index = computer
.stateful
.any_address_indexes
.get_anyaddressindex_once(outputtype, type_index)?;
let address_data = match any_address_index.to_enum() {
AnyAddressDataIndexEnum::Loaded(index) => computer
.stateful
.addresses_data
.loaded
.iter()?
.get_unwrap(index),
AnyAddressDataIndexEnum::Empty(index) => computer
.stateful
.addresses_data
.empty
.iter()?
.get_unwrap(index)
.into(),
};
Ok(AddressStats {
address: address.into(),
chain_stats: AddressChainStats {
type_index,
funded_txo_count: address_data.funded_txo_count,
funded_txo_sum: address_data.received,
spent_txo_count: address_data.spent_txo_count,
spent_txo_sum: address_data.sent,
tx_count: address_data.tx_count,
},
mempool_stats: query.mempool().map(|mempool| {
mempool
.get_addresses()
.get(&bytes)
.map(|(stats, _)| stats)
.cloned()
.unwrap_or_default()
}),
})
}
@@ -1,24 +0,0 @@
use std::str::FromStr;
use brk_error::{Error, Result};
use brk_types::{Address, AddressBytes, Txid};
use crate::Query;
/// Maximum number of mempool txids to return
const MAX_MEMPOOL_TXIDS: usize = 50;
/// Get mempool transaction IDs for an address
pub fn get_address_mempool_txids(address: Address, query: &Query) -> Result<Vec<Txid>> {
let mempool = query.mempool().ok_or(Error::Str("Mempool not available"))?;
let bytes = AddressBytes::from_str(&address.address)?;
let addresses = mempool.get_addresses();
let txids: Vec<Txid> = addresses
.get(&bytes)
.map(|(_, txids)| txids.iter().take(MAX_MEMPOOL_TXIDS).cloned().collect())
.unwrap_or_default();
Ok(txids)
}
-12
View File
@@ -1,12 +0,0 @@
mod addr;
mod mempool_txids;
mod resolve;
mod txids;
mod utxos;
mod validate;
pub use addr::*;
pub use mempool_txids::*;
pub use txids::*;
pub use utxos::*;
pub use validate::*;
@@ -1,27 +0,0 @@
use std::str::FromStr;
use brk_error::{Error, Result};
use brk_types::{Address, AddressBytes, AddressHash, OutputType, TypeIndex};
use crate::Query;
/// Resolve an address string to its output type and type_index
pub fn resolve_address(address: &Address, query: &Query) -> Result<(OutputType, TypeIndex)> {
let stores = &query.indexer().stores;
let bytes = AddressBytes::from_str(&address.address)?;
let outputtype = OutputType::from(&bytes);
let hash = AddressHash::from(&bytes);
let Ok(Some(type_index)) = stores
.addresstype_to_addresshash_to_addressindex
.get(outputtype)
.unwrap()
.get(&hash)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownAddress);
};
Ok((outputtype, type_index))
}
-60
View File
@@ -1,60 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{Address, AddressIndexTxIndex, TxIndex, Txid, Unit};
use vecdb::TypedVecIterator;
use super::resolve::resolve_address;
use crate::Query;
/// Get transaction IDs for an address, newest first
pub fn get_address_txids(
address: Address,
after_txid: Option<Txid>,
limit: usize,
query: &Query,
) -> Result<Vec<Txid>> {
let indexer = query.indexer();
let stores = &indexer.stores;
let (outputtype, type_index) = resolve_address(&address, query)?;
let store = stores
.addresstype_to_addressindex_and_txindex
.get(outputtype)
.unwrap();
let prefix = u32::from(type_index).to_be_bytes();
let after_txindex = if let Some(after_txid) = after_txid {
let txindex = stores
.txidprefix_to_txindex
.get(&after_txid.into())
.map_err(|_| Error::Str("Failed to look up after_txid"))?
.ok_or(Error::Str("after_txid not found"))?
.into_owned();
Some(txindex)
} else {
None
};
let txindices: Vec<TxIndex> = store
.prefix(prefix)
.rev()
.filter(|(key, _): &(AddressIndexTxIndex, Unit)| {
if let Some(after) = after_txindex {
key.txindex() < after
} else {
true
}
})
.take(limit)
.map(|(key, _)| key.txindex())
.collect();
let mut txindex_to_txid_iter = indexer.vecs.tx.txindex_to_txid.iter()?;
let txids: Vec<Txid> = txindices
.into_iter()
.map(|txindex| txindex_to_txid_iter.get_unwrap(txindex))
.collect();
Ok(txids)
}
-65
View File
@@ -1,65 +0,0 @@
use brk_error::Result;
use brk_types::{
Address, AddressIndexOutPoint, Sats, TxIndex, TxStatus, Txid, Unit, Utxo, Vout,
};
use vecdb::TypedVecIterator;
use super::resolve::resolve_address;
use crate::Query;
/// Get UTXOs for an address
pub fn get_address_utxos(address: Address, query: &Query) -> Result<Vec<Utxo>> {
let indexer = query.indexer();
let stores = &indexer.stores;
let vecs = &indexer.vecs;
let (outputtype, type_index) = resolve_address(&address, query)?;
let store = stores
.addresstype_to_addressindex_and_unspentoutpoint
.get(outputtype)
.unwrap();
let prefix = u32::from(type_index).to_be_bytes();
// Collect outpoints (txindex, vout)
let outpoints: Vec<(TxIndex, Vout)> = store
.prefix(prefix)
.map(|(key, _): (AddressIndexOutPoint, Unit)| (key.txindex(), key.vout()))
.collect();
// Create iterators for looking up tx data
let mut txindex_to_txid_iter = vecs.tx.txindex_to_txid.iter()?;
let mut txindex_to_height_iter = vecs.tx.txindex_to_height.iter()?;
let mut txindex_to_first_txoutindex_iter = vecs.tx.txindex_to_first_txoutindex.iter()?;
let mut txoutindex_to_value_iter = vecs.txout.txoutindex_to_value.iter()?;
let mut height_to_blockhash_iter = vecs.block.height_to_blockhash.iter()?;
let mut height_to_timestamp_iter = vecs.block.height_to_timestamp.iter()?;
let utxos: Vec<Utxo> = outpoints
.into_iter()
.map(|(txindex, vout)| {
let txid: Txid = txindex_to_txid_iter.get_unwrap(txindex);
let height = txindex_to_height_iter.get_unwrap(txindex);
let first_txoutindex = txindex_to_first_txoutindex_iter.get_unwrap(txindex);
let txoutindex = first_txoutindex + vout;
let value: Sats = txoutindex_to_value_iter.get_unwrap(txoutindex);
let block_hash = height_to_blockhash_iter.get_unwrap(height);
let block_time = height_to_timestamp_iter.get_unwrap(height);
Utxo {
txid,
vout,
status: TxStatus {
confirmed: true,
block_height: Some(height),
block_hash: Some(block_hash),
block_time: Some(block_time),
},
value,
}
})
.collect();
Ok(utxos)
}
@@ -1,41 +0,0 @@
use bitcoin::hex::DisplayHex;
use brk_types::{AddressBytes, AddressValidation, OutputType};
/// Validate a Bitcoin address and return details
pub fn validate_address(address: &str) -> AddressValidation {
let Ok(script) = AddressBytes::address_to_script(address) else {
return AddressValidation::invalid();
};
let output_type = OutputType::from(&script);
let script_hex = script.as_bytes().to_lower_hex_string();
let is_script = matches!(output_type, OutputType::P2SH);
let is_witness = matches!(
output_type,
OutputType::P2WPKH | OutputType::P2WSH | OutputType::P2TR | OutputType::P2A
);
let (witness_version, witness_program) = if is_witness {
let version = script.witness_version().map(|v| v.to_num());
// Witness program is after the version byte and push opcode
let program = if script.len() > 2 {
Some(script.as_bytes()[2..].to_lower_hex_string())
} else {
None
};
(version, program)
} else {
(None, None)
};
AddressValidation {
isvalid: true,
address: Some(address.to_string()),
script_pub_key: Some(script_hex),
isscript: Some(is_script),
iswitness: Some(is_witness),
witness_version,
witness_program,
}
}
@@ -1,80 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{BlockTimestamp, Date, DateIndex, Height, Timestamp};
use jiff::Timestamp as JiffTimestamp;
use vecdb::{GenericStoredVec, TypedVecIterator};
use crate::Query;
/// Get the block closest to a given timestamp using dateindex for fast lookup
pub fn get_block_by_timestamp(timestamp: Timestamp, query: &Query) -> Result<BlockTimestamp> {
let indexer = query.indexer();
let computer = query.computer();
let max_height = query.get_height();
let max_height_usize: usize = max_height.into();
if max_height_usize == 0 {
return Err(Error::Str("No blocks indexed"));
}
let target = timestamp;
let date = Date::from(target);
let dateindex = DateIndex::try_from(date).unwrap_or_default();
// Get first height of the target date
let first_height_of_day = computer
.indexes
.dateindex_to_first_height
.read_once(dateindex)
.unwrap_or(Height::from(0usize));
let start: usize = usize::from(first_height_of_day).min(max_height_usize);
// Use iterator for efficient sequential access
let mut timestamp_iter = indexer.vecs.block.height_to_timestamp.iter()?;
// Search forward from start to find the last block <= target timestamp
let mut best_height = start;
let mut best_ts = timestamp_iter.get_unwrap(Height::from(start));
for h in (start + 1)..=max_height_usize {
let height = Height::from(h);
let block_ts = timestamp_iter.get_unwrap(height);
if block_ts <= target {
best_height = h;
best_ts = block_ts;
} else {
break;
}
}
// Check one block before start in case we need to go backward
if start > 0 && best_ts > target {
let prev_height = Height::from(start - 1);
let prev_ts = timestamp_iter.get_unwrap(prev_height);
if prev_ts <= target {
best_height = start - 1;
best_ts = prev_ts;
}
}
let height = Height::from(best_height);
let blockhash = indexer
.vecs
.block
.height_to_blockhash
.iter()?
.get_unwrap(height);
// Convert timestamp to ISO 8601 format
let ts_secs: i64 = (*best_ts).into();
let iso_timestamp = JiffTimestamp::from_second(ts_secs)
.map(|t| t.to_string())
.unwrap_or_else(|_| best_ts.to_string());
Ok(BlockTimestamp {
height,
hash: blockhash,
timestamp: iso_timestamp,
})
}
@@ -1,19 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{BlockHash, BlockHashPrefix, Height};
use crate::Query;
/// Resolve a block hash to height
pub fn get_height_by_hash(hash: &str, query: &Query) -> Result<Height> {
let indexer = query.indexer();
let blockhash: BlockHash = hash.parse().map_err(|_| Error::Str("Invalid block hash"))?;
let prefix = BlockHashPrefix::from(&blockhash);
indexer
.stores
.blockhashprefix_to_height
.get(&prefix)?
.map(|h| *h)
.ok_or(Error::Str("Block not found"))
}
-62
View File
@@ -1,62 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{BlockInfo, Height, TxIndex};
use vecdb::{AnyVec, GenericStoredVec, VecIndex};
use crate::Query;
/// Get block info by height
pub fn get_block_by_height(height: Height, query: &Query) -> Result<BlockInfo> {
let indexer = query.indexer();
let max_height = max_height(query);
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let blockhash = indexer.vecs.block.height_to_blockhash.read_once(height)?;
let difficulty = indexer.vecs.block.height_to_difficulty.read_once(height)?;
let timestamp = indexer.vecs.block.height_to_timestamp.read_once(height)?;
let size = indexer.vecs.block.height_to_total_size.read_once(height)?;
let weight = indexer.vecs.block.height_to_weight.read_once(height)?;
let tx_count = tx_count_at_height(height, max_height, query)?;
Ok(BlockInfo {
id: blockhash,
height,
tx_count,
size: *size,
weight,
timestamp,
difficulty: *difficulty,
})
}
fn max_height(query: &Query) -> Height {
Height::from(
query
.indexer()
.vecs
.block
.height_to_blockhash
.len()
.saturating_sub(1),
)
}
fn tx_count_at_height(height: Height, max_height: Height, query: &Query) -> Result<u32> {
let indexer = query.indexer();
let computer = query.computer();
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = if height < max_height {
indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())?
} else {
TxIndex::from(computer.indexes.txindex_to_txindex.len())
};
Ok((next_first_txindex.to_usize() - first_txindex.to_usize()) as u32)
}
-27
View File
@@ -1,27 +0,0 @@
use brk_error::Result;
use brk_types::{BlockInfo, Height};
use crate::Query;
use super::info::get_block_by_height;
const DEFAULT_BLOCK_COUNT: u32 = 10;
/// Get a list of blocks, optionally starting from a specific height
pub fn get_blocks(start_height: Option<Height>, query: &Query) -> Result<Vec<BlockInfo>> {
let max_height = query.get_height();
let start = start_height.unwrap_or(max_height);
let start = start.min(max_height);
let start_u32: u32 = start.into();
let count = DEFAULT_BLOCK_COUNT.min(start_u32 + 1);
let mut blocks = Vec::with_capacity(count as usize);
for i in 0..count {
let height = Height::from(start_u32 - i);
blocks.push(get_block_by_height(height, query)?);
}
Ok(blocks)
}
-19
View File
@@ -1,19 +0,0 @@
mod by_timestamp;
mod height_by_hash;
mod info;
mod list;
mod raw;
mod status;
mod txid_at_index;
mod txids;
mod txs;
pub use by_timestamp::*;
pub use height_by_hash::*;
pub use info::*;
pub use list::*;
pub use raw::*;
pub use status::*;
pub use txid_at_index::*;
pub use txids::*;
pub use txs::*;
-29
View File
@@ -1,29 +0,0 @@
use brk_error::{Error, Result};
use brk_types::Height;
use vecdb::{AnyVec, GenericStoredVec};
use crate::Query;
/// Get raw block bytes by height
pub fn get_block_raw(height: Height, query: &Query) -> Result<Vec<u8>> {
let indexer = query.indexer();
let computer = query.computer();
let reader = query.reader();
let max_height = Height::from(
indexer
.vecs
.block
.height_to_blockhash
.len()
.saturating_sub(1),
);
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let position = computer.blks.height_to_position.read_once(height)?;
let size = indexer.vecs.block.height_to_total_size.read_once(height)?;
reader.read_raw_bytes(position, *size as usize)
}
@@ -1,37 +0,0 @@
use brk_error::Result;
use brk_types::{BlockStatus, Height};
use vecdb::{AnyVec, GenericStoredVec};
use crate::Query;
/// Get block status by height
pub fn get_block_status_by_height(height: Height, query: &Query) -> Result<BlockStatus> {
let indexer = query.indexer();
let max_height = Height::from(
indexer
.vecs
.block
.height_to_blockhash
.len()
.saturating_sub(1),
);
if height > max_height {
return Ok(BlockStatus::not_in_best_chain());
}
let next_best = if height < max_height {
Some(
indexer
.vecs
.block
.height_to_blockhash
.read_once(height.incremented())?,
)
} else {
None
};
Ok(BlockStatus::in_best_chain(height, next_best))
}
@@ -1,36 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{Height, TxIndex, Txid};
use vecdb::{AnyVec, GenericStoredVec, TypedVecIterator};
use crate::Query;
/// Get a single txid at a specific index within a block
pub fn get_block_txid_at_index(height: Height, index: usize, query: &Query) -> Result<Txid> {
let indexer = query.indexer();
let max_height = query.get_height();
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())
.unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len()));
let first: usize = first_txindex.into();
let next: usize = next_first_txindex.into();
let tx_count = next - first;
if index >= tx_count {
return Err(Error::Str("Transaction index out of range"));
}
let txindex = TxIndex::from(first + index);
let txid = indexer.vecs.tx.txindex_to_txid.iter()?.get_unwrap(txindex);
Ok(txid)
}
-38
View File
@@ -1,38 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{Height, TxIndex, Txid};
use vecdb::{AnyVec, GenericStoredVec};
use crate::Query;
/// Get all txids in a block by height
pub fn get_block_txids(height: Height, query: &Query) -> Result<Vec<Txid>> {
let indexer = query.indexer();
let max_height = query.get_height();
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())
.unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len()));
let first: usize = first_txindex.into();
let next: usize = next_first_txindex.into();
let count = next - first;
let txids: Vec<Txid> = indexer
.vecs
.tx
.txindex_to_txid
.iter()?
.skip(first)
.take(count)
.collect();
Ok(txids)
}
-45
View File
@@ -1,45 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{Height, Transaction, TxIndex};
use vecdb::{AnyVec, GenericStoredVec};
use crate::{Query, chain::tx::get_transaction_by_index};
pub const BLOCK_TXS_PAGE_SIZE: usize = 25;
/// Get paginated transactions in a block by height
pub fn get_block_txs(height: Height, start_index: usize, query: &Query) -> Result<Vec<Transaction>> {
let indexer = query.indexer();
let max_height = query.get_height();
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())
.unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len()));
let first: usize = first_txindex.into();
let next: usize = next_first_txindex.into();
let tx_count = next - first;
if start_index >= tx_count {
return Ok(Vec::new());
}
let end_index = (start_index + BLOCK_TXS_PAGE_SIZE).min(tx_count);
let count = end_index - start_index;
let mut txs = Vec::with_capacity(count);
for i in start_index..end_index {
let txindex = TxIndex::from(first + i);
let tx = get_transaction_by_index(txindex, query)?;
txs.push(tx);
}
Ok(txs)
}
@@ -1,20 +0,0 @@
use brk_error::{Error, Result};
use brk_types::MempoolBlock;
use crate::Query;
/// Get projected mempool blocks for fee estimation
pub fn get_mempool_blocks(query: &Query) -> Result<Vec<MempoolBlock>> {
let mempool = query.mempool().ok_or(Error::Str("Mempool not available"))?;
let block_stats = mempool.get_block_stats();
let blocks = block_stats
.into_iter()
.map(|stats| {
MempoolBlock::new(stats.tx_count, stats.total_vsize, stats.total_fee, stats.fee_range)
})
.collect();
Ok(blocks)
}
@@ -1,11 +0,0 @@
use brk_error::{Error, Result};
use brk_types::RecommendedFees;
use crate::Query;
pub fn get_recommended_fees(query: &Query) -> Result<RecommendedFees> {
query
.mempool()
.map(|mempool| mempool.get_fees())
.ok_or(Error::MempoolNotAvailable)
}
@@ -1,10 +0,0 @@
use brk_error::{Error, Result};
use brk_types::MempoolInfo;
use crate::Query;
/// Get mempool statistics
pub fn get_mempool_info(query: &Query) -> Result<MempoolInfo> {
let mempool = query.mempool().ok_or(Error::Str("Mempool not available"))?;
Ok(mempool.get_info())
}
@@ -1,9 +0,0 @@
mod blocks;
mod fees;
mod info;
mod txids;
pub use blocks::*;
pub use fees::*;
pub use info::*;
pub use txids::*;
@@ -1,11 +0,0 @@
use brk_error::{Error, Result};
use brk_types::Txid;
use crate::Query;
/// Get all mempool transaction IDs
pub fn get_mempool_txids(query: &Query) -> Result<Vec<Txid>> {
let mempool = query.mempool().ok_or(Error::Str("Mempool not available"))?;
let txs = mempool.get_txs();
Ok(txs.keys().cloned().collect())
}
@@ -1,44 +0,0 @@
use brk_error::Result;
use brk_types::{BlockFeeRatesEntry, FeeRatePercentiles, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
pub fn get_block_fee_rates(
time_period: TimePeriod,
query: &Query,
) -> Result<Vec<BlockFeeRatesEntry>> {
let computer = query.computer();
let current_height = query.get_height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
let vecs = &computer.chain.indexes_to_fee_rate.dateindex;
let mut min = vecs.unwrap_min().iter();
let mut pct10 = vecs.unwrap_pct10().iter();
let mut pct25 = vecs.unwrap_pct25().iter();
let mut median = vecs.unwrap_median().iter();
let mut pct75 = vecs.unwrap_pct75().iter();
let mut pct90 = vecs.unwrap_pct90().iter();
let mut max = vecs.unwrap_max().iter();
Ok(iter.collect(|di, ts, h| {
Some(BlockFeeRatesEntry {
avg_height: h.into(),
timestamp: *ts as u32,
percentiles: FeeRatePercentiles::new(
min.get(di).unwrap_or_default(),
pct10.get(di).unwrap_or_default(),
pct25.get(di).unwrap_or_default(),
median.get(di).unwrap_or_default(),
pct75.get(di).unwrap_or_default(),
pct90.get(di).unwrap_or_default(),
max.get(di).unwrap_or_default(),
),
})
}))
}
@@ -1,32 +0,0 @@
use brk_error::Result;
use brk_types::{BlockFeesEntry, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
pub fn get_block_fees(time_period: TimePeriod, query: &Query) -> Result<Vec<BlockFeesEntry>> {
let computer = query.computer();
let current_height = query.get_height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
let mut fees = computer
.chain
.indexes_to_fee
.sats
.dateindex
.unwrap_average()
.iter();
Ok(iter.collect(|di, ts, h| {
fees.get(di).map(|fee| BlockFeesEntry {
avg_height: h.into(),
timestamp: *ts as u32,
avg_fees: u64::from(*fee),
})
}))
}
@@ -1,33 +0,0 @@
use brk_error::Result;
use brk_types::{BlockRewardsEntry, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
pub fn get_block_rewards(time_period: TimePeriod, query: &Query) -> Result<Vec<BlockRewardsEntry>> {
let computer = query.computer();
let current_height = query.get_height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
// coinbase = subsidy + fees
let mut rewards = computer
.chain
.indexes_to_coinbase
.sats
.dateindex
.unwrap_average()
.iter();
Ok(iter.collect(|di, ts, h| {
rewards.get(di).map(|reward| BlockRewardsEntry {
avg_height: h.into(),
timestamp: *ts as u32,
avg_rewards: u64::from(*reward),
})
}))
}
@@ -1,62 +0,0 @@
use brk_error::Result;
use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
pub fn get_block_sizes_weights(
time_period: TimePeriod,
query: &Query,
) -> Result<BlockSizesWeights> {
let computer = query.computer();
let current_height = query.get_height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
let mut sizes_vec = computer
.chain
.indexes_to_block_size
.dateindex
.unwrap_average()
.iter();
let mut weights_vec = computer
.chain
.indexes_to_block_weight
.dateindex
.unwrap_average()
.iter();
let entries: Vec<_> = iter.collect(|di, ts, h| {
let size = sizes_vec.get(di).map(|s| u64::from(*s));
let weight = weights_vec.get(di).map(|w| u64::from(*w));
Some((h.into(), *ts as u32, size, weight))
});
let sizes = entries
.iter()
.filter_map(|(h, ts, size, _)| {
size.map(|s| BlockSizeEntry {
avg_height: *h,
timestamp: *ts,
avg_size: s,
})
})
.collect();
let weights = entries
.iter()
.filter_map(|(h, ts, _, weight)| {
weight.map(|w| BlockWeightEntry {
avg_height: *h,
timestamp: *ts,
avg_weight: w,
})
})
.collect();
Ok(BlockSizesWeights { sizes, weights })
}
@@ -1,120 +0,0 @@
use std::time::{SystemTime, UNIX_EPOCH};
use brk_error::Result;
use brk_types::{DifficultyAdjustment, DifficultyEpoch, Height};
use vecdb::GenericStoredVec;
use crate::Query;
/// Blocks per difficulty epoch (2 weeks target)
const BLOCKS_PER_EPOCH: u32 = 2016;
/// Target block time in seconds (10 minutes)
const TARGET_BLOCK_TIME: u64 = 600;
/// Get difficulty adjustment information
pub fn get_difficulty_adjustment(query: &Query) -> Result<DifficultyAdjustment> {
let indexer = query.indexer();
let computer = query.computer();
let current_height = query.get_height();
let current_height_u32: u32 = current_height.into();
// Get current epoch
let current_epoch = computer
.indexes
.height_to_difficultyepoch
.read_once(current_height)?;
let current_epoch_usize: usize = current_epoch.into();
// Get epoch start height
let epoch_start_height = computer
.indexes
.difficultyepoch_to_first_height
.read_once(current_epoch)?;
let epoch_start_u32: u32 = epoch_start_height.into();
// Calculate epoch progress
let next_retarget_height = epoch_start_u32 + BLOCKS_PER_EPOCH;
let blocks_into_epoch = current_height_u32 - epoch_start_u32;
let remaining_blocks = next_retarget_height - current_height_u32;
let progress_percent = (blocks_into_epoch as f64 / BLOCKS_PER_EPOCH as f64) * 100.0;
// Get timestamps using difficultyepoch_to_timestamp for epoch start
let epoch_start_timestamp = computer
.chain
.difficultyepoch_to_timestamp
.read_once(current_epoch)?;
let current_timestamp = indexer
.vecs
.block
.height_to_timestamp
.read_once(current_height)?;
// Calculate average block time in current epoch
let elapsed_time = (*current_timestamp - *epoch_start_timestamp) as u64;
let time_avg = if blocks_into_epoch > 0 {
elapsed_time / blocks_into_epoch as u64
} else {
TARGET_BLOCK_TIME
};
// Estimate remaining time and retarget date
let remaining_time = remaining_blocks as u64 * time_avg;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(*current_timestamp as u64);
let estimated_retarget_date = now + remaining_time;
// Calculate expected vs actual time for difficulty change estimate
let expected_time = blocks_into_epoch as u64 * TARGET_BLOCK_TIME;
let difficulty_change = if elapsed_time > 0 && blocks_into_epoch > 0 {
((expected_time as f64 / elapsed_time as f64) - 1.0) * 100.0
} else {
0.0
};
// Time offset from expected schedule
let time_offset = expected_time as i64 - elapsed_time as i64;
// Calculate previous retarget using stored difficulty values
let previous_retarget = if current_epoch_usize > 0 {
let prev_epoch = DifficultyEpoch::from(current_epoch_usize - 1);
let prev_epoch_start = computer
.indexes
.difficultyepoch_to_first_height
.read_once(prev_epoch)?;
let prev_difficulty = indexer
.vecs
.block
.height_to_difficulty
.read_once(prev_epoch_start)?;
let curr_difficulty = indexer
.vecs
.block
.height_to_difficulty
.read_once(epoch_start_height)?;
if *prev_difficulty > 0.0 {
((*curr_difficulty / *prev_difficulty) - 1.0) * 100.0
} else {
0.0
}
} else {
0.0
};
Ok(DifficultyAdjustment {
progress_percent,
difficulty_change,
estimated_retarget_date,
remaining_blocks,
remaining_time,
previous_retarget,
next_retarget_height: Height::from(next_retarget_height),
time_avg,
adjusted_time_avg: time_avg,
time_offset,
})
}
@@ -1,26 +0,0 @@
use brk_error::Result;
use brk_types::{DifficultyAdjustmentEntry, TimePeriod};
use vecdb::VecIndex;
use crate::Query;
use super::epochs::iter_difficulty_epochs;
/// Get historical difficulty adjustments.
pub fn get_difficulty_adjustments(
time_period: Option<TimePeriod>,
query: &Query,
) -> Result<Vec<DifficultyAdjustmentEntry>> {
let current_height = query.get_height();
let end = current_height.to_usize();
let start = match time_period {
Some(tp) => end.saturating_sub(tp.block_count()),
None => 0,
};
let mut entries = iter_difficulty_epochs(query.computer(), start, end);
// Return in reverse chronological order (newest first)
entries.reverse();
Ok(entries)
}
@@ -1,99 +0,0 @@
use brk_error::Result;
use brk_types::{DateIndex, DifficultyEntry, HashrateEntry, HashrateSummary, Height, TimePeriod};
use vecdb::{GenericStoredVec, IterableVec, VecIndex};
use super::epochs::iter_difficulty_epochs;
use crate::Query;
/// Get hashrate and difficulty data for a time period.
pub fn get_hashrate(time_period: Option<TimePeriod>, query: &Query) -> Result<HashrateSummary> {
let indexer = query.indexer();
let computer = query.computer();
let current_height = query.get_height();
// Get current difficulty
let current_difficulty = *indexer
.vecs
.block
.height_to_difficulty
.read_once(current_height)?;
// Get current hashrate
let current_dateindex = computer
.indexes
.height_to_dateindex
.read_once(current_height)?;
let current_hashrate = *computer
.chain
.indexes_to_hash_rate
.dateindex
.unwrap_last()
.read_once(current_dateindex)? as u128;
// Calculate start height based on time period
let end = current_height.to_usize();
let start = match time_period {
Some(tp) => end.saturating_sub(tp.block_count()),
None => 0,
};
// Get hashrate entries using iterators for efficiency
let start_dateindex = computer
.indexes
.height_to_dateindex
.read_once(Height::from(start))?;
let end_dateindex = current_dateindex;
// Sample at regular intervals to avoid too many data points
let total_days = end_dateindex
.to_usize()
.saturating_sub(start_dateindex.to_usize())
+ 1;
let step = (total_days / 200).max(1); // Max ~200 data points
// Create iterators for the loop
let mut hashrate_iter = computer
.chain
.indexes_to_hash_rate
.dateindex
.unwrap_last()
.iter();
let mut timestamp_iter = computer
.chain
.timeindexes_to_timestamp
.dateindex_extra
.unwrap_first()
.iter();
let mut hashrates = Vec::with_capacity(total_days / step + 1);
let mut di = start_dateindex.to_usize();
while di <= end_dateindex.to_usize() {
let dateindex = DateIndex::from(di);
if let (Some(hr), Some(timestamp)) =
(hashrate_iter.get(dateindex), timestamp_iter.get(dateindex))
{
hashrates.push(HashrateEntry {
timestamp,
avg_hashrate: (*hr) as u128,
});
}
di += step;
}
// Get difficulty adjustments within the period
let difficulty: Vec<DifficultyEntry> = iter_difficulty_epochs(computer, start, end)
.into_iter()
.map(|e| DifficultyEntry {
timestamp: e.timestamp,
difficulty: e.difficulty,
height: e.height,
})
.collect();
Ok(HashrateSummary {
hashrates,
difficulty,
current_hashrate,
current_difficulty,
})
}
-21
View File
@@ -1,21 +0,0 @@
mod block_fee_rates;
mod block_fees;
mod block_rewards;
mod block_sizes_weights;
mod dateindex_iter;
mod difficulty;
mod difficulty_adjustments;
mod epochs;
mod hashrate;
mod pools;
mod reward_stats;
pub use block_fee_rates::*;
pub use block_fees::*;
pub use block_rewards::*;
pub use block_sizes_weights::*;
pub use difficulty::*;
pub use difficulty_adjustments::*;
pub use hashrate::*;
pub use pools::*;
pub use reward_stats::*;
-172
View File
@@ -1,172 +0,0 @@
use brk_error::{Error, Result};
use brk_types::{
Height, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo, PoolInfo, PoolSlug,
PoolStats, PoolsSummary, TimePeriod, pools,
};
use vecdb::{AnyVec, GenericStoredVec, IterableVec, VecIndex};
use crate::Query;
/// Get mining pool statistics for a time period using pre-computed cumulative counts.
pub fn get_mining_pools(time_period: TimePeriod, query: &Query) -> Result<PoolsSummary> {
let computer = query.computer();
let current_height = query.get_height();
let end = current_height.to_usize();
// No blocks indexed yet
if computer.pools.height_to_pool.len() == 0 {
return Ok(PoolsSummary {
pools: vec![],
block_count: 0,
last_estimated_hashrate: 0,
});
}
// Calculate start height based on time period
let start = end.saturating_sub(time_period.block_count());
let pools = pools();
let mut pool_data: Vec<(&'static brk_types::Pool, u32)> = Vec::new();
// For each pool, get cumulative count at end and start, subtract to get range count
for (pool_id, pool_vecs) in &computer.pools.vecs {
let mut cumulative = pool_vecs
.indexes_to_blocks_mined
.height_extra
.unwrap_cumulative()
.iter();
let count_at_end: u32 = *cumulative.get(current_height).unwrap_or_default();
let count_at_start: u32 = if start == 0 {
0
} else {
*cumulative.get(Height::from(start - 1)).unwrap_or_default()
};
let block_count = count_at_end.saturating_sub(count_at_start);
// Only include pools that mined at least one block in the period
if block_count > 0 {
pool_data.push((pools.get(*pool_id), block_count));
}
}
// Sort by block count descending
pool_data.sort_by(|a, b| b.1.cmp(&a.1));
let total_blocks: u32 = pool_data.iter().map(|(_, count)| count).sum();
// Build stats with ranks
let pool_stats: Vec<PoolStats> = pool_data
.into_iter()
.enumerate()
.map(|(idx, (pool, block_count))| {
let share = if total_blocks > 0 {
block_count as f64 / total_blocks as f64
} else {
0.0
};
PoolStats::new(pool, block_count, (idx + 1) as u32, share)
})
.collect();
// TODO: Calculate actual hashrate from difficulty
let last_estimated_hashrate = 0u128;
Ok(PoolsSummary {
pools: pool_stats,
block_count: total_blocks,
last_estimated_hashrate,
})
}
/// Get list of all known mining pools (no statistics).
pub fn get_all_pools() -> Vec<PoolInfo> {
pools().iter().map(PoolInfo::from).collect()
}
/// Get detailed information about a specific pool by slug.
pub fn get_pool_detail(slug: PoolSlug, query: &Query) -> Result<PoolDetail> {
let computer = query.computer();
let current_height = query.get_height();
let end = current_height.to_usize();
let pools_list = pools();
let pool = pools_list.get(slug);
// Get pool vecs for this specific pool
let pool_vecs = computer
.pools
.vecs
.get(&slug)
.ok_or_else(|| Error::Str("Pool data not found"))?;
let mut cumulative = pool_vecs
.indexes_to_blocks_mined
.height_extra
.unwrap_cumulative()
.iter();
// Get total blocks (all time)
let total_all: u32 = *cumulative.get(current_height).unwrap_or_default();
// Get blocks for 24h (144 blocks)
let start_24h = end.saturating_sub(144);
let count_before_24h: u32 = if start_24h == 0 {
0
} else {
*cumulative
.get(Height::from(start_24h - 1))
.unwrap_or_default()
};
let total_24h = total_all.saturating_sub(count_before_24h);
// Get blocks for 1w (1008 blocks)
let start_1w = end.saturating_sub(1008);
let count_before_1w: u32 = if start_1w == 0 {
0
} else {
*cumulative
.get(Height::from(start_1w - 1))
.unwrap_or_default()
};
let total_1w = total_all.saturating_sub(count_before_1w);
// Calculate total network blocks for share calculation
let network_blocks_all = (end + 1) as u32;
let network_blocks_24h = (end - start_24h + 1) as u32;
let network_blocks_1w = (end - start_1w + 1) as u32;
let share_all = if network_blocks_all > 0 {
total_all as f64 / network_blocks_all as f64
} else {
0.0
};
let share_24h = if network_blocks_24h > 0 {
total_24h as f64 / network_blocks_24h as f64
} else {
0.0
};
let share_1w = if network_blocks_1w > 0 {
total_1w as f64 / network_blocks_1w as f64
} else {
0.0
};
Ok(PoolDetail {
pool: PoolDetailInfo::from(pool),
block_count: PoolBlockCounts {
all: total_all,
day: total_24h,
week: total_1w,
},
block_share: PoolBlockShares {
all: share_all,
day: share_24h,
week: share_1w,
},
estimated_hashrate: 0, // TODO: Calculate from share and network hashrate
reported_hashrate: None,
})
}
@@ -1,58 +0,0 @@
use brk_error::Result;
use brk_types::{Height, RewardStats, Sats};
use vecdb::{IterableVec, VecIndex};
use crate::Query;
pub fn get_reward_stats(block_count: usize, query: &Query) -> Result<RewardStats> {
let computer = query.computer();
let current_height = query.get_height();
let end_block = current_height;
let start_block = Height::from(current_height.to_usize().saturating_sub(block_count - 1));
let mut coinbase_iter = computer
.chain
.indexes_to_coinbase
.sats
.height
.as_ref()
.unwrap()
.iter();
let mut fee_iter = computer.chain.indexes_to_fee.sats.height.unwrap_sum().iter();
let mut tx_count_iter = computer
.chain
.indexes_to_tx_count
.height
.as_ref()
.unwrap()
.iter();
let mut total_reward = Sats::ZERO;
let mut total_fee = Sats::ZERO;
let mut total_tx: u64 = 0;
for height in start_block.to_usize()..=end_block.to_usize() {
let h = Height::from(height);
if let Some(coinbase) = coinbase_iter.get(h) {
total_reward += Sats::from(u64::from(*coinbase));
}
if let Some(fee) = fee_iter.get(h) {
total_fee += Sats::from(u64::from(*fee));
}
if let Some(tx_count) = tx_count_iter.get(h) {
total_tx += u64::from(*tx_count);
}
}
Ok(RewardStats {
start_block,
end_block,
total_reward,
total_fee,
total_tx,
})
}
-11
View File
@@ -1,11 +0,0 @@
mod addr;
mod block;
mod mempool;
mod mining;
mod tx;
pub use addr::*;
pub use block::*;
pub use mempool::*;
pub use mining::*;
pub use tx::*;
-50
View File
@@ -1,50 +0,0 @@
use std::str::FromStr;
use bitcoin::hex::DisplayHex;
use brk_error::{Error, Result};
use brk_types::{TxIndex, Txid, TxidPath, TxidPrefix};
use vecdb::GenericStoredVec;
use crate::Query;
pub fn get_transaction_hex(TxidPath { txid }: TxidPath, query: &Query) -> Result<String> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// First check mempool for unconfirmed transactions
if let Some(mempool) = query.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(&txid)
{
return Ok(tx_with_hex.hex().to_string());
}
// Look up confirmed transaction by txid prefix
let prefix = TxidPrefix::from(&txid);
let indexer = query.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
get_transaction_hex_by_index(txindex, query)
}
pub fn get_transaction_hex_by_index(txindex: TxIndex, query: &Query) -> Result<String> {
let indexer = query.indexer();
let reader = query.reader();
let computer = query.computer();
let total_size = indexer.vecs.tx.txindex_to_total_size.read_once(txindex)?;
let position = computer.blks.txindex_to_position.read_once(txindex)?;
let buffer = reader.read_raw_bytes(position, *total_size as usize)?;
Ok(buffer.to_lower_hex_string())
}
-9
View File
@@ -1,9 +0,0 @@
mod hex;
mod outspend;
mod status;
mod tx;
pub use hex::*;
pub use outspend::*;
pub use status::*;
pub use tx::*;
-174
View File
@@ -1,174 +0,0 @@
use std::str::FromStr;
use brk_error::{Error, Result};
use brk_types::{TxInIndex, TxOutspend, TxStatus, Txid, TxidPath, TxidPrefix, Vin, Vout};
use vecdb::{GenericStoredVec, TypedVecIterator};
use crate::Query;
/// Get the spend status of a specific output
pub fn get_tx_outspend(
TxidPath { txid }: TxidPath,
vout: Vout,
query: &Query,
) -> Result<TxOutspend> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// Mempool outputs are unspent in on-chain terms
if let Some(mempool) = query.mempool()
&& mempool.get_txs().contains_key(&txid)
{
return Ok(TxOutspend::UNSPENT);
}
// Look up confirmed transaction
let prefix = TxidPrefix::from(&txid);
let indexer = query.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
// Calculate txoutindex
let first_txoutindex = indexer
.vecs
.tx
.txindex_to_first_txoutindex
.read_once(txindex)?;
let txoutindex = first_txoutindex + vout;
// Look up spend status
let computer = query.computer();
let txinindex = computer
.stateful
.txoutindex_to_txinindex
.read_once(txoutindex)?;
if txinindex == TxInIndex::UNSPENT {
return Ok(TxOutspend::UNSPENT);
}
get_outspend_details(txinindex, query)
}
/// Get the spend status of all outputs in a transaction
pub fn get_tx_outspends(TxidPath { txid }: TxidPath, query: &Query) -> Result<Vec<TxOutspend>> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// Mempool outputs are unspent in on-chain terms
if let Some(mempool) = query.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]);
}
// Look up confirmed transaction
let prefix = TxidPrefix::from(&txid);
let indexer = query.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
// Get output range
let first_txoutindex = indexer
.vecs
.tx
.txindex_to_first_txoutindex
.read_once(txindex)?;
let next_first_txoutindex = indexer
.vecs
.tx
.txindex_to_first_txoutindex
.read_once(txindex.incremented())?;
let output_count = usize::from(next_first_txoutindex) - usize::from(first_txoutindex);
// Get spend status for each output
let computer = query.computer();
let mut txoutindex_to_txinindex_iter = computer.stateful.txoutindex_to_txinindex.iter()?;
let mut outspends = Vec::with_capacity(output_count);
for i in 0..output_count {
let txoutindex = first_txoutindex + Vout::from(i);
let txinindex = txoutindex_to_txinindex_iter.get_unwrap(txoutindex);
if txinindex == TxInIndex::UNSPENT {
outspends.push(TxOutspend::UNSPENT);
} else {
outspends.push(get_outspend_details(txinindex, query)?);
}
}
Ok(outspends)
}
/// Get spending transaction details from a txinindex
fn get_outspend_details(txinindex: TxInIndex, query: &Query) -> Result<TxOutspend> {
let indexer = query.indexer();
// Look up spending txindex directly
let spending_txindex = indexer
.vecs
.txin
.txinindex_to_txindex
.read_once(txinindex)?;
// Calculate vin
let spending_first_txinindex = indexer
.vecs
.tx
.txindex_to_first_txinindex
.read_once(spending_txindex)?;
let vin = Vin::from(usize::from(txinindex) - usize::from(spending_first_txinindex));
// Get spending tx details
let spending_txid = indexer
.vecs
.tx
.txindex_to_txid
.read_once(spending_txindex)?;
let spending_height = indexer
.vecs
.tx
.txindex_to_height
.read_once(spending_txindex)?;
let block_hash = indexer
.vecs
.block
.height_to_blockhash
.read_once(spending_height)?;
let block_time = indexer
.vecs
.block
.height_to_timestamp
.read_once(spending_height)?;
Ok(TxOutspend {
spent: true,
txid: Some(spending_txid),
vin: Some(vin),
status: Some(TxStatus {
confirmed: true,
block_height: Some(spending_height),
block_hash: Some(block_hash),
block_time: Some(block_time),
}),
})
}
-46
View File
@@ -1,46 +0,0 @@
use std::str::FromStr;
use brk_error::{Error, Result};
use brk_types::{TxStatus, Txid, TxidPath, TxidPrefix};
use vecdb::GenericStoredVec;
use crate::Query;
pub fn get_transaction_status(TxidPath { txid }: TxidPath, query: &Query) -> Result<TxStatus> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// First check mempool for unconfirmed transactions
if let Some(mempool) = query.mempool()
&& mempool.get_txs().contains_key(&txid)
{
return Ok(TxStatus::UNCONFIRMED);
}
// Look up confirmed transaction by txid prefix
let prefix = TxidPrefix::from(&txid);
let indexer = query.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
// Get block info for status
let height = indexer.vecs.tx.txindex_to_height.read_once(txindex)?;
let block_hash = indexer.vecs.block.height_to_blockhash.read_once(height)?;
let block_time = indexer.vecs.block.height_to_timestamp.read_once(height)?;
Ok(TxStatus {
confirmed: true,
block_height: Some(height),
block_hash: Some(block_hash),
block_time: Some(block_time),
})
}
-166
View File
@@ -1,166 +0,0 @@
use std::{io::Cursor, str::FromStr};
use bitcoin::consensus::Decodable;
use brk_error::{Error, Result};
use brk_types::{
Sats, Transaction, TxIn, TxIndex, TxOut, TxStatus, Txid, TxidPath, TxidPrefix, Vout, Weight,
};
use vecdb::{GenericStoredVec, TypedVecIterator};
use crate::Query;
pub fn get_transaction(TxidPath { txid }: TxidPath, query: &Query) -> Result<Transaction> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// First check mempool for unconfirmed transactions
if let Some(mempool) = query.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(&txid)
{
return Ok(tx_with_hex.tx().clone());
}
// Look up confirmed transaction by txid prefix
let prefix = TxidPrefix::from(&txid);
let indexer = query.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
get_transaction_by_index(txindex, query)
}
pub fn get_transaction_by_index(txindex: TxIndex, query: &Query) -> Result<Transaction> {
let indexer = query.indexer();
let reader = query.reader();
let computer = query.computer();
// Get tx metadata using read_once for single lookups
let txid = indexer.vecs.tx.txindex_to_txid.read_once(txindex)?;
let height = indexer.vecs.tx.txindex_to_height.read_once(txindex)?;
let version = indexer.vecs.tx.txindex_to_txversion.read_once(txindex)?;
let lock_time = indexer.vecs.tx.txindex_to_rawlocktime.read_once(txindex)?;
let total_size = indexer.vecs.tx.txindex_to_total_size.read_once(txindex)?;
let first_txinindex = indexer
.vecs
.tx
.txindex_to_first_txinindex
.read_once(txindex)?;
let position = computer.blks.txindex_to_position.read_once(txindex)?;
// Get block info for status
let block_hash = indexer.vecs.block.height_to_blockhash.read_once(height)?;
let block_time = indexer.vecs.block.height_to_timestamp.read_once(height)?;
// Read and decode the raw transaction from blk file
let buffer = reader.read_raw_bytes(position, *total_size as usize)?;
let mut cursor = Cursor::new(buffer);
let tx = bitcoin::Transaction::consensus_decode(&mut cursor)
.map_err(|_| Error::Str("Failed to decode transaction"))?;
// For iterating through inputs, we need iterators (multiple lookups)
let mut txindex_to_txid_iter = indexer.vecs.tx.txindex_to_txid.iter()?;
let mut txindex_to_first_txoutindex_iter =
indexer.vecs.tx.txindex_to_first_txoutindex.iter()?;
let mut txinindex_to_outpoint_iter = indexer.vecs.txin.txinindex_to_outpoint.iter()?;
let mut txoutindex_to_value_iter = indexer.vecs.txout.txoutindex_to_value.iter()?;
// Build inputs with prevout information
let input: Vec<TxIn> = tx
.input
.iter()
.enumerate()
.map(|(i, txin)| {
let txinindex = first_txinindex + i;
let outpoint = txinindex_to_outpoint_iter.get_unwrap(txinindex);
let is_coinbase = outpoint.is_coinbase();
// Get prevout info if not coinbase
let (prev_txid, prev_vout, prevout) = if is_coinbase {
(Txid::COINBASE, Vout::MAX, None)
} else {
let prev_txindex = outpoint.txindex();
let prev_vout = outpoint.vout();
let prev_txid = txindex_to_txid_iter.get_unwrap(prev_txindex);
// Calculate the txoutindex for the prevout
let prev_first_txoutindex =
txindex_to_first_txoutindex_iter.get_unwrap(prev_txindex);
let prev_txoutindex = prev_first_txoutindex + prev_vout;
// Get the value of the prevout
let prev_value = txoutindex_to_value_iter.get_unwrap(prev_txoutindex);
// We don't have the script_pubkey stored directly, so we need to reconstruct
// For now, we'll get it from the decoded transaction's witness/scriptsig
// which can reveal the prevout script type, but the actual script needs
// to be fetched from the spending tx or reconstructed from address bytes
let prevout = Some(TxOut::from((
bitcoin::ScriptBuf::new(), // Placeholder - would need to reconstruct
prev_value,
)));
(prev_txid, prev_vout, prevout)
};
TxIn {
txid: prev_txid,
vout: prev_vout,
prevout,
script_sig: txin.script_sig.clone(),
script_sig_asm: (),
is_coinbase,
sequence: txin.sequence.0,
inner_redeem_script_asm: (),
}
})
.collect();
// Calculate weight before consuming tx.output
let weight = Weight::from(tx.weight());
// Calculate sigop cost
// Note: Using |_| None means P2SH and SegWit sigops won't be counted accurately
// since we don't provide the prevout scripts. This matches mempool tx behavior.
// For accurate counting, we'd need to reconstruct prevout scripts from indexed data.
let total_sigop_cost = tx.total_sigop_cost(|_| None);
// Build outputs
let output: Vec<TxOut> = tx.output.into_iter().map(TxOut::from).collect();
// Build status
let status = TxStatus {
confirmed: true,
block_height: Some(height),
block_hash: Some(block_hash),
block_time: Some(block_time),
};
let mut transaction = Transaction {
index: Some(txindex),
txid,
version,
lock_time,
total_size: *total_size as usize,
weight,
total_sigop_cost,
fee: Sats::ZERO, // Will be computed below
input,
output,
status,
};
// Compute fee from inputs - outputs
transaction.compute_fee();
Ok(transaction)
}
-52
View File
@@ -1,52 +0,0 @@
use serde::{Deserialize, Deserializer};
use serde_json::Value;
pub fn de_unquote_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<Value> = Option::deserialize(deserializer)?;
if value.is_none() {
return Ok(None);
}
let value = value.unwrap();
if let Some(mut s) = value.as_str().map(|s| s.to_string()) {
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
s = s[1..s.len() - 1].to_string();
}
s.parse::<i64>().map(Some).map_err(serde::de::Error::custom)
} else if let Some(n) = value.as_i64() {
Ok(Some(n))
} else {
Err(serde::de::Error::custom("expected a string or number"))
}
}
pub fn de_unquote_usize<'de, D>(deserializer: D) -> Result<Option<usize>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<Value> = Option::deserialize(deserializer)?;
if value.is_none() {
return Ok(None);
}
let value = value.unwrap();
if let Some(mut s) = value.as_str().map(|s| s.to_string()) {
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
s = s[1..s.len() - 1].to_string();
}
s.parse::<usize>()
.map(Some)
.map_err(serde::de::Error::custom)
} else if let Some(n) = value.as_u64() {
Ok(Some(n as usize))
} else {
Err(serde::de::Error::custom("expected a string or number"))
}
}
+235
View File
@@ -0,0 +1,235 @@
use std::str::FromStr;
use bitcoin::{Network, PublicKey, ScriptBuf};
use brk_error::{Error, Result};
use brk_types::{
Address, AddressBytes, AddressChainStats, AddressHash, AddressIndexOutPoint,
AddressIndexTxIndex, AddressStats, AnyAddressDataIndexEnum, OutputType, Sats, TxIndex,
TxStatus, Txid, TypeIndex, Unit, Utxo, Vout,
};
use vecdb::TypedVecIterator;
use crate::Query;
/// Maximum number of mempool txids to return
const MAX_MEMPOOL_TXIDS: usize = 50;
impl Query {
pub fn address(&self, Address { address }: Address) -> Result<AddressStats> {
let indexer = self.indexer();
let computer = self.computer();
let stores = &indexer.stores;
let script = if let Ok(address) = bitcoin::Address::from_str(&address) {
if !address.is_valid_for_network(Network::Bitcoin) {
return Err(Error::InvalidNetwork);
}
let address = address.assume_checked();
address.script_pubkey()
} else if let Ok(pubkey) = PublicKey::from_str(&address) {
ScriptBuf::new_p2pk(&pubkey)
} else {
return Err(Error::InvalidAddress);
};
let outputtype = OutputType::from(&script);
let Ok(bytes) = AddressBytes::try_from((&script, outputtype)) else {
return Err(Error::Str("Failed to convert the address to bytes"));
};
let addresstype = outputtype;
let hash = AddressHash::from(&bytes);
let Ok(Some(type_index)) = stores
.addresstype_to_addresshash_to_addressindex
.get(addresstype)
.unwrap()
.get(&hash)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownAddress);
};
let any_address_index = computer
.stateful
.any_address_indexes
.get_anyaddressindex_once(outputtype, type_index)?;
let address_data = match any_address_index.to_enum() {
AnyAddressDataIndexEnum::Loaded(index) => computer
.stateful
.addresses_data
.loaded
.iter()?
.get_unwrap(index),
AnyAddressDataIndexEnum::Empty(index) => computer
.stateful
.addresses_data
.empty
.iter()?
.get_unwrap(index)
.into(),
};
Ok(AddressStats {
address: address.into(),
chain_stats: AddressChainStats {
type_index,
funded_txo_count: address_data.funded_txo_count,
funded_txo_sum: address_data.received,
spent_txo_count: address_data.spent_txo_count,
spent_txo_sum: address_data.sent,
tx_count: address_data.tx_count,
},
mempool_stats: self.mempool().map(|mempool| {
mempool
.get_addresses()
.get(&bytes)
.map(|(stats, _)| stats)
.cloned()
.unwrap_or_default()
}),
})
}
pub fn address_txids(
&self,
address: Address,
after_txid: Option<Txid>,
limit: usize,
) -> Result<Vec<Txid>> {
let indexer = self.indexer();
let stores = &indexer.stores;
let (outputtype, type_index) = self.resolve_address(&address)?;
let store = stores
.addresstype_to_addressindex_and_txindex
.get(outputtype)
.unwrap();
let prefix = u32::from(type_index).to_be_bytes();
let after_txindex = if let Some(after_txid) = after_txid {
let txindex = stores
.txidprefix_to_txindex
.get(&after_txid.into())
.map_err(|_| Error::Str("Failed to look up after_txid"))?
.ok_or(Error::Str("after_txid not found"))?
.into_owned();
Some(txindex)
} else {
None
};
let txindices: Vec<TxIndex> = store
.prefix(prefix)
.rev()
.filter(|(key, _): &(AddressIndexTxIndex, Unit)| {
if let Some(after) = after_txindex {
key.txindex() < after
} else {
true
}
})
.take(limit)
.map(|(key, _)| key.txindex())
.collect();
let mut txindex_to_txid_iter = indexer.vecs.tx.txindex_to_txid.iter()?;
let txids: Vec<Txid> = txindices
.into_iter()
.map(|txindex| txindex_to_txid_iter.get_unwrap(txindex))
.collect();
Ok(txids)
}
pub fn address_utxos(&self, address: Address) -> Result<Vec<Utxo>> {
let indexer = self.indexer();
let stores = &indexer.stores;
let vecs = &indexer.vecs;
let (outputtype, type_index) = self.resolve_address(&address)?;
let store = stores
.addresstype_to_addressindex_and_unspentoutpoint
.get(outputtype)
.unwrap();
let prefix = u32::from(type_index).to_be_bytes();
let outpoints: Vec<(TxIndex, Vout)> = store
.prefix(prefix)
.map(|(key, _): (AddressIndexOutPoint, Unit)| (key.txindex(), key.vout()))
.collect();
let mut txindex_to_txid_iter = vecs.tx.txindex_to_txid.iter()?;
let mut txindex_to_height_iter = vecs.tx.txindex_to_height.iter()?;
let mut txindex_to_first_txoutindex_iter = vecs.tx.txindex_to_first_txoutindex.iter()?;
let mut txoutindex_to_value_iter = vecs.txout.txoutindex_to_value.iter()?;
let mut height_to_blockhash_iter = vecs.block.height_to_blockhash.iter()?;
let mut height_to_timestamp_iter = vecs.block.height_to_timestamp.iter()?;
let utxos: Vec<Utxo> = outpoints
.into_iter()
.map(|(txindex, vout)| {
let txid: Txid = txindex_to_txid_iter.get_unwrap(txindex);
let height = txindex_to_height_iter.get_unwrap(txindex);
let first_txoutindex = txindex_to_first_txoutindex_iter.get_unwrap(txindex);
let txoutindex = first_txoutindex + vout;
let value: Sats = txoutindex_to_value_iter.get_unwrap(txoutindex);
let block_hash = height_to_blockhash_iter.get_unwrap(height);
let block_time = height_to_timestamp_iter.get_unwrap(height);
Utxo {
txid,
vout,
status: TxStatus {
confirmed: true,
block_height: Some(height),
block_hash: Some(block_hash),
block_time: Some(block_time),
},
value,
}
})
.collect();
Ok(utxos)
}
pub fn address_mempool_txids(&self, address: Address) -> Result<Vec<Txid>> {
let mempool = self.mempool().ok_or(Error::Str("Mempool not available"))?;
let bytes = AddressBytes::from_str(&address.address)?;
let addresses = mempool.get_addresses();
let txids: Vec<Txid> = addresses
.get(&bytes)
.map(|(_, txids)| txids.iter().take(MAX_MEMPOOL_TXIDS).cloned().collect())
.unwrap_or_default();
Ok(txids)
}
/// Resolve an address string to its output type and type_index
fn resolve_address(&self, address: &Address) -> Result<(OutputType, TypeIndex)> {
let stores = &self.indexer().stores;
let bytes = AddressBytes::from_str(&address.address)?;
let outputtype = OutputType::from(&bytes);
let hash = AddressHash::from(&bytes);
let Ok(Some(type_index)) = stores
.addresstype_to_addresshash_to_addressindex
.get(outputtype)
.unwrap()
.get(&hash)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownAddress);
};
Ok((outputtype, type_index))
}
}
+103
View File
@@ -0,0 +1,103 @@
use brk_error::{Error, Result};
use brk_types::{BlockHash, BlockHashPrefix, BlockInfo, Height, TxIndex};
use vecdb::{AnyVec, GenericStoredVec, VecIndex};
use crate::Query;
const DEFAULT_BLOCK_COUNT: u32 = 10;
impl Query {
pub fn block(&self, hash: &str) -> Result<BlockInfo> {
let height = self.height_by_hash(hash)?;
self.block_by_height(height)
}
pub fn block_by_height(&self, height: Height) -> Result<BlockInfo> {
let indexer = self.indexer();
let max_height = self.max_height();
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let blockhash = indexer.vecs.block.height_to_blockhash.read_once(height)?;
let difficulty = indexer.vecs.block.height_to_difficulty.read_once(height)?;
let timestamp = indexer.vecs.block.height_to_timestamp.read_once(height)?;
let size = indexer.vecs.block.height_to_total_size.read_once(height)?;
let weight = indexer.vecs.block.height_to_weight.read_once(height)?;
let tx_count = self.tx_count_at_height(height, max_height)?;
Ok(BlockInfo {
id: blockhash,
height,
tx_count,
size: *size,
weight,
timestamp,
difficulty: *difficulty,
})
}
pub fn blocks(&self, start_height: Option<Height>) -> Result<Vec<BlockInfo>> {
let max_height = self.height();
let start = start_height.unwrap_or(max_height);
let start = start.min(max_height);
let start_u32: u32 = start.into();
let count = DEFAULT_BLOCK_COUNT.min(start_u32 + 1);
let mut blocks = Vec::with_capacity(count as usize);
for i in 0..count {
let height = Height::from(start_u32 - i);
blocks.push(self.block_by_height(height)?);
}
Ok(blocks)
}
// === Helper methods ===
pub fn height_by_hash(&self, hash: &str) -> Result<Height> {
let indexer = self.indexer();
let blockhash: BlockHash = hash.parse().map_err(|_| Error::Str("Invalid block hash"))?;
let prefix = BlockHashPrefix::from(&blockhash);
indexer
.stores
.blockhashprefix_to_height
.get(&prefix)?
.map(|h| *h)
.ok_or(Error::Str("Block not found"))
}
fn max_height(&self) -> Height {
Height::from(
self.indexer()
.vecs
.block
.height_to_blockhash
.len()
.saturating_sub(1),
)
}
fn tx_count_at_height(&self, height: Height, max_height: Height) -> Result<u32> {
let indexer = self.indexer();
let computer = self.computer();
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = if height < max_height {
indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())?
} else {
TxIndex::from(computer.indexes.txindex_to_txindex.len())
};
Ok((next_first_txindex.to_usize() - first_txindex.to_usize()) as u32)
}
}
+7
View File
@@ -0,0 +1,7 @@
mod info;
mod raw;
mod status;
mod timestamp;
mod txs;
pub const BLOCK_TXS_PAGE_SIZE: usize = 25;
+35
View File
@@ -0,0 +1,35 @@
use brk_error::{Error, Result};
use brk_types::Height;
use vecdb::{AnyVec, GenericStoredVec};
use crate::Query;
impl Query {
pub fn block_raw(&self, hash: &str) -> Result<Vec<u8>> {
let height = self.height_by_hash(hash)?;
self.block_raw_by_height(height)
}
fn block_raw_by_height(&self, height: Height) -> Result<Vec<u8>> {
let indexer = self.indexer();
let computer = self.computer();
let reader = self.reader();
let max_height = Height::from(
indexer
.vecs
.block
.height_to_blockhash
.len()
.saturating_sub(1),
);
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let position = computer.blks.height_to_position.read_once(height)?;
let size = indexer.vecs.block.height_to_total_size.read_once(height)?;
reader.read_raw_bytes(position, *size as usize)
}
}
+43
View File
@@ -0,0 +1,43 @@
use brk_error::Result;
use brk_types::{BlockStatus, Height};
use vecdb::{AnyVec, GenericStoredVec};
use crate::Query;
impl Query {
pub fn block_status(&self, hash: &str) -> Result<BlockStatus> {
let height = self.height_by_hash(hash)?;
self.block_status_by_height(height)
}
fn block_status_by_height(&self, height: Height) -> Result<BlockStatus> {
let indexer = self.indexer();
let max_height = Height::from(
indexer
.vecs
.block
.height_to_blockhash
.len()
.saturating_sub(1),
);
if height > max_height {
return Ok(BlockStatus::not_in_best_chain());
}
let next_best = if height < max_height {
Some(
indexer
.vecs
.block
.height_to_blockhash
.read_once(height.incremented())?,
)
} else {
None
};
Ok(BlockStatus::in_best_chain(height, next_best))
}
}
@@ -0,0 +1,81 @@
use brk_error::{Error, Result};
use brk_types::{BlockTimestamp, Date, DateIndex, Height, Timestamp};
use jiff::Timestamp as JiffTimestamp;
use vecdb::{GenericStoredVec, TypedVecIterator};
use crate::Query;
impl Query {
pub fn block_by_timestamp(&self, timestamp: Timestamp) -> Result<BlockTimestamp> {
let indexer = self.indexer();
let computer = self.computer();
let max_height = self.height();
let max_height_usize: usize = max_height.into();
if max_height_usize == 0 {
return Err(Error::Str("No blocks indexed"));
}
let target = timestamp;
let date = Date::from(target);
let dateindex = DateIndex::try_from(date).unwrap_or_default();
// Get first height of the target date
let first_height_of_day = computer
.indexes
.dateindex_to_first_height
.read_once(dateindex)
.unwrap_or(Height::from(0usize));
let start: usize = usize::from(first_height_of_day).min(max_height_usize);
// Use iterator for efficient sequential access
let mut timestamp_iter = indexer.vecs.block.height_to_timestamp.iter()?;
// Search forward from start to find the last block <= target timestamp
let mut best_height = start;
let mut best_ts = timestamp_iter.get_unwrap(Height::from(start));
for h in (start + 1)..=max_height_usize {
let height = Height::from(h);
let block_ts = timestamp_iter.get_unwrap(height);
if block_ts <= target {
best_height = h;
best_ts = block_ts;
} else {
break;
}
}
// Check one block before start in case we need to go backward
if start > 0 && best_ts > target {
let prev_height = Height::from(start - 1);
let prev_ts = timestamp_iter.get_unwrap(prev_height);
if prev_ts <= target {
best_height = start - 1;
best_ts = prev_ts;
}
}
let height = Height::from(best_height);
let blockhash = indexer
.vecs
.block
.height_to_blockhash
.iter()?
.get_unwrap(height);
// Convert timestamp to ISO 8601 format
let ts_secs: i64 = (*best_ts).into();
let iso_timestamp = JiffTimestamp::from_second(ts_secs)
.map(|t| t.to_string())
.unwrap_or_else(|_| best_ts.to_string());
Ok(BlockTimestamp {
height,
hash: blockhash,
timestamp: iso_timestamp,
})
}
}
+128
View File
@@ -0,0 +1,128 @@
use brk_error::{Error, Result};
use brk_types::{Height, Transaction, TxIndex, Txid};
use vecdb::{AnyVec, GenericStoredVec, TypedVecIterator};
use super::BLOCK_TXS_PAGE_SIZE;
use crate::Query;
impl Query {
pub fn block_txids(&self, hash: &str) -> Result<Vec<Txid>> {
let height = self.height_by_hash(hash)?;
self.block_txids_by_height(height)
}
pub fn block_txs(&self, hash: &str, start_index: usize) -> Result<Vec<Transaction>> {
let height = self.height_by_hash(hash)?;
self.block_txs_by_height(height, start_index)
}
pub fn block_txid_at_index(&self, hash: &str, index: usize) -> Result<Txid> {
let height = self.height_by_hash(hash)?;
self.block_txid_at_index_by_height(height, index)
}
// === Helper methods ===
fn block_txids_by_height(&self, height: Height) -> Result<Vec<Txid>> {
let indexer = self.indexer();
let max_height = self.height();
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())
.unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len()));
let first: usize = first_txindex.into();
let next: usize = next_first_txindex.into();
let count = next - first;
let txids: Vec<Txid> = indexer
.vecs
.tx
.txindex_to_txid
.iter()?
.skip(first)
.take(count)
.collect();
Ok(txids)
}
fn block_txs_by_height(
&self,
height: Height,
start_index: usize,
) -> Result<Vec<Transaction>> {
let indexer = self.indexer();
let max_height = self.height();
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())
.unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len()));
let first: usize = first_txindex.into();
let next: usize = next_first_txindex.into();
let tx_count = next - first;
if start_index >= tx_count {
return Ok(Vec::new());
}
let end_index = (start_index + BLOCK_TXS_PAGE_SIZE).min(tx_count);
let count = end_index - start_index;
let mut txs = Vec::with_capacity(count);
for i in start_index..end_index {
let txindex = TxIndex::from(first + i);
let tx = self.transaction_by_index(txindex)?;
txs.push(tx);
}
Ok(txs)
}
fn block_txid_at_index_by_height(&self, height: Height, index: usize) -> Result<Txid> {
let indexer = self.indexer();
let max_height = self.height();
if height > max_height {
return Err(Error::Str("Block height out of range"));
}
let first_txindex = indexer.vecs.tx.height_to_first_txindex.read_once(height)?;
let next_first_txindex = indexer
.vecs
.tx
.height_to_first_txindex
.read_once(height.incremented())
.unwrap_or_else(|_| TxIndex::from(indexer.vecs.tx.txindex_to_txid.len()));
let first: usize = first_txindex.into();
let next: usize = next_first_txindex.into();
let tx_count = next - first;
if index >= tx_count {
return Err(Error::Str("Transaction index out of range"));
}
let txindex = TxIndex::from(first + index);
let txid = indexer.vecs.tx.txindex_to_txid.iter()?.get_unwrap(txindex);
Ok(txid)
}
}
+38
View File
@@ -0,0 +1,38 @@
use brk_error::{Error, Result};
use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid};
use crate::Query;
impl Query {
pub fn mempool_info(&self) -> Result<MempoolInfo> {
let mempool = self.mempool().ok_or(Error::Str("Mempool not available"))?;
Ok(mempool.get_info())
}
pub fn mempool_txids(&self) -> Result<Vec<Txid>> {
let mempool = self.mempool().ok_or(Error::Str("Mempool not available"))?;
let txs = mempool.get_txs();
Ok(txs.keys().cloned().collect())
}
pub fn recommended_fees(&self) -> Result<RecommendedFees> {
self.mempool()
.map(|mempool| mempool.get_fees())
.ok_or(Error::MempoolNotAvailable)
}
pub fn mempool_blocks(&self) -> Result<Vec<MempoolBlock>> {
let mempool = self.mempool().ok_or(Error::Str("Mempool not available"))?;
let block_stats = mempool.get_block_stats();
let blocks = block_stats
.into_iter()
.map(|stats| {
MempoolBlock::new(stats.tx_count, stats.total_vsize, stats.total_fee, stats.fee_range)
})
.collect();
Ok(blocks)
}
}
@@ -0,0 +1,43 @@
use brk_error::Result;
use brk_types::{BlockFeeRatesEntry, FeeRatePercentiles, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
impl Query {
pub fn block_fee_rates(&self, time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
let computer = self.computer();
let current_height = self.height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
let vecs = &computer.chain.indexes_to_fee_rate.dateindex;
let mut min = vecs.unwrap_min().iter();
let mut pct10 = vecs.unwrap_pct10().iter();
let mut pct25 = vecs.unwrap_pct25().iter();
let mut median = vecs.unwrap_median().iter();
let mut pct75 = vecs.unwrap_pct75().iter();
let mut pct90 = vecs.unwrap_pct90().iter();
let mut max = vecs.unwrap_max().iter();
Ok(iter.collect(|di, ts, h| {
Some(BlockFeeRatesEntry {
avg_height: h,
timestamp: ts,
percentiles: FeeRatePercentiles::new(
min.get(di).unwrap_or_default(),
pct10.get(di).unwrap_or_default(),
pct25.get(di).unwrap_or_default(),
median.get(di).unwrap_or_default(),
pct75.get(di).unwrap_or_default(),
pct90.get(di).unwrap_or_default(),
max.get(di).unwrap_or_default(),
),
})
}))
}
}
@@ -0,0 +1,34 @@
use brk_error::Result;
use brk_types::{BlockFeesEntry, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
impl Query {
pub fn block_fees(&self, time_period: TimePeriod) -> Result<Vec<BlockFeesEntry>> {
let computer = self.computer();
let current_height = self.height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
let mut fees = computer
.chain
.indexes_to_fee
.sats
.dateindex
.unwrap_average()
.iter();
Ok(iter.collect(|di, ts, h| {
fees.get(di).map(|fee| BlockFeesEntry {
avg_height: h,
timestamp: ts,
avg_fees: fee,
})
}))
}
}
@@ -0,0 +1,35 @@
use brk_error::Result;
use brk_types::{BlockRewardsEntry, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
impl Query {
pub fn block_rewards(&self, time_period: TimePeriod) -> Result<Vec<BlockRewardsEntry>> {
let computer = self.computer();
let current_height = self.height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
// coinbase = subsidy + fees
let mut rewards = computer
.chain
.indexes_to_coinbase
.sats
.dateindex
.unwrap_average()
.iter();
Ok(iter.collect(|di, ts, h| {
rewards.get(di).map(|reward| BlockRewardsEntry {
avg_height: h.into(),
timestamp: *ts,
avg_rewards: *reward,
})
}))
}
}
@@ -0,0 +1,61 @@
use brk_error::Result;
use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod};
use vecdb::{IterableVec, VecIndex};
use super::dateindex_iter::DateIndexIter;
use crate::Query;
impl Query {
pub fn block_sizes_weights(&self, time_period: TimePeriod) -> Result<BlockSizesWeights> {
let computer = self.computer();
let current_height = self.height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = DateIndexIter::new(computer, start, current_height.to_usize());
let mut sizes_vec = computer
.chain
.indexes_to_block_size
.dateindex
.unwrap_average()
.iter();
let mut weights_vec = computer
.chain
.indexes_to_block_weight
.dateindex
.unwrap_average()
.iter();
let entries: Vec<_> = iter.collect(|di, ts, h| {
let size = sizes_vec.get(di).map(|s| *s);
let weight = weights_vec.get(di).map(|w| *w);
Some((h.into(), (*ts), size, weight))
});
let sizes = entries
.iter()
.filter_map(|(h, ts, size, _)| {
size.map(|s| BlockSizeEntry {
avg_height: *h,
timestamp: *ts,
avg_size: s,
})
})
.collect();
let weights = entries
.iter()
.filter_map(|(h, ts, _, weight)| {
weight.map(|w| BlockWeightEntry {
avg_height: *h,
timestamp: *ts,
avg_weight: w,
})
})
.collect();
Ok(BlockSizesWeights { sizes, weights })
}
}
@@ -39,7 +39,11 @@ impl<'a> DateIndexIter<'a> {
where
F: FnMut(DateIndex, Timestamp, Height) -> Option<T>,
{
let total = self.end_di.to_usize().saturating_sub(self.start_di.to_usize()) + 1;
let total = self
.end_di
.to_usize()
.saturating_sub(self.start_di.to_usize())
+ 1;
let mut timestamps = self
.computer
.chain
@@ -54,10 +58,10 @@ impl<'a> DateIndexIter<'a> {
while i <= self.end_di.to_usize() {
let di = DateIndex::from(i);
if let (Some(ts), Some(h)) = (timestamps.get(di), heights.get(di)) {
if let Some(entry) = transform(di, ts, h) {
entries.push(entry);
}
if let (Some(ts), Some(h)) = (timestamps.get(di), heights.get(di))
&& let Some(entry) = transform(di, ts, h)
{
entries.push(entry);
}
i += self.step;
}
@@ -0,0 +1,121 @@
use std::time::{SystemTime, UNIX_EPOCH};
use brk_error::Result;
use brk_types::{DifficultyAdjustment, DifficultyEpoch, Height};
use vecdb::GenericStoredVec;
use crate::Query;
/// Blocks per difficulty epoch (2 weeks target)
const BLOCKS_PER_EPOCH: u32 = 2016;
/// Target block time in seconds (10 minutes)
const TARGET_BLOCK_TIME: u64 = 600;
impl Query {
pub fn difficulty_adjustment(&self) -> Result<DifficultyAdjustment> {
let indexer = self.indexer();
let computer = self.computer();
let current_height = self.height();
let current_height_u32: u32 = current_height.into();
// Get current epoch
let current_epoch = computer
.indexes
.height_to_difficultyepoch
.read_once(current_height)?;
let current_epoch_usize: usize = current_epoch.into();
// Get epoch start height
let epoch_start_height = computer
.indexes
.difficultyepoch_to_first_height
.read_once(current_epoch)?;
let epoch_start_u32: u32 = epoch_start_height.into();
// Calculate epoch progress
let next_retarget_height = epoch_start_u32 + BLOCKS_PER_EPOCH;
let blocks_into_epoch = current_height_u32 - epoch_start_u32;
let remaining_blocks = next_retarget_height - current_height_u32;
let progress_percent = (blocks_into_epoch as f64 / BLOCKS_PER_EPOCH as f64) * 100.0;
// Get timestamps using difficultyepoch_to_timestamp for epoch start
let epoch_start_timestamp = computer
.chain
.difficultyepoch_to_timestamp
.read_once(current_epoch)?;
let current_timestamp = indexer
.vecs
.block
.height_to_timestamp
.read_once(current_height)?;
// Calculate average block time in current epoch
let elapsed_time = (*current_timestamp - *epoch_start_timestamp) as u64;
let time_avg = if blocks_into_epoch > 0 {
elapsed_time / blocks_into_epoch as u64
} else {
TARGET_BLOCK_TIME
};
// Estimate remaining time and retarget date
let remaining_time = remaining_blocks as u64 * time_avg;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(*current_timestamp as u64);
let estimated_retarget_date = now + remaining_time;
// Calculate expected vs actual time for difficulty change estimate
let expected_time = blocks_into_epoch as u64 * TARGET_BLOCK_TIME;
let difficulty_change = if elapsed_time > 0 && blocks_into_epoch > 0 {
((expected_time as f64 / elapsed_time as f64) - 1.0) * 100.0
} else {
0.0
};
// Time offset from expected schedule
let time_offset = expected_time as i64 - elapsed_time as i64;
// Calculate previous retarget using stored difficulty values
let previous_retarget = if current_epoch_usize > 0 {
let prev_epoch = DifficultyEpoch::from(current_epoch_usize - 1);
let prev_epoch_start = computer
.indexes
.difficultyepoch_to_first_height
.read_once(prev_epoch)?;
let prev_difficulty = indexer
.vecs
.block
.height_to_difficulty
.read_once(prev_epoch_start)?;
let curr_difficulty = indexer
.vecs
.block
.height_to_difficulty
.read_once(epoch_start_height)?;
if *prev_difficulty > 0.0 {
((*curr_difficulty / *prev_difficulty) - 1.0) * 100.0
} else {
0.0
}
} else {
0.0
};
Ok(DifficultyAdjustment {
progress_percent,
difficulty_change,
estimated_retarget_date,
remaining_blocks,
remaining_time,
previous_retarget,
next_retarget_height: Height::from(next_retarget_height),
time_avg,
adjusted_time_avg: time_avg,
time_offset,
})
}
}
@@ -0,0 +1,26 @@
use brk_error::Result;
use brk_types::{DifficultyAdjustmentEntry, TimePeriod};
use vecdb::VecIndex;
use super::epochs::iter_difficulty_epochs;
use crate::Query;
impl Query {
pub fn difficulty_adjustments(
&self,
time_period: Option<TimePeriod>,
) -> Result<Vec<DifficultyAdjustmentEntry>> {
let current_height = self.height();
let end = current_height.to_usize();
let start = match time_period {
Some(tp) => end.saturating_sub(tp.block_count()),
None => 0,
};
let mut entries = iter_difficulty_epochs(self.computer(), start, end);
// Return in reverse chronological order (newest first)
entries.reverse();
Ok(entries)
}
}
@@ -0,0 +1,100 @@
use brk_error::Result;
use brk_types::{DateIndex, DifficultyEntry, HashrateEntry, HashrateSummary, Height, TimePeriod};
use vecdb::{GenericStoredVec, IterableVec, VecIndex};
use super::epochs::iter_difficulty_epochs;
use crate::Query;
impl Query {
pub fn hashrate(&self, time_period: Option<TimePeriod>) -> Result<HashrateSummary> {
let indexer = self.indexer();
let computer = self.computer();
let current_height = self.height();
// Get current difficulty
let current_difficulty = *indexer
.vecs
.block
.height_to_difficulty
.read_once(current_height)?;
// Get current hashrate
let current_dateindex = computer
.indexes
.height_to_dateindex
.read_once(current_height)?;
let current_hashrate = *computer
.chain
.indexes_to_hash_rate
.dateindex
.unwrap_last()
.read_once(current_dateindex)? as u128;
// Calculate start height based on time period
let end = current_height.to_usize();
let start = match time_period {
Some(tp) => end.saturating_sub(tp.block_count()),
None => 0,
};
// Get hashrate entries using iterators for efficiency
let start_dateindex = computer
.indexes
.height_to_dateindex
.read_once(Height::from(start))?;
let end_dateindex = current_dateindex;
// Sample at regular intervals to avoid too many data points
let total_days = end_dateindex
.to_usize()
.saturating_sub(start_dateindex.to_usize())
+ 1;
let step = (total_days / 200).max(1); // Max ~200 data points
// Create iterators for the loop
let mut hashrate_iter = computer
.chain
.indexes_to_hash_rate
.dateindex
.unwrap_last()
.iter();
let mut timestamp_iter = computer
.chain
.timeindexes_to_timestamp
.dateindex_extra
.unwrap_first()
.iter();
let mut hashrates = Vec::with_capacity(total_days / step + 1);
let mut di = start_dateindex.to_usize();
while di <= end_dateindex.to_usize() {
let dateindex = DateIndex::from(di);
if let (Some(hr), Some(timestamp)) =
(hashrate_iter.get(dateindex), timestamp_iter.get(dateindex))
{
hashrates.push(HashrateEntry {
timestamp,
avg_hashrate: (*hr) as u128,
});
}
di += step;
}
// Get difficulty adjustments within the period
let difficulty: Vec<DifficultyEntry> = iter_difficulty_epochs(computer, start, end)
.into_iter()
.map(|e| DifficultyEntry {
timestamp: e.timestamp,
difficulty: e.difficulty,
height: e.height,
})
.collect();
Ok(HashrateSummary {
hashrates,
difficulty,
current_hashrate,
current_difficulty,
})
}
}
+11
View File
@@ -0,0 +1,11 @@
mod block_fee_rates;
mod block_fees;
mod block_rewards;
mod block_sizes;
mod dateindex_iter;
mod difficulty;
mod difficulty_adjustments;
mod epochs;
mod hashrate;
mod pools;
mod reward_stats;
+171
View File
@@ -0,0 +1,171 @@
use brk_error::{Error, Result};
use brk_types::{
Height, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo, PoolInfo, PoolSlug,
PoolStats, PoolsSummary, TimePeriod, pools,
};
use vecdb::{AnyVec, IterableVec, VecIndex};
use crate::Query;
impl Query {
pub fn mining_pools(&self, time_period: TimePeriod) -> Result<PoolsSummary> {
let computer = self.computer();
let current_height = self.height();
let end = current_height.to_usize();
// No blocks indexed yet
if computer.pools.height_to_pool.len() == 0 {
return Ok(PoolsSummary {
pools: vec![],
block_count: 0,
last_estimated_hashrate: 0,
});
}
// Calculate start height based on time period
let start = end.saturating_sub(time_period.block_count());
let pools = pools();
let mut pool_data: Vec<(&'static brk_types::Pool, u32)> = Vec::new();
// For each pool, get cumulative count at end and start, subtract to get range count
for (pool_id, pool_vecs) in &computer.pools.vecs {
let mut cumulative = pool_vecs
.indexes_to_blocks_mined
.height_extra
.unwrap_cumulative()
.iter();
let count_at_end: u32 = *cumulative.get(current_height).unwrap_or_default();
let count_at_start: u32 = if start == 0 {
0
} else {
*cumulative.get(Height::from(start - 1)).unwrap_or_default()
};
let block_count = count_at_end.saturating_sub(count_at_start);
// Only include pools that mined at least one block in the period
if block_count > 0 {
pool_data.push((pools.get(*pool_id), block_count));
}
}
// Sort by block count descending
pool_data.sort_by(|a, b| b.1.cmp(&a.1));
let total_blocks: u32 = pool_data.iter().map(|(_, count)| count).sum();
// Build stats with ranks
let pool_stats: Vec<PoolStats> = pool_data
.into_iter()
.enumerate()
.map(|(idx, (pool, block_count))| {
let share = if total_blocks > 0 {
block_count as f64 / total_blocks as f64
} else {
0.0
};
PoolStats::new(pool, block_count, (idx + 1) as u32, share)
})
.collect();
// TODO: Calculate actual hashrate from difficulty
let last_estimated_hashrate = 0u128;
Ok(PoolsSummary {
pools: pool_stats,
block_count: total_blocks,
last_estimated_hashrate,
})
}
pub fn all_pools(&self) -> Vec<PoolInfo> {
pools().iter().map(PoolInfo::from).collect()
}
pub fn pool_detail(&self, slug: PoolSlug) -> Result<PoolDetail> {
let computer = self.computer();
let current_height = self.height();
let end = current_height.to_usize();
let pools_list = pools();
let pool = pools_list.get(slug);
// Get pool vecs for this specific pool
let pool_vecs = computer
.pools
.vecs
.get(&slug)
.ok_or_else(|| Error::Str("Pool data not found"))?;
let mut cumulative = pool_vecs
.indexes_to_blocks_mined
.height_extra
.unwrap_cumulative()
.iter();
// Get total blocks (all time)
let total_all: u32 = *cumulative.get(current_height).unwrap_or_default();
// Get blocks for 24h (144 blocks)
let start_24h = end.saturating_sub(144);
let count_before_24h: u32 = if start_24h == 0 {
0
} else {
*cumulative
.get(Height::from(start_24h - 1))
.unwrap_or_default()
};
let total_24h = total_all.saturating_sub(count_before_24h);
// Get blocks for 1w (1008 blocks)
let start_1w = end.saturating_sub(1008);
let count_before_1w: u32 = if start_1w == 0 {
0
} else {
*cumulative
.get(Height::from(start_1w - 1))
.unwrap_or_default()
};
let total_1w = total_all.saturating_sub(count_before_1w);
// Calculate total network blocks for share calculation
let network_blocks_all = (end + 1) as u32;
let network_blocks_24h = (end - start_24h + 1) as u32;
let network_blocks_1w = (end - start_1w + 1) as u32;
let share_all = if network_blocks_all > 0 {
total_all as f64 / network_blocks_all as f64
} else {
0.0
};
let share_24h = if network_blocks_24h > 0 {
total_24h as f64 / network_blocks_24h as f64
} else {
0.0
};
let share_1w = if network_blocks_1w > 0 {
total_1w as f64 / network_blocks_1w as f64
} else {
0.0
};
Ok(PoolDetail {
pool: PoolDetailInfo::from(pool),
block_count: PoolBlockCounts {
all: total_all,
day: total_24h,
week: total_1w,
},
block_share: PoolBlockShares {
all: share_all,
day: share_24h,
week: share_1w,
},
estimated_hashrate: 0, // TODO: Calculate from share and network hashrate
reported_hashrate: None,
})
}
}
@@ -0,0 +1,66 @@
use brk_error::Result;
use brk_types::{Height, RewardStats, Sats};
use vecdb::{IterableVec, VecIndex};
use crate::Query;
impl Query {
pub fn reward_stats(&self, block_count: usize) -> Result<RewardStats> {
let computer = self.computer();
let current_height = self.height();
let end_block = current_height;
let start_block = Height::from(current_height.to_usize().saturating_sub(block_count - 1));
let mut coinbase_iter = computer
.chain
.indexes_to_coinbase
.sats
.height
.as_ref()
.unwrap()
.iter();
let mut fee_iter = computer
.chain
.indexes_to_fee
.sats
.height
.unwrap_sum()
.iter();
let mut tx_count_iter = computer
.chain
.indexes_to_tx_count
.height
.as_ref()
.unwrap()
.iter();
let mut total_reward = Sats::ZERO;
let mut total_fee = Sats::ZERO;
let mut total_tx: u64 = 0;
for height in start_block.to_usize()..=end_block.to_usize() {
let h = Height::from(height);
if let Some(coinbase) = coinbase_iter.get(h) {
total_reward += coinbase;
}
if let Some(fee) = fee_iter.get(h) {
total_fee += fee;
}
if let Some(tx_count) = tx_count_iter.get(h) {
total_tx += *tx_count;
}
}
Ok(RewardStats {
start_block,
end_block,
total_reward,
total_fee,
total_tx,
})
}
}
+11
View File
@@ -0,0 +1,11 @@
//! Query implementation modules.
//!
//! Each module extends `Query` with domain-specific methods using `impl Query` blocks.
mod address;
mod block;
mod mempool;
mod mining;
mod transaction;
pub use block::BLOCK_TXS_PAGE_SIZE;
+405
View File
@@ -0,0 +1,405 @@
use std::{io::Cursor, str::FromStr};
use bitcoin::{consensus::Decodable, hex::DisplayHex};
use brk_error::{Error, Result};
use brk_types::{
Sats, Transaction, TxIn, TxInIndex, TxIndex, TxOut, TxOutspend, TxStatus, Txid, TxidPath,
TxidPrefix, Vin, Vout, Weight,
};
use vecdb::{GenericStoredVec, TypedVecIterator};
use crate::Query;
impl Query {
pub fn transaction(&self, TxidPath { txid }: TxidPath) -> Result<Transaction> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// First check mempool for unconfirmed transactions
if let Some(mempool) = self.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(&txid)
{
return Ok(tx_with_hex.tx().clone());
}
// Look up confirmed transaction by txid prefix
let prefix = TxidPrefix::from(&txid);
let indexer = self.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
self.transaction_by_index(txindex)
}
pub fn transaction_status(&self, TxidPath { txid }: TxidPath) -> Result<TxStatus> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// First check mempool for unconfirmed transactions
if let Some(mempool) = self.mempool()
&& mempool.get_txs().contains_key(&txid)
{
return Ok(TxStatus::UNCONFIRMED);
}
// Look up confirmed transaction by txid prefix
let prefix = TxidPrefix::from(&txid);
let indexer = self.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
// Get block info for status
let height = indexer.vecs.tx.txindex_to_height.read_once(txindex)?;
let block_hash = indexer.vecs.block.height_to_blockhash.read_once(height)?;
let block_time = indexer.vecs.block.height_to_timestamp.read_once(height)?;
Ok(TxStatus {
confirmed: true,
block_height: Some(height),
block_hash: Some(block_hash),
block_time: Some(block_time),
})
}
pub fn transaction_hex(&self, TxidPath { txid }: TxidPath) -> Result<String> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// First check mempool for unconfirmed transactions
if let Some(mempool) = self.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(&txid)
{
return Ok(tx_with_hex.hex().to_string());
}
// Look up confirmed transaction by txid prefix
let prefix = TxidPrefix::from(&txid);
let indexer = self.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
self.transaction_hex_by_index(txindex)
}
pub fn outspend(&self, TxidPath { txid }: TxidPath, vout: Vout) -> Result<TxOutspend> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// Mempool outputs are unspent in on-chain terms
if let Some(mempool) = self.mempool()
&& mempool.get_txs().contains_key(&txid)
{
return Ok(TxOutspend::UNSPENT);
}
// Look up confirmed transaction
let prefix = TxidPrefix::from(&txid);
let indexer = self.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
// Calculate txoutindex
let first_txoutindex = indexer
.vecs
.tx
.txindex_to_first_txoutindex
.read_once(txindex)?;
let txoutindex = first_txoutindex + vout;
// Look up spend status
let computer = self.computer();
let txinindex = computer
.stateful
.txoutindex_to_txinindex
.read_once(txoutindex)?;
if txinindex == TxInIndex::UNSPENT {
return Ok(TxOutspend::UNSPENT);
}
self.outspend_details(txinindex)
}
pub fn outspends(&self, TxidPath { txid }: TxidPath) -> Result<Vec<TxOutspend>> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(Error::InvalidTxid);
};
let txid = Txid::from(txid);
// 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]);
}
// Look up confirmed transaction
let prefix = TxidPrefix::from(&txid);
let indexer = self.indexer();
let Ok(Some(txindex)) = indexer
.stores
.txidprefix_to_txindex
.get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return Err(Error::UnknownTxid);
};
// Get output range
let first_txoutindex = indexer
.vecs
.tx
.txindex_to_first_txoutindex
.read_once(txindex)?;
let next_first_txoutindex = indexer
.vecs
.tx
.txindex_to_first_txoutindex
.read_once(txindex.incremented())?;
let output_count = usize::from(next_first_txoutindex) - usize::from(first_txoutindex);
// Get spend status for each output
let computer = self.computer();
let mut txoutindex_to_txinindex_iter = computer.stateful.txoutindex_to_txinindex.iter()?;
let mut outspends = Vec::with_capacity(output_count);
for i in 0..output_count {
let txoutindex = first_txoutindex + Vout::from(i);
let txinindex = txoutindex_to_txinindex_iter.get_unwrap(txoutindex);
if txinindex == TxInIndex::UNSPENT {
outspends.push(TxOutspend::UNSPENT);
} else {
outspends.push(self.outspend_details(txinindex)?);
}
}
Ok(outspends)
}
// === Helper methods ===
pub fn transaction_by_index(&self, txindex: TxIndex) -> Result<Transaction> {
let indexer = self.indexer();
let reader = self.reader();
let computer = self.computer();
// Get tx metadata using read_once for single lookups
let txid = indexer.vecs.tx.txindex_to_txid.read_once(txindex)?;
let height = indexer.vecs.tx.txindex_to_height.read_once(txindex)?;
let version = indexer.vecs.tx.txindex_to_txversion.read_once(txindex)?;
let lock_time = indexer.vecs.tx.txindex_to_rawlocktime.read_once(txindex)?;
let total_size = indexer.vecs.tx.txindex_to_total_size.read_once(txindex)?;
let first_txinindex = indexer
.vecs
.tx
.txindex_to_first_txinindex
.read_once(txindex)?;
let position = computer.blks.txindex_to_position.read_once(txindex)?;
// Get block info for status
let block_hash = indexer.vecs.block.height_to_blockhash.read_once(height)?;
let block_time = indexer.vecs.block.height_to_timestamp.read_once(height)?;
// Read and decode the raw transaction from blk file
let buffer = reader.read_raw_bytes(position, *total_size as usize)?;
let mut cursor = Cursor::new(buffer);
let tx = bitcoin::Transaction::consensus_decode(&mut cursor)
.map_err(|_| Error::Str("Failed to decode transaction"))?;
// For iterating through inputs, we need iterators (multiple lookups)
let mut txindex_to_txid_iter = indexer.vecs.tx.txindex_to_txid.iter()?;
let mut txindex_to_first_txoutindex_iter =
indexer.vecs.tx.txindex_to_first_txoutindex.iter()?;
let mut txinindex_to_outpoint_iter = indexer.vecs.txin.txinindex_to_outpoint.iter()?;
let mut txoutindex_to_value_iter = indexer.vecs.txout.txoutindex_to_value.iter()?;
// Build inputs with prevout information
let input: Vec<TxIn> = tx
.input
.iter()
.enumerate()
.map(|(i, txin)| {
let txinindex = first_txinindex + i;
let outpoint = txinindex_to_outpoint_iter.get_unwrap(txinindex);
let is_coinbase = outpoint.is_coinbase();
// Get prevout info if not coinbase
let (prev_txid, prev_vout, prevout) = if is_coinbase {
(Txid::COINBASE, Vout::MAX, None)
} else {
let prev_txindex = outpoint.txindex();
let prev_vout = outpoint.vout();
let prev_txid = txindex_to_txid_iter.get_unwrap(prev_txindex);
// Calculate the txoutindex for the prevout
let prev_first_txoutindex =
txindex_to_first_txoutindex_iter.get_unwrap(prev_txindex);
let prev_txoutindex = prev_first_txoutindex + prev_vout;
// Get the value of the prevout
let prev_value = txoutindex_to_value_iter.get_unwrap(prev_txoutindex);
let prevout = Some(TxOut::from((
bitcoin::ScriptBuf::new(), // Placeholder - would need to reconstruct
prev_value,
)));
(prev_txid, prev_vout, prevout)
};
TxIn {
txid: prev_txid,
vout: prev_vout,
prevout,
script_sig: txin.script_sig.clone(),
script_sig_asm: (),
is_coinbase,
sequence: txin.sequence.0,
inner_redeem_script_asm: (),
}
})
.collect();
// Calculate weight before consuming tx.output
let weight = Weight::from(tx.weight());
// Calculate sigop cost
let total_sigop_cost = tx.total_sigop_cost(|_| None);
// Build outputs
let output: Vec<TxOut> = tx.output.into_iter().map(TxOut::from).collect();
// Build status
let status = TxStatus {
confirmed: true,
block_height: Some(height),
block_hash: Some(block_hash),
block_time: Some(block_time),
};
let mut transaction = Transaction {
index: Some(txindex),
txid,
version,
lock_time,
total_size: *total_size as usize,
weight,
total_sigop_cost,
fee: Sats::ZERO, // Will be computed below
input,
output,
status,
};
// Compute fee from inputs - outputs
transaction.compute_fee();
Ok(transaction)
}
fn transaction_hex_by_index(&self, txindex: TxIndex) -> Result<String> {
let indexer = self.indexer();
let reader = self.reader();
let computer = self.computer();
let total_size = indexer.vecs.tx.txindex_to_total_size.read_once(txindex)?;
let position = computer.blks.txindex_to_position.read_once(txindex)?;
let buffer = reader.read_raw_bytes(position, *total_size as usize)?;
Ok(buffer.to_lower_hex_string())
}
fn outspend_details(&self, txinindex: TxInIndex) -> Result<TxOutspend> {
let indexer = self.indexer();
// Look up spending txindex directly
let spending_txindex = indexer
.vecs
.txin
.txinindex_to_txindex
.read_once(txinindex)?;
// Calculate vin
let spending_first_txinindex = indexer
.vecs
.tx
.txindex_to_first_txinindex
.read_once(spending_txindex)?;
let vin = Vin::from(usize::from(txinindex) - usize::from(spending_first_txinindex));
// Get spending tx details
let spending_txid = indexer
.vecs
.tx
.txindex_to_txid
.read_once(spending_txindex)?;
let spending_height = indexer
.vecs
.tx
.txindex_to_height
.read_once(spending_txindex)?;
let block_hash = indexer
.vecs
.block
.height_to_blockhash
.read_once(spending_height)?;
let block_time = indexer
.vecs
.block
.height_to_timestamp
.read_once(spending_height)?;
Ok(TxOutspend {
spent: true,
txid: Some(spending_txid),
vin: Some(vin),
status: Some(TxStatus {
confirmed: true,
block_height: Some(spending_height),
block_hash: Some(block_hash),
block_time: Some(block_time),
}),
})
}
}
+66 -237
View File
@@ -9,44 +9,30 @@ use brk_indexer::Indexer;
use brk_mempool::Mempool;
use brk_reader::Reader;
use brk_traversable::TreeNode;
use brk_types::{
Address, AddressStats, BlockInfo, BlockStatus, BlockTimestamp, Format, HashrateSummary,
Height, Index, IndexInfo, Limit, MempoolInfo, Metric, MetricCount, PoolDetail, PoolInfo,
PoolSlug, PoolsSummary, RecommendedFees, TimePeriod, Timestamp, Transaction, TxOutspend,
TxStatus, Txid, TxidPath, Utxo, Vout,
};
use brk_types::{Format, Height, Index, IndexInfo, Limit, Metric, MetricCount};
use vecdb::{AnyExportableVec, AnyStoredVec};
// Infrastructure modules
#[cfg(feature = "tokio")]
mod r#async;
mod chain;
mod deser;
mod output;
mod pagination;
mod params;
mod vecs;
// Query impl blocks (extend Query with domain methods)
mod r#impl;
// Re-exports
#[cfg(feature = "tokio")]
pub use r#async::*;
pub use output::{Output, Value};
pub use pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam};
pub use params::{Params, ParamsDeprec, ParamsOpt};
use vecs::Vecs;
pub use crate::chain::BLOCK_TXS_PAGE_SIZE;
pub use crate::chain::validate_address;
use crate::{
chain::{
get_address, get_address_mempool_txids, get_address_txids, get_address_utxos,
get_all_pools, get_block_by_height, get_block_by_timestamp, get_block_raw,
get_block_status_by_height, get_block_txid_at_index, get_block_txids, get_block_txs,
get_blocks, get_difficulty_adjustment, get_hashrate, get_height_by_hash,
get_mempool_blocks, get_mempool_info, get_mempool_txids, get_mining_pools, get_pool_detail,
get_recommended_fees, get_transaction, get_transaction_hex, get_transaction_status,
get_tx_outspend, get_tx_outspends,
},
vecs::{IndexToVec, MetricToVec},
pub use brk_types::{
DataRange, DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics,
Pagination, PaginationIndex,
};
pub use r#impl::BLOCK_TXS_PAGE_SIZE;
pub use output::{Output, Value};
use crate::vecs::{IndexToVec, MetricToVec};
use vecs::Vecs;
#[derive(Clone)]
pub struct Query(Arc<QueryInner<'static>>);
@@ -79,217 +65,17 @@ impl Query {
}))
}
pub fn get_height(&self) -> Height {
/// Current indexed height
pub fn height(&self) -> Height {
Height::from(self.indexer().vecs.block.height_to_blockhash.stamp())
}
pub fn get_address(&self, address: Address) -> Result<AddressStats> {
get_address(address, self)
}
pub fn get_address_txids(
&self,
address: Address,
after_txid: Option<Txid>,
limit: usize,
) -> Result<Vec<Txid>> {
get_address_txids(address, after_txid, limit, self)
}
pub fn get_address_utxos(&self, address: Address) -> Result<Vec<Utxo>> {
get_address_utxos(address, self)
}
pub fn get_address_mempool_txids(&self, address: Address) -> Result<Vec<Txid>> {
get_address_mempool_txids(address, self)
}
pub fn get_transaction(&self, txid: TxidPath) -> Result<Transaction> {
get_transaction(txid, self)
}
pub fn get_transaction_status(&self, txid: TxidPath) -> Result<TxStatus> {
get_transaction_status(txid, self)
}
pub fn get_transaction_hex(&self, txid: TxidPath) -> Result<String> {
get_transaction_hex(txid, self)
}
pub fn get_tx_outspend(&self, txid: TxidPath, vout: Vout) -> Result<TxOutspend> {
get_tx_outspend(txid, vout, self)
}
pub fn get_tx_outspends(&self, txid: TxidPath) -> Result<Vec<TxOutspend>> {
get_tx_outspends(txid, self)
}
pub fn get_block(&self, hash: &str) -> Result<BlockInfo> {
let height = get_height_by_hash(hash, self)?;
get_block_by_height(height, self)
}
pub fn get_block_by_height(&self, height: Height) -> Result<BlockInfo> {
get_block_by_height(height, self)
}
pub fn get_block_by_timestamp(&self, timestamp: Timestamp) -> Result<BlockTimestamp> {
get_block_by_timestamp(timestamp, self)
}
pub fn get_block_status(&self, hash: &str) -> Result<BlockStatus> {
let height = get_height_by_hash(hash, self)?;
get_block_status_by_height(height, self)
}
pub fn get_blocks(&self, start_height: Option<Height>) -> Result<Vec<BlockInfo>> {
get_blocks(start_height, self)
}
pub fn get_block_txids(&self, hash: &str) -> Result<Vec<Txid>> {
let height = get_height_by_hash(hash, self)?;
get_block_txids(height, self)
}
pub fn get_block_txs(&self, hash: &str, start_index: usize) -> Result<Vec<Transaction>> {
let height = get_height_by_hash(hash, self)?;
get_block_txs(height, start_index, self)
}
pub fn get_block_txid_at_index(&self, hash: &str, index: usize) -> Result<Txid> {
let height = get_height_by_hash(hash, self)?;
get_block_txid_at_index(height, index, self)
}
pub fn get_block_raw(&self, hash: &str) -> Result<Vec<u8>> {
let height = get_height_by_hash(hash, self)?;
get_block_raw(height, self)
}
pub fn get_mempool_info(&self) -> Result<MempoolInfo> {
get_mempool_info(self)
}
pub fn get_mempool_txids(&self) -> Result<Vec<Txid>> {
get_mempool_txids(self)
}
pub fn get_recommended_fees(&self) -> Result<RecommendedFees> {
get_recommended_fees(self)
}
pub fn get_mempool_blocks(&self) -> Result<Vec<brk_types::MempoolBlock>> {
get_mempool_blocks(self)
}
pub fn get_difficulty_adjustment(&self) -> Result<brk_types::DifficultyAdjustment> {
get_difficulty_adjustment(self)
}
pub fn get_mining_pools(&self, time_period: TimePeriod) -> Result<PoolsSummary> {
get_mining_pools(time_period, self)
}
pub fn get_all_pools(&self) -> Vec<PoolInfo> {
get_all_pools()
}
pub fn get_pool_detail(&self, slug: PoolSlug) -> Result<PoolDetail> {
get_pool_detail(slug, self)
}
pub fn get_hashrate(&self, time_period: Option<TimePeriod>) -> Result<HashrateSummary> {
get_hashrate(time_period, self)
}
pub fn get_difficulty_adjustments(
&self,
time_period: Option<TimePeriod>,
) -> Result<Vec<brk_types::DifficultyAdjustmentEntry>> {
chain::get_difficulty_adjustments(time_period, self)
}
pub fn get_block_fees(&self, time_period: TimePeriod) -> Result<Vec<brk_types::BlockFeesEntry>> {
chain::get_block_fees(time_period, self)
}
pub fn get_block_rewards(
&self,
time_period: TimePeriod,
) -> Result<Vec<brk_types::BlockRewardsEntry>> {
chain::get_block_rewards(time_period, self)
}
pub fn get_block_fee_rates(
&self,
time_period: TimePeriod,
) -> Result<Vec<brk_types::BlockFeeRatesEntry>> {
chain::get_block_fee_rates(time_period, self)
}
pub fn get_block_sizes_weights(
&self,
time_period: TimePeriod,
) -> Result<brk_types::BlockSizesWeights> {
chain::get_block_sizes_weights(time_period, self)
}
pub fn get_reward_stats(&self, block_count: usize) -> Result<brk_types::RewardStats> {
chain::get_reward_stats(block_count, self)
}
// === Metrics methods ===
pub fn match_metric(&self, metric: &Metric, limit: Limit) -> Vec<&'static str> {
self.vecs().matches(metric, limit)
}
pub fn search_metric_with_index(
&self,
metric: &str,
index: Index,
// params: &Params,
) -> Result<Vec<(String, &&dyn AnyExportableVec)>> {
todo!();
// let all_metrics = &self.vecs.metrics;
// let metrics = &params.metrics;
// let index = params.index;
// let ids_to_vec = self
// .vecs
// .index_to_metric_to_vec
// .get(&index)
// .ok_or(Error::String(format!(
// "Index \"{}\" isn't a valid index",
// index
// )))?;
// metrics
// .iter()
// .map(|metric| {
// let vec = ids_to_vec.get(metric.as_str()).ok_or_else(|| {
// let matches: Vec<&str> = MATCHER.with(|matcher| {
// let matcher = matcher.borrow();
// let mut scored: Vec<(&str, i64)> = all_metrics
// .iter()
// .filter_map(|m| matcher.fuzzy_match(m, metric).map(|s| (*m, s)))
// .collect();
// scored.sort_unstable_by_key(|&(_, s)| std::cmp::Reverse(s));
// scored.into_iter().take(5).map(|(m, _)| m).collect()
// });
// let mut message = format!("No vec \"{metric}\" for index \"{index}\".\n");
// if !matches.is_empty() {
// message += &format!("\nDid you mean: {matches:?}\n");
// }
// Error::String(message)
// });
// vec.map(|vec| (metric.clone(), vec))
// })
// .collect::<Result<Vec<_>>>()
}
fn columns_to_csv(
columns: &[&&dyn AnyExportableVec],
from: Option<i64>,
@@ -336,7 +122,7 @@ impl Query {
pub fn format(
&self,
metrics: Vec<&&dyn AnyExportableVec>,
params: &ParamsOpt,
params: &DataRangeFormat,
) -> Result<Output> {
let from = params.from().map(|from| {
metrics
@@ -381,9 +167,50 @@ impl Query {
})
}
pub fn search_and_format(&self, params: Params) -> Result<Output> {
todo!()
// self.format(self.search(&params)?, &params.rest)
/// Search for vecs matching the given metrics and index
pub fn search(&self, params: &MetricSelection) -> Vec<&'static dyn AnyExportableVec> {
params
.metrics
.iter()
.filter_map(|metric| self.vecs().get(metric, params.index))
.collect()
}
/// Calculate total weight of the vecs for the given range
pub fn weight(vecs: &[&dyn AnyExportableVec], from: Option<i64>, to: Option<i64>) -> usize {
vecs.iter().map(|v| v.range_weight(from, to)).sum()
}
pub fn search_and_format(&self, params: MetricSelection) -> Result<Output> {
let vecs = self.search(&params);
if vecs.is_empty() {
return Ok(Output::default(params.range.format()));
}
self.format(vecs.iter().collect(), &params.range)
}
/// Search and format with weight limit (for DDoS prevention)
pub fn search_and_format_checked(
&self,
params: MetricSelection,
max_weight: usize,
) -> Result<Output> {
let vecs = self.search(&params);
if vecs.is_empty() {
return Ok(Output::default(params.range.format()));
}
let weight = Self::weight(&vecs, params.from(), params.to());
if weight > max_weight {
return Err(Error::String(format!(
"Request too heavy: {weight} bytes exceeds limit of {max_weight} bytes"
)));
}
self.format(vecs.iter().collect(), &params.range)
}
pub fn metric_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> {
@@ -413,7 +240,7 @@ impl Query {
&self.vecs().indexes
}
pub fn get_metrics(&self, pagination: PaginationParam) -> PaginatedMetrics {
pub fn get_metrics(&self, pagination: Pagination) -> PaginatedMetrics {
self.vecs().metrics(pagination)
}
@@ -421,7 +248,7 @@ impl Query {
self.vecs().catalog()
}
pub fn get_index_to_vecids(&self, paginated_index: PaginatedIndexParam) -> Vec<&str> {
pub fn get_index_to_vecids(&self, paginated_index: PaginationIndex) -> Option<&[&str]> {
self.vecs().index_to_ids(paginated_index)
}
@@ -429,6 +256,8 @@ impl Query {
self.vecs().metric_to_indexes(metric)
}
// === Core accessors ===
#[inline]
pub fn reader(&self) -> &Reader {
&self.0.reader
-45
View File
@@ -1,45 +0,0 @@
use brk_types::Index;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::deser::de_unquote_usize;
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct PaginationParam {
#[schemars(description = "Pagination index")]
#[serde(default, alias = "p", deserialize_with = "de_unquote_usize")]
pub page: Option<usize>,
}
impl PaginationParam {
pub const PER_PAGE: usize = 1_000;
pub fn start(&self, len: usize) -> usize {
(self.page.unwrap_or_default() * Self::PER_PAGE).clamp(0, len)
}
pub fn end(&self, len: usize) -> usize {
((self.page.unwrap_or_default() + 1) * Self::PER_PAGE).clamp(0, len)
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct PaginatedIndexParam {
pub index: Index,
#[serde(flatten)]
pub pagination: PaginationParam,
}
/// A paginated list of available metric names (1000 per page)
#[derive(Debug, Serialize, JsonSchema)]
pub struct PaginatedMetrics {
/// Current page number (0-indexed)
#[schemars(example = 0)]
pub current_page: usize,
/// Maximum valid page index (0-indexed)
#[schemars(example = 21000)]
pub max_page: usize,
/// List of metric names (max 1000 per page)
#[schemars(example = ["price_open", "price_close", "realized_price", "..."])]
pub metrics: &'static [&'static str],
}
-130
View File
@@ -1,130 +0,0 @@
use std::ops::Deref;
use brk_types::{Format, Index, Metric, Metrics};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::deser::{de_unquote_i64, de_unquote_usize};
#[derive(Debug, Deserialize, JsonSchema)]
pub struct Params {
/// Requested metrics
#[serde(alias = "m")]
pub metrics: Metrics,
#[serde(alias = "i")]
pub index: Index,
#[serde(flatten)]
pub rest: ParamsOpt,
}
impl Deref for Params {
type Target = ParamsOpt;
fn deref(&self) -> &Self::Target {
&self.rest
}
}
impl From<(Index, Metric, ParamsOpt)> for Params {
#[inline]
fn from((index, metric, rest): (Index, Metric, ParamsOpt)) -> Self {
Self {
index,
metrics: Metrics::from(metric),
rest,
}
}
}
impl From<(Index, Metrics, ParamsOpt)> for Params {
#[inline]
fn from((index, metrics, rest): (Index, Metrics, ParamsOpt)) -> Self {
Self {
index,
metrics,
rest,
}
}
}
#[derive(Default, Debug, Deserialize, JsonSchema)]
pub struct ParamsOpt {
/// Inclusive starting index, if negative will be from the end
#[serde(default, alias = "f", deserialize_with = "de_unquote_i64")]
from: Option<i64>,
/// Exclusive ending index, if negative will be from the end, overrides 'count'
#[serde(default, alias = "t", deserialize_with = "de_unquote_i64")]
to: Option<i64>,
/// Number of values requested
#[serde(default, alias = "c", deserialize_with = "de_unquote_usize")]
count: Option<usize>,
/// Format of the output
#[serde(default)]
format: Format,
}
impl ParamsOpt {
pub fn set_from(mut self, from: i64) -> Self {
self.from.replace(from);
self
}
pub fn set_to(mut self, to: i64) -> Self {
self.to.replace(to);
self
}
pub fn set_count(mut self, count: usize) -> Self {
self.count.replace(count);
self
}
pub fn from(&self) -> Option<i64> {
self.from
}
pub fn to(&self) -> Option<i64> {
if self.to.is_none()
&& let Some(c) = self.count
{
let c = c as i64;
if let Some(f) = self.from {
if f >= 0 || f.abs() > c {
return Some(f + c);
}
} else {
return Some(c);
}
}
self.to
}
pub fn format(&self) -> Format {
self.format
}
}
#[derive(Debug, Deserialize)]
pub struct ParamsDeprec {
#[serde(alias = "i")]
pub index: Index,
#[serde(alias = "v")]
pub ids: Metrics,
#[serde(flatten)]
pub rest: ParamsOpt,
}
impl From<ParamsDeprec> for Params {
#[inline]
fn from(value: ParamsDeprec) -> Self {
Params {
index: value.index,
metrics: value.ids,
rest: value.rest,
}
}
}
+23 -35
View File
@@ -3,13 +3,11 @@ use std::collections::BTreeMap;
use brk_computer::Computer;
use brk_indexer::Indexer;
use brk_traversable::{Traversable, TreeNode};
use brk_types::{Index, IndexInfo, Limit, Metric};
use brk_types::{Index, IndexInfo, Limit, Metric, PaginatedMetrics, Pagination, PaginationIndex};
use derive_deref::{Deref, DerefMut};
use quickmatch::{QuickMatch, QuickMatchConfig};
use vecdb::AnyExportableVec;
use crate::pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam};
#[derive(Default)]
pub struct Vecs<'a> {
pub metric_to_index_to_vec: BTreeMap<&'a str, IndexToVec<'a>>,
@@ -18,7 +16,6 @@ pub struct Vecs<'a> {
pub indexes: Vec<IndexInfo>,
pub distinct_metric_count: usize,
pub total_metric_count: usize,
pub longest_metric_len: usize,
catalog: Option<TreeNode>,
matcher: Option<QuickMatch<'a>>,
metric_to_indexes: BTreeMap<&'a str, Vec<Index>>,
@@ -58,12 +55,6 @@ impl<'a> Vecs<'a> {
sort_ids(&mut ids);
this.metrics = ids;
this.longest_metric_len = this
.metrics
.iter()
.map(|s| s.len())
.max()
.unwrap_or_default();
this.distinct_metric_count = this.metric_to_index_to_vec.keys().count();
this.total_metric_count = this
.index_to_metric_to_vec
@@ -107,44 +98,35 @@ impl<'a> Vecs<'a> {
this
}
// Not the most performant or type safe but only built once so that's okay
fn insert(&mut self, vec: &'a dyn AnyExportableVec) {
let name = vec.name();
// dbg!(vec.region_name());
let serialized_index = vec.index_type_to_string();
let index = Index::try_from(serialized_index)
.inspect_err(|_| {
dbg!(&serialized_index);
})
.unwrap();
.unwrap_or_else(|_| panic!("Unknown index type: {serialized_index}"));
let prev = self
.metric_to_index_to_vec
.entry(name)
.or_default()
.insert(index, vec);
if prev.is_some() {
dbg!(serialized_index, name);
panic!()
}
assert!(prev.is_none(), "Duplicate metric: {name} for index {index:?}");
let prev = self
.index_to_metric_to_vec
.entry(index)
.or_default()
.insert(name, vec);
if prev.is_some() {
dbg!(serialized_index, name);
panic!()
}
assert!(prev.is_none(), "Duplicate metric: {name} for index {index:?}");
}
pub fn metrics(&'static self, pagination: PaginationParam) -> PaginatedMetrics {
pub fn metrics(&'static self, pagination: Pagination) -> PaginatedMetrics {
let len = self.metrics.len();
let start = pagination.start(len);
let end = pagination.end(len);
PaginatedMetrics {
current_page: pagination.page.unwrap_or_default(),
max_page: len.div_ceil(PaginationParam::PER_PAGE).saturating_sub(1),
current_page: pagination.page(),
max_page: len.div_ceil(Pagination::PER_PAGE).saturating_sub(1),
metrics: &self.metrics[start..end],
}
}
@@ -156,28 +138,34 @@ impl<'a> Vecs<'a> {
pub fn index_to_ids(
&self,
PaginatedIndexParam { index, pagination }: PaginatedIndexParam,
) -> Vec<&'a str> {
let vec = self.index_to_metrics.get(&index).unwrap();
PaginationIndex { index, pagination }: PaginationIndex,
) -> Option<&[&'a str]> {
let vec = self.index_to_metrics.get(&index)?;
let len = vec.len();
let start = pagination.start(len);
let end = pagination.end(len);
vec.iter().skip(start).take(end).cloned().collect()
Some(&vec[start..end])
}
pub fn catalog(&self) -> &TreeNode {
self.catalog.as_ref().unwrap()
self.catalog.as_ref().expect("catalog not initialized")
}
pub fn matches(&self, metric: &Metric, limit: Limit) -> Vec<&'_ str> {
self.matcher()
self.matcher
.as_ref()
.expect("matcher not initialized")
.matches_with(metric, &QuickMatchConfig::new().with_limit(*limit))
}
fn matcher(&self) -> &QuickMatch<'_> {
self.matcher.as_ref().unwrap()
/// Look up a vec by metric name and index
pub fn get(&self, metric: &Metric, index: Index) -> Option<&'a dyn AnyExportableVec> {
let metric_name = metric.replace("-", "_");
self.metric_to_index_to_vec
.get(metric_name.as_str())
.and_then(|index_to_vec| index_to_vec.get(&index).copied())
}
}
+3 -1
View File
@@ -15,7 +15,6 @@ brk_computer = { workspace = true }
brk_error = { workspace = true }
brk_fetcher = { workspace = true }
brk_indexer = { workspace = true }
brk_iterator = { workspace = true }
brk_logger = { workspace = true }
brk_mcp = { workspace = true }
brk_query = { workspace = true }
@@ -33,3 +32,6 @@ serde_json = { workspace = true }
tokio = { workspace = true }
tower-http = { version = "0.6.8", features = ["compression-full", "trace"] }
tracing = "0.1.43"
[dev-dependencies]
brk_mempool = { workspace = true }
+19 -218
View File
@@ -1,238 +1,39 @@
# brk_server
HTTP server providing REST API access to Bitcoin analytics data
HTTP server for the Bitcoin Research Kit.
[![Crates.io](https://img.shields.io/crates/v/brk_server.svg)](https://crates.io/crates/brk_server)
[![Documentation](https://docs.rs/brk_server/badge.svg)](https://docs.rs/brk_server)
## Overview
This crate provides a high-performance HTTP server built on `axum` that exposes Bitcoin blockchain analytics data through a comprehensive REST API. It integrates with the entire BRK ecosystem, serving data from indexers, computers, and parsers with intelligent caching, compression, and multiple output formats.
This crate provides an HTTP server that exposes BRK's blockchain data through a REST API. It serves as the web interface layer for the Bitcoin Research Kit, making data accessible to applications, dashboards, and research tools.
**Key Features:**
Built on `axum` with automatic OpenAPI documentation via Scalar.
- RESTful API for blockchain data queries with flexible filtering
- Multiple output formats: JSON, CSV
- Intelligent caching system with ETags and conditional requests
- HTTP compression (Gzip, Brotli, Deflate, Zstd) for bandwidth efficiency
- Static file serving for web interfaces and documentation
- Bitcoin address and transaction lookup endpoints
- Vector database query interface with pagination
- Health monitoring and status endpoints
**Target Use Cases:**
- Bitcoin data APIs for applications and research
- Web-based blockchain explorers and analytics dashboards
- Research data export and analysis tools
- Integration with external systems requiring Bitcoin data
## Installation
```bash
cargo add brk_server
```
## Quick Start
## Usage
```rust
use brk_server::Server;
use brk_query::Interface;
use std::path::PathBuf;
use brk_query::AsyncQuery;
// Initialize interface with your data sources
let interface = Interface::new(/* your config */);
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool));
let server = Server::new(&query, None);
// Optional static file serving directory
let files_path = Some(PathBuf::from("./web"));
// Create and start server
let server = Server::new(interface, files_path);
// Start server with optional MCP (Model Context Protocol) support
// Starts on port 3110 (or next available)
server.serve(true).await?;
```
## API Overview
Once running:
- **API Documentation**: `http://localhost:3110/api`
- **OpenAPI Spec**: `http://localhost:3110/api.json`
### Core Endpoints
## Features
**Blockchain Queries:**
- `GET /api/address/{address}` - Address information, balance, transaction counts
- `GET /api/tx/{txid}` - Transaction details including version, locktime
- `GET /api/vecs/{variant}` - Vector database queries with filtering
**System Information:**
- `GET /api/vecs/index-count` - Total number of indexes available
- `GET /api/vecs/id-count` - Vector ID statistics
- `GET /api/vecs/indexes` - List of available data indexes
- `GET /health` - Service health status
- `GET /version` - Server version information
### Vector Database API
**Query Interface:**
- `GET /api/vecs/query` - Generic vector query with parameters
- `GET /api/vecs/{variant}?from={start}&to={end}&format={format}` - Range queries
**Supported Parameters:**
- `from` / `to`: Range filtering (height, timestamp, date-based)
- `format`: Output format (json, csv)
- Pagination parameters for large datasets
### Address API Response Format
```json
{
"address": "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
"type": "p2pkh",
"index": 12345,
"chain_stats": {
"funded_txo_sum": 500000000,
"spent_txo_sum": 400000000,
"utxo_count": 5,
"balance": 100000000,
"balance_usd": 4200.5,
"realized_value": 450000000,
"avg_cost_basis": 45000.0
}
}
```
## Examples
### Basic Server Setup
```rust
use brk_server::Server;
use brk_query::Interface;
// Initialize with BRK interface
let interface = Interface::builder()
.with_indexer_path("./data/indexer")
.with_computer_path("./data/computer")
.build()?;
let server = Server::new(interface, None);
// Server automatically finds available port starting from 3110
server.serve(false).await?;
```
### Address Balance Lookup
```bash
# Get address information
curl http://localhost:3110/api/address/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
# Response includes balance, transaction counts, USD value
{
"address": "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
"type": "p2pkh",
"chain_stats": {
"balance": 100000000,
"balance_usd": 4200.50,
"utxo_count": 5
}
}
```
### Data Export Queries
```bash
# Export height-to-price data as CSV
curl "http://localhost:3110/api/vecs/height-to-price?from=800000&to=800100&format=csv" \
-H "Accept-Encoding: gzip"
# Query with caching - subsequent requests return 304 Not Modified
curl "http://localhost:3110/api/vecs/dateindex-to-price-ohlc?from=0&to=1000" \
-H "If-None-Match: \"etag-hash\""
```
### Transaction Details
```bash
# Get transaction information
curl http://localhost:3110/api/tx/abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
# Response includes version, locktime, and internal indexing
{
"txid": "abcdef...",
"index": 98765,
"version": 2,
"locktime": 0
}
```
## Architecture
### Server Stack
- **HTTP Framework**: `axum` with async/await for high concurrency
- **Compression**: Multi-algorithm support (Gzip, Brotli, Deflate, Zstd)
- **Caching**: `quick_cache` with LRU eviction and ETag validation
- **Tracing**: Request/response logging with latency tracking
- **Static Files**: Optional web interface serving
### Caching Strategy
The server implements intelligent caching:
- **ETags**: Generated from data version and query parameters
- **Conditional Requests**: 304 Not Modified responses for unchanged data
- **Memory Cache**: LRU cache with configurable capacity (5,000 entries)
- **Cache Control**: `must-revalidate` headers for data consistency
### Request Processing
1. **Route Matching**: Path-based routing to appropriate handlers
2. **Parameter Validation**: Query parameter parsing and validation
3. **Data Retrieval**: Interface calls to indexer/computer components
4. **Caching Logic**: ETag generation and cache lookup
5. **Format Conversion**: JSON/CSV output formatting
6. **Compression**: Response compression based on Accept-Encoding
7. **Response**: HTTP response with appropriate headers
### Static File Serving
Optional static file serving supports:
- Web interface hosting for blockchain explorers
- Documentation and API reference serving
- Asset serving (CSS, JS, images) with proper MIME types
- Directory browsing with index.html fallback
## Configuration
### Environment Variables
The server automatically configures itself but respects:
- Port selection: Starts at 3110, increments if unavailable
- Compression: Enabled by default for all supported algorithms
- CORS: Permissive headers for cross-origin requests
### Memory Management
- Cache size: 5,000 entries by default
- Request weight limits: 65MB maximum per query
- Timeout handling: 50ms cache guard timeout
- Compression: Adaptive based on content type and size
## Code Analysis Summary
**Main Components**: `Server` struct with `AppState` containing interface, cache, and file paths \
**HTTP Framework**: Built on `axum` with middleware for compression, tracing, and CORS \
**API Routes**: Address lookup, transaction details, vector queries, and system information \
**Caching Layer**: `quick_cache` integration with ETag-based conditional requests \
**Data Integration**: Direct interface to BRK indexer, computer, parser, and fetcher components \
**Static Serving**: Optional file serving for web interfaces and documentation \
**Architecture**: Async HTTP server with intelligent caching and multi-format data export capabilities
---
_This README was generated by Claude Code_
- **REST API** for addresses, blocks, transactions, mempool, mining stats, and metrics
- **OpenAPI documentation** with interactive Scalar UI
- **Multiple formats**: JSON and CSV output
- **HTTP caching**: ETag-based conditional requests
- **Compression**: Gzip, Brotli, Deflate, Zstd
- **MCP support**: Model Context Protocol for AI integrations
- **Static file serving**: Optional web interface hosting
@@ -1,15 +1,10 @@
use std::{
path::Path,
thread::{self, sleep},
time::Duration,
};
use std::{path::Path, thread};
use brk_computer::Computer;
use brk_error::Result;
use brk_fetcher::Fetcher;
use brk_indexer::Indexer;
use brk_iterator::Blocks;
use brk_mempool::Mempool;
use brk_query::AsyncQuery;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
@@ -30,10 +25,7 @@ fn run() -> Result<()> {
brk_logger::init(Some(Path::new(".log")))?;
let bitcoin_dir = Client::default_bitcoin_path();
// let bitcoin_dir = Path::new("/Volumes/WD_BLACK1/bitcoin");
let outputs_dir = Path::new(&std::env::var("HOME").unwrap()).join(".brk");
// let outputs_dir = Path::new("../../_outputs");
let client = Client::new(
Client::default_url(),
@@ -41,51 +33,45 @@ fn run() -> Result<()> {
)?;
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let blocks = Blocks::new(&client, &reader);
let mut indexer = Indexer::forced_import(&outputs_dir)?;
let indexer = Indexer::forced_import(&outputs_dir)?;
let fetcher = Some(Fetcher::import(true, None)?);
let computer = Computer::forced_import(&outputs_dir, &indexer, fetcher)?;
let mut computer = Computer::forced_import(&outputs_dir, &indexer, fetcher)?;
let mempool = Mempool::new(&client);
let mempool_clone = mempool.clone();
thread::spawn(move || {
mempool_clone.start();
});
let exit = Exit::new();
exit.set_ctrlc_handler();
let query = AsyncQuery::build(&reader, &indexer, &computer, None);
let future = async move {
let server = Server::new(&query, None);
tokio::spawn(async move {
server.serve(true).await.unwrap();
});
Ok(()) as Result<()>
};
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool));
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let _handle = runtime.spawn(future);
// Option 1: block_on to run and properly propagate errors
runtime.block_on(async move {
let server = Server::new(&query, None);
loop {
client.wait_for_synced_node()?;
let handle = tokio::spawn(async move { server.serve(true).await });
let last_height = client.get_last_height()?;
info!("{} blocks found.", u32::from(last_height) + 1);
let starting_indexes = indexer.checked_index(&blocks, &client, &exit)?;
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
info!("Waiting for new blocks...");
while last_height == client.get_last_height()? {
sleep(Duration::from_secs(1))
// Await the handle to catch both panics and errors
match handle.await {
Ok(Ok(())) => info!("Server shut down cleanly"),
Ok(Err(e)) => log::error!("Server error: {e:?}"),
Err(e) => {
// JoinError - either panic or cancellation
if e.is_panic() {
log::error!("Server panicked: {:?}", e.into_panic());
} else {
log::error!("Server task cancelled");
}
}
}
}
Ok(()) as Result<()>
})
}
+9 -37
View File
@@ -2,16 +2,12 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, Query, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_query::validate_address;
use brk_types::{Address, AddressStats, AddressTxidsParam, AddressValidation, Txid, Utxo};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -31,11 +27,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<Address>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address(address).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address(address)).await
}, |op| op
.addresses_tag()
.summary("Address information")
@@ -55,11 +47,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_txids(address, params.after_txid, params.limit).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(address, params.after_txid, params.limit)).await
}, |op| op
.addresses_tag()
.summary("Address transaction IDs")
@@ -78,11 +66,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<Address>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_utxos(address).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_utxos(address)).await
}, |op| op
.addresses_tag()
.summary("Address UTXOs")
@@ -101,17 +85,13 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<Address>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_mempool_txids(address).await.to_json_response(&etag)
// Mempool txs for an address - use MaxAge since it's volatile
state.cached_json(&headers, CacheStrategy::MaxAge(5), move |q| q.address_mempool_txids(address)).await
}, |op| op
.addresses_tag()
.summary("Address mempool transactions")
.description("Get unconfirmed transaction IDs for an address from the mempool (up to 50).")
.ok_response::<Vec<Txid>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
@@ -125,11 +105,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_txids(address, params.after_txid, 25).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(address, params.after_txid, 25)).await
}, |op| op
.addresses_tag()
.summary("Address confirmed transactions")
@@ -148,11 +124,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<String>,
State(state): State<AppState>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(validate_address(&address), etag)
state.cached_json(&headers, CacheStrategy::Static, move |_q| Ok(AddressValidation::from_address(&address))).await
}, |op| op
.addresses_tag()
.summary("Validate address")
+12 -59
View File
@@ -2,7 +2,7 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_query::BLOCK_TXS_PAGE_SIZE;
@@ -11,10 +11,7 @@ use brk_types::{
BlockTimestamp, Height, HeightPath, StartHeightPath, TimestampPath, Transaction, Txid,
};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -35,11 +32,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block(path.hash).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block(&path.hash)).await
},
|op| {
op.blocks_tag()
@@ -61,14 +54,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_status(path.hash)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_status(&path.hash)).await
},
|op| {
op.blocks_tag()
@@ -90,14 +76,8 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<HeightPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_by_height(Height::from(path.height))
.await
.to_json_response(&etag)
let height = Height::from(path.height);
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_height(height)).await
},
|op| {
op.blocks_tag()
@@ -119,12 +99,8 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<StartHeightPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
let start_height = path.start_height.map(Height::from);
state.get_blocks(start_height).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(start_height)).await
},
|op| {
op.blocks_tag()
@@ -145,11 +121,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_txids(path.hash).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txids(&path.hash)).await
},
|op| {
op.blocks_tag()
@@ -171,11 +143,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashStartIndexPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_txs(path.hash, path.start_index).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txs(&path.hash, path.start_index)).await
},
|op| {
op.blocks_tag()
@@ -198,11 +166,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashTxIndexPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_txid_at_index(path.hash, path.index).await.to_display_response(&etag)
state.cached_text(&headers, CacheStrategy::Height, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
},
|op| {
op.blocks_tag()
@@ -224,14 +188,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<TimestampPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_by_timestamp(path.timestamp)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_timestamp(path.timestamp)).await
},
|op| {
op.blocks_tag()
@@ -251,11 +208,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_raw(path.hash).await.to_bytes_response(&etag)
state.cached_bytes(&headers, CacheStrategy::Height, move |q| q.block_raw(&path.hash)).await
},
|op| {
op.blocks_tag()
+6 -29
View File
@@ -2,15 +2,12 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::State,
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -26,18 +23,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/mempool/info",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_mempool_info().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_info()).await
},
|op| {
op.mempool_tag()
.summary("Mempool statistics")
.description("Get current mempool statistics including transaction count, total vsize, and total fees.")
.ok_response::<MempoolInfo>()
.not_modified()
.server_error()
},
),
@@ -46,18 +38,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/mempool/txids",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_mempool_txids().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_txids()).await
},
|op| {
op.mempool_tag()
.summary("Mempool transaction IDs")
.description("Get all transaction IDs currently in the mempool.")
.ok_response::<Vec<Txid>>()
.not_modified()
.server_error()
},
),
@@ -66,18 +53,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/v1/fees/recommended",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_recommended_fees().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(3), |q| q.recommended_fees()).await
},
|op| {
op.mempool_tag()
.summary("Recommended fees")
.description("Get recommended fee rates for different confirmation targets based on current mempool state.")
.ok_response::<RecommendedFees>()
.not_modified()
.server_error()
},
),
@@ -86,18 +68,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/v1/fees/mempool-blocks",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_mempool_blocks().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_blocks()).await
},
|op| {
op.mempool_tag()
.summary("Projected mempool blocks")
.description("Get projected blocks from the mempool for fee estimation. Each block contains statistics about transactions that would be included if a block were mined now.")
.ok_response::<Vec<MempoolBlock>>()
.not_modified()
.server_error()
},
),
+56 -85
View File
@@ -1,31 +1,29 @@
use std::time::Duration;
use axum::{
Json,
body::Body,
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::{Error, Result};
use brk_query::{Output, Params};
use brk_query::{MetricSelection, Output};
use brk_types::Format;
use quick_cache::sync::GuardResult;
use vecdb::Stamp;
use crate::{HeaderMapExtended, ResponseExtended};
use crate::{CacheStrategy, cache::CacheParams, extended::HeaderMapExtended};
use super::AppState;
/// Maximum allowed request weight in bytes (650KB)
const MAX_WEIGHT: usize = 65 * 10_000;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<Params>,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state) {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => {
let mut response =
@@ -36,91 +34,64 @@ pub async fn handler(
}
}
fn req_to_response_res(
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<Params>,
AppState {
query: interface,
cache,
..
}: AppState,
) -> Result<Response> {
todo!();
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
) -> brk_error::Result<Response> {
let format = params.format();
let height = query.sync(|q| q.height());
let to = params.to();
// let vecs = interface.search(&params)?;
let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || height.into());
// if vecs.is_empty() {
// return Ok(Json(vec![] as Vec<usize>).into_response());
// }
if cache_params.matches_etag(&headers) {
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
response.headers_mut().insert_cors();
return Ok(response);
}
// let from = params.from();
// let to = params.to();
// let format = params.format();
let cache_key = format!("{}{}{}", uri.path(), uri.query().unwrap_or(""), cache_params.etag_str());
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
// // TODO: From and to should be capped here
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
match query
.run(move |q| q.search_and_format_checked(params, MAX_WEIGHT))
.await?
{
Output::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
}
s.into_response()
}
Output::Json(v) => {
let json = v.to_vec();
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(json.clone().into());
}
json.into_response()
}
}
};
// let weight = vecs
// .iter()
// .map(|(_, v)| v.range_weight(from, to))
// .sum::<usize>();
let headers = response.headers_mut();
headers.insert_cors();
if let Some(etag) = &cache_params.etag {
headers.insert_etag(etag);
}
headers.insert_cache_control(&cache_params.cache_control);
// if weight > MAX_WEIGHT {
// return Err(Error::String(format!(
// "Request is too heavy, max weight is {MAX_WEIGHT} bytes"
// )));
// }
match format {
Format::CSV => {
headers.insert_content_disposition_attachment();
headers.insert_content_type_text_csv()
}
Format::JSON => headers.insert_content_type_application_json(),
}
// // TODO: height should be from vec, but good enough for now
// let etag = vecs
// .first()
// .unwrap()
// .1
// .etag(Stamp::from(interface.get_height()), to);
// if headers.has_etag(etag) {
// return Ok(Response::new_not_modified());
// }
// let guard_res = cache.get_value_or_guard(
// &format!("{}{}{etag}", uri.path(), uri.query().unwrap_or("")),
// Some(Duration::from_millis(50)),
// );
// let mut response = if let GuardResult::Value(v) = guard_res {
// Response::new(Body::from(v))
// } else {
// match interface.format(vecs, &params.rest)? {
// Output::CSV(s) => {
// if let GuardResult::Guard(g) = guard_res {
// let _ = g.insert(s.clone().into());
// }
// s.into_response()
// }
// Output::Json(v) => {
// let json = v.to_vec();
// if let GuardResult::Guard(g) = guard_res {
// let _ = g.insert(json.clone().into());
// }
// json.into_response()
// }
// }
// };
// let headers = response.headers_mut();
// headers.insert_cors();
// headers.insert_etag(&etag);
// headers.insert_cache_control_must_revalidate();
// match format {
// Format::CSV => {
// headers.insert_content_disposition_attachment();
// headers.insert_content_type_text_csv()
// }
// Format::JSON => headers.insert_content_type_application_json(),
// }
// Ok(response)
Ok(response)
}
+34 -61
View File
@@ -1,18 +1,15 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, Query, State},
http::{HeaderMap, StatusCode, Uri},
http::{HeaderMap, Uri},
response::{IntoResponse, Redirect, Response},
routing::get,
};
use brk_query::{PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt};
use brk_query::{DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, Pagination};
use brk_traversable::TreeNode;
use brk_types::{Index, IndexInfo, Limit, Metric, MetricCount, Metrics};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -34,11 +31,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
State(state): State<AppState>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(state.metric_count().await, etag)
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metric_count())).await
},
|op| op
.metrics_tag()
@@ -55,11 +48,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
State(state): State<AppState>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json( state.get_indexes().await, etag)
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.get_indexes().to_vec())).await
},
|op| op
.metrics_tag()
@@ -77,13 +66,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |
headers: HeaderMap,
State(state): State<AppState>,
Query(pagination): Query<PaginationParam>
Query(pagination): Query<Pagination>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(state.get_metrics(pagination).await, etag)
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.get_metrics(pagination))).await
},
|op| op
.metrics_tag()
@@ -96,12 +81,8 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.api_route(
"/api/metrics/catalog",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| -> Response {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(state.get_metrics_catalog().await, etag)
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.get_metrics_catalog().clone())).await
},
|op| op
.metrics_tag()
@@ -122,11 +103,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
Path(metric): Path<Metric>,
Query(limit): Query<Limit>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
state.match_metric(metric, limit).await.to_json_response(etag)
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.match_metric(&metric, limit))).await
},
|op| op
.metrics_tag()
@@ -144,20 +121,18 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(metric): Path<Metric>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
if let Some(indexes) = state.metric_to_indexes(metric.clone()).await {
return Response::new_json(indexes, etag)
}
// REMOVE UNWRAP !!
let value = if let Some(first) = state.match_metric(metric.clone(), Limit::MIN).await.unwrap().first() {
format!("Could not find '{metric}', did you mean '{first}' ?")
} else {
format!("Could not find '{metric}'.")
};
Response::new_json_with(StatusCode::NOT_FOUND, value, etag)
state.cached_json(&headers, CacheStrategy::Static, move |q| {
if let Some(indexes) = q.metric_to_indexes(metric.clone()) {
return Ok(indexes.clone())
}
Err(brk_error::Error::String(
if let Some(first) = q.match_metric(&metric, Limit::MIN).first() {
format!("Could not find '{metric}', did you mean '{first}' ?")
} else {
format!("Could not find '{metric}'.")
}
))
}).await
},
|op| op
.metrics_tag()
@@ -173,7 +148,6 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
)
// WIP
.route("/api/metrics/bulk", get(data::handler))
// WIP
.route(
"/api/metric/{metric}/{index}",
get(
@@ -181,16 +155,15 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
state: State<AppState>,
Path((metric, index)): Path<(Metric, Index)>,
Query(params_opt): Query<ParamsOpt>|
Query(range): Query<DataRangeFormat>|
-> Response {
todo!();
// data::handler(
// uri,
// headers,
// Query(Params::from(((index, metric), params_opt))),
// state,
// )
// .await
data::handler(
uri,
headers,
Query(MetricSelection::from((index, metric, range))),
state,
)
.await
},
),
)
@@ -202,7 +175,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
get(
async |uri: Uri,
headers: HeaderMap,
Query(params): Query<ParamsDeprec>,
Query(params): Query<MetricSelectionLegacy>,
state: State<AppState>|
-> Response {
data::handler(uri, headers, Query(params.into()), state).await
@@ -218,7 +191,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |uri: Uri,
headers: HeaderMap,
Path(variant): Path<String>,
Query(params_opt): Query<ParamsOpt>,
Query(range): Query<DataRangeFormat>,
state: State<AppState>|
-> Response {
let separator = "_to_";
@@ -230,10 +203,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
return format!("Index {ser_index} doesn't exist").into_response();
};
let params = Params::from((
let params = MetricSelection::from((
index,
Metrics::from(split.collect::<Vec<_>>().join(separator)),
params_opt,
range,
));
data::handler(uri, headers, Query(params), state).await
},
+25 -115
View File
@@ -2,7 +2,7 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{
@@ -11,10 +11,7 @@ use brk_types::{
PoolSlugPath, PoolsSummary, RewardStats, TimePeriodPath,
};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -32,14 +29,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/difficulty-adjustment",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_difficulty_adjustment()
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustment()).await
},
|op| {
op.mining_tag()
@@ -55,11 +45,8 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pools",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-pools");
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_all_pools().await.to_json_response(&etag)
// Pool list is static, only changes on code update
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.all_pools())).await
},
|op| {
op.mining_tag()
@@ -72,17 +59,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/pools/:time_period",
"/api/v1/mining/pools/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_mining_pools(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("{:?}", path.time_period)), move |q| q.mining_pools(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -95,17 +75,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/pool/:slug",
"/api/v1/mining/pool/{slug}",
get_with(
async |headers: HeaderMap, Path(path): Path<PoolSlugPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}-{:?}", state.get_height().await, path.slug);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_pool_detail(path.slug)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(path.slug), move |q| q.pool_detail(path.slug)).await
},
|op| {
op.mining_tag()
@@ -122,14 +95,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-hashrate-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_hashrate(None)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with("hashrate"), |q| q.hashrate(None)).await
},
|op| {
op.mining_tag()
@@ -142,17 +108,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/hashrate/:time_period",
"/api/v1/mining/hashrate/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-hashrate-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_hashrate(Some(path.time_period))
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("hashrate-{:?}", path.time_period)), move |q| q.hashrate(Some(path.time_period))).await
},
|op| {
op.mining_tag()
@@ -168,14 +127,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/difficulty-adjustments",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-diff-adj-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_difficulty_adjustments(None)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with("diff-adj"), |q| q.difficulty_adjustments(None)).await
},
|op| {
op.mining_tag()
@@ -188,17 +140,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/difficulty-adjustments/:time_period",
"/api/v1/mining/difficulty-adjustments/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-diff-adj-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_difficulty_adjustments(Some(path.time_period))
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("diff-adj-{:?}", path.time_period)), move |q| q.difficulty_adjustments(Some(path.time_period))).await
},
|op| {
op.mining_tag()
@@ -211,17 +156,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/fees/:time_period",
"/api/v1/mining/blocks/fees/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-fees-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_fees(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("fees-{:?}", path.time_period)), move |q| q.block_fees(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -234,17 +172,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/rewards/:time_period",
"/api/v1/mining/blocks/rewards/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-rewards-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_rewards(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("rewards-{:?}", path.time_period)), move |q| q.block_rewards(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -257,17 +188,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/fee-rates/:time_period",
"/api/v1/mining/blocks/fee-rates/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-feerates-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_fee_rates(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("feerates-{:?}", path.time_period)), move |q| q.block_fee_rates(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -280,17 +204,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/sizes-weights/:time_period",
"/api/v1/mining/blocks/sizes-weights/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-sizes-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_sizes_weights(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("sizes-{:?}", path.time_period)), move |q| q.block_sizes_weights(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -303,17 +220,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/reward-stats/:block_count",
"/api/v1/mining/reward-stats/{block_count}",
get_with(
async |headers: HeaderMap, Path(path): Path<BlockCountPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-reward-stats-{}-{}", state.get_height().await, path.block_count);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_reward_stats(path.block_count)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("reward-stats-{}", path.block_count)), move |q| q.reward_stats(path.block_count)).await
},
|op| {
op.mining_tag()
+6 -2
View File
@@ -6,6 +6,7 @@ use aide::{
};
use axum::{
Extension, Json,
extract::State,
http::HeaderMap,
response::{Html, Redirect, Response},
routing::get,
@@ -13,7 +14,7 @@ use axum::{
use brk_types::Health;
use crate::{
VERSION,
CacheStrategy, VERSION,
api::{
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, transactions::TxRoutes,
@@ -49,12 +50,15 @@ impl ApiRoutes for ApiRouter<AppState> {
.api_route(
"/version",
get_with(
async || -> Json<&'static str> { Json(VERSION) },
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, |_| Ok(env!("CARGO_PKG_VERSION"))).await
},
|op| {
op.server_tag()
.summary("API version")
.description("Returns the current version of the API server")
.ok_response::<String>()
.not_modified()
},
),
)
+21 -8
View File
@@ -29,7 +29,8 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Addresses".to_string(),
description: Some(
"Explore Bitcoin addresses."
"Query Bitcoin address data including balances, transaction history, and UTXOs. \
Supports all address types: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR."
.to_string()
),
..Default::default()
@@ -37,7 +38,17 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Blocks".to_string(),
description: Some(
"Explore Bitcoin blocks."
"Retrieve block data by hash or height. Access block headers, transaction lists, \
and raw block bytes."
.to_string()
),
..Default::default()
},
Tag {
name: "Mempool".to_string(),
description: Some(
"Monitor unconfirmed transactions and fee estimates. Get mempool statistics, \
transaction IDs, and recommended fee rates for different confirmation targets."
.to_string()
),
..Default::default()
@@ -45,8 +56,8 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Metrics".to_string(),
description: Some(
"Access Bitcoin network metrics and time-series data. Query historical and real-time \
statistics across various blockchain dimensions and aggregation levels."
"Access Bitcoin network metrics and time-series data. Query historical statistics \
across various indexes (date, week, month, year, halving epoch) with JSON or CSV output."
.to_string()
),
..Default::default()
@@ -54,7 +65,8 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Mining".to_string(),
description: Some(
"Explore mining related endpoints."
"Mining statistics including pool distribution, hashrate, difficulty adjustments, \
block rewards, and fee rates across configurable time periods."
.to_string()
),
..Default::default()
@@ -62,15 +74,16 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Server".to_string(),
description: Some(
"Metadata and utility endpoints for API status, health checks, and system information."
"API metadata and health monitoring. Version information and service status."
.to_string()
),
..Default::default()
..Default::default()
},
Tag {
name: "Transactions".to_string(),
description: Some(
"Explore Bitcoin transactions."
"Retrieve transaction data by txid. Access full transaction details, confirmation \
status, raw hex, and output spend information."
.to_string()
),
..Default::default()
+7 -30
View File
@@ -2,15 +2,12 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{Transaction, TxOutspend, TxStatus, TxidPath, TxidVoutPath};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -31,11 +28,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_transaction(txid).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction(txid)).await
},
|op| op
.transactions_tag()
@@ -58,11 +51,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_transaction_status(txid).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction_status(txid)).await
},
|op| op
.transactions_tag()
@@ -85,11 +74,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_transaction_hex(txid).await.to_text_response(&etag)
state.cached_text(&headers, CacheStrategy::Height, move |q| q.transaction_hex(txid)).await
},
|op| op
.transactions_tag()
@@ -112,12 +97,8 @@ impl TxRoutes for ApiRouter<AppState> {
Path(path): Path<TxidVoutPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
let txid = TxidPath { txid: path.txid };
state.get_tx_outspend(txid, path.vout).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspend(txid, path.vout)).await
},
|op| op
.transactions_tag()
@@ -140,11 +121,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_tx_outspends(txid).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspends(txid)).await
},
|op| op
.transactions_tag()
+108
View File
@@ -0,0 +1,108 @@
use axum::http::HeaderMap;
use crate::{VERSION, extended::HeaderMapExtended};
/// Minimum confirmations before data is considered immutable
pub const MIN_CONFIRMATIONS: u32 = 6;
/// Cache strategy for HTTP responses.
///
/// # Future optimization: Immutable caching for blocks/txs
///
/// The `Immutable` variant supports caching deeply-confirmed blocks/txs forever
/// (1 year, `immutable` directive). To use it, you need the confirmation count:
///
/// ```ignore
/// // Example: cache block by hash as immutable if deeply confirmed
/// let confirmations = current_height - block_height + 1;
/// let prefix = *BlockHashPrefix::from(&hash);
/// state.cached_json(&headers, CacheStrategy::immutable(prefix, confirmations), |q| q.block(&hash)).await
/// ```
///
/// Currently all block/tx handlers use `Height` for simplicity since determining
/// confirmations requires knowing the block height upfront (an extra lookup).
/// This could be optimized by either:
/// 1. Including confirmation count in the response type
/// 2. Doing a lightweight height lookup before the main query
pub enum CacheStrategy {
/// Immutable data (blocks by hash with 6+ confirmations)
/// Etag = VERSION-{prefix:x}, Cache-Control: immutable, 1yr
/// Falls back to Height if < 6 confirmations
Immutable { prefix: u64, confirmations: u32 },
/// Data that changes with each new block (addresses, block-by-height)
/// Etag = VERSION-{height}, Cache-Control: must-revalidate
Height,
/// Data that changes with height + depends on parameter
/// Etag = VERSION-{height}-{suffix}, Cache-Control: must-revalidate
HeightWith(String),
/// Static data (validate-address, metrics catalog)
/// Etag = VERSION only, Cache-Control: 1hr
Static,
/// Volatile data (mempool) - no etag, just max-age
/// Cache-Control: max-age={seconds}
MaxAge(u64),
}
impl CacheStrategy {
/// Create Immutable strategy - pass *prefix (deref BlockHashPrefix/TxidPrefix to u64)
pub fn immutable(prefix: u64, confirmations: u32) -> Self {
Self::Immutable {
prefix,
confirmations,
}
}
/// Create HeightWith from any Display type
pub fn height_with(suffix: impl std::fmt::Display) -> Self {
Self::HeightWith(suffix.to_string())
}
}
/// Resolved cache parameters
pub struct CacheParams {
pub etag: Option<String>,
pub cache_control: String,
}
impl CacheParams {
pub fn etag_str(&self) -> &str {
self.etag.as_deref().unwrap_or("")
}
pub fn matches_etag(&self, headers: &HeaderMap) -> bool {
self.etag.as_ref().is_some_and(|etag| headers.has_etag(etag))
}
pub fn resolve(strategy: &CacheStrategy, height: impl FnOnce() -> u32) -> Self {
use CacheStrategy::*;
match strategy {
Immutable {
prefix,
confirmations,
} if *confirmations >= MIN_CONFIRMATIONS => Self {
etag: Some(format!("{VERSION}-{prefix:x}")),
cache_control: "public, max-age=31536000, immutable".into(),
},
Immutable { .. } | Height => Self {
etag: Some(format!("{VERSION}-{}", height())),
cache_control: "public, max-age=1, must-revalidate".into(),
},
HeightWith(suffix) => Self {
etag: Some(format!("{VERSION}-{}-{suffix}", height())),
cache_control: "public, max-age=1, must-revalidate".into(),
},
Static => Self {
etag: Some(VERSION.to_string()),
cache_control: "public, max-age=1, must-revalidate".into(),
},
MaxAge(secs) => Self {
etag: None,
cache_control: format!("public, max-age={secs}"),
},
}
}
}
+7 -8
View File
@@ -27,6 +27,7 @@ pub trait HeaderMapExtended {
fn check_if_modified_since(&self, path: &Path) -> Result<(ModifiedState, DateTime)>;
fn check_if_modified_since_(&self, duration: Duration) -> Result<(ModifiedState, DateTime)>;
fn insert_cache_control(&mut self, value: &str);
fn insert_cache_control_must_revalidate(&mut self);
fn insert_cache_control_immutable(&mut self);
fn insert_etag(&mut self, etag: &str);
@@ -56,18 +57,16 @@ impl HeaderMapExtended for HeaderMap {
self.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "*".parse().unwrap());
}
fn insert_cache_control(&mut self, value: &str) {
self.insert(header::CACHE_CONTROL, value.parse().unwrap());
}
fn insert_cache_control_must_revalidate(&mut self) {
self.insert(
header::CACHE_CONTROL,
"public, max-age=1, must-revalidate".parse().unwrap(),
);
self.insert_cache_control("public, max-age=1, must-revalidate");
}
fn insert_cache_control_immutable(&mut self) {
self.insert(
header::CACHE_CONTROL,
"public, max-age=31536000, immutable".parse().unwrap(),
);
self.insert_cache_control("public, max-age=31536000, immutable");
}
fn insert_content_disposition_attachment(&mut self) {
@@ -6,6 +6,7 @@ use axum::{
use serde::Serialize;
use super::header_map::HeaderMapExtended;
use crate::cache::CacheParams;
pub trait ResponseExtended
where
@@ -16,12 +17,17 @@ where
where
T: Serialize;
fn new_json_with<T>(status: StatusCode, value: T, etag: &str) -> Self
where
T: Serialize;
fn new_json_cached<T>(value: T, params: &CacheParams) -> Self
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> {
@@ -85,4 +91,46 @@ impl ResponseExtended for Response<Body> {
headers.insert_etag(etag);
response
}
fn new_json_cached<T>(value: T, params: &CacheParams) -> Self
where
T: Serialize,
{
let bytes = serde_json::to_vec(&value).unwrap();
let mut response = Response::builder().body(bytes.into()).unwrap();
let headers = response.headers_mut();
headers.insert_cors();
headers.insert_content_type_application_json();
headers.insert_cache_control(&params.cache_control);
if let Some(etag) = &params.etag {
headers.insert_etag(etag);
}
response
}
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_cors();
headers.insert_content_type_text_plain();
headers.insert_cache_control(&params.cache_control);
if let Some(etag) = &params.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_cors();
headers.insert_content_type_octet_stream();
headers.insert_cache_control(&params.cache_control);
if let Some(etag) = &params.etag {
headers.insert_etag(etag);
}
response
}
}
-15
View File
@@ -1,5 +1,3 @@
use std::fmt::Display;
use axum::{http::StatusCode, response::Response};
use brk_error::{Error, Result};
use serde::Serialize;
@@ -14,9 +12,6 @@ pub trait ResultExtended<T> {
fn to_text_response(self, etag: &str) -> Response
where
T: AsRef<str>;
fn to_display_response(self, etag: &str) -> Response
where
T: Display;
fn to_bytes_response(self, etag: &str) -> Response
where
T: Into<Vec<u8>>;
@@ -59,16 +54,6 @@ impl<T> ResultExtended<T> for Result<T> {
}
}
fn to_display_response(self, etag: &str) -> Response
where
T: Display,
{
match self.with_status() {
Ok(value) => Response::new_text(&value.to_string(), etag),
Err((status, message)) => Response::new_text_with(status, &message, etag),
}
}
fn to_bytes_response(self, etag: &str) -> Response
where
T: Into<Vec<u8>>,
+2 -2
View File
@@ -1,7 +1,7 @@
use std::path::PathBuf;
use aide::axum::ApiRouter;
use axum::routing::get;
use axum::{response::Redirect, routing::get};
use super::AppState;
@@ -19,7 +19,7 @@ impl FilesRoutes for ApiRouter<AppState> {
self.route("/{*path}", get(file_handler))
.route("/", get(index_handler))
} else {
self
self.route("/", get(Redirect::temporary("/api")))
}
}
}
+77 -11
View File
@@ -1,6 +1,6 @@
#![doc = include_str!("../README.md")]
#![doc = "\n## Example\n\n```rust"]
#![doc = include_str!("../examples/main.rs")]
#![doc = include_str!("../examples/server.rs")]
#![doc = "```"]
use std::{ops::Deref, path::PathBuf, sync::Arc, time::Duration};
@@ -28,10 +28,12 @@ use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tracing::Span;
mod api;
pub mod cache;
mod extended;
mod files;
use api::*;
pub use cache::{CacheParams, CacheStrategy};
use extended::*;
#[derive(Clone)]
@@ -48,6 +50,71 @@ impl Deref for AppState {
}
}
impl AppState {
/// JSON response with caching
pub async fn cached_json<T, F>(
&self,
headers: &axum::http::HeaderMap,
strategy: CacheStrategy,
f: F,
) -> axum::http::Response<axum::body::Body>
where
T: serde::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, &params),
Err(e) => ResultExtended::<T>::to_json_response(Err(e), params.etag_str()),
}
}
/// Text response with caching
pub async fn cached_text<T, F>(
&self,
headers: &axum::http::HeaderMap,
strategy: CacheStrategy,
f: F,
) -> axum::http::Response<axum::body::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(), &params),
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: &axum::http::HeaderMap,
strategy: CacheStrategy,
f: F,
) -> axum::http::Response<axum::body::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(), &params),
Err(e) => ResultExtended::<T>::to_bytes_response(Err(e), params.etag_str()),
}
}
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub struct Server(AppState);
@@ -132,21 +199,20 @@ impl Server {
.layer(response_uri_layer)
.layer(trace_layer);
let mut port = 3110;
const BASE_PORT: u16 = 3110;
const MAX_PORT: u16 = BASE_PORT + 100;
let mut listener;
loop {
listener = TcpListener::bind(format!("0.0.0.0:{port}")).await;
if listener.is_ok() {
break;
let mut port = BASE_PORT;
let listener = loop {
match TcpListener::bind(format!("0.0.0.0:{port}")).await {
Ok(l) => break l,
Err(_) if port < MAX_PORT => port += 1,
Err(e) => return Err(e.into()),
}
port += 1;
}
};
info!("Starting server on port {port}...");
let listener = listener.unwrap();
let mut openapi = create_openapi();
serve(
listener,
+42
View File
@@ -1,6 +1,9 @@
use bitcoin::hex::DisplayHex;
use schemars::JsonSchema;
use serde::Serialize;
use crate::{AddressBytes, OutputType};
/// Address validation result
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct AddressValidation {
@@ -34,6 +37,7 @@ pub struct AddressValidation {
}
impl AddressValidation {
/// Returns an invalid validation result
pub fn invalid() -> Self {
Self {
isvalid: false,
@@ -45,4 +49,42 @@ impl AddressValidation {
witness_program: None,
}
}
/// Validate a Bitcoin address string and return details
pub fn from_address(address: &str) -> Self {
let Ok(script) = AddressBytes::address_to_script(address) else {
return Self::invalid();
};
let output_type = OutputType::from(&script);
let script_hex = script.as_bytes().to_lower_hex_string();
let is_script = matches!(output_type, OutputType::P2SH);
let is_witness = matches!(
output_type,
OutputType::P2WPKH | OutputType::P2WSH | OutputType::P2TR | OutputType::P2A
);
let (witness_version, witness_program) = if is_witness {
let version = script.witness_version().map(|v| v.to_num());
let program = if script.len() > 2 {
Some(script.as_bytes()[2..].to_lower_hex_string())
} else {
None
};
(version, program)
} else {
(None, None)
};
Self {
isvalid: true,
address: Some(address.to_string()),
script_pub_key: Some(script_hex),
isscript: Some(is_script),
iswitness: Some(is_witness),
witness_version,
witness_program,
}
}
}
+5 -3
View File
@@ -1,11 +1,13 @@
use schemars::JsonSchema;
use serde::Serialize;
use crate::{Height, Sats, Timestamp};
/// A single block fees data point.
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BlockFeesEntry {
pub avg_height: u32,
pub timestamp: u32,
pub avg_fees: u64,
pub avg_height: Height,
pub timestamp: Timestamp,
pub avg_fees: Sats,
}
+4 -2
View File
@@ -1,14 +1,16 @@
use schemars::JsonSchema;
use serde::Serialize;
use crate::{Height, Timestamp};
use super::FeeRatePercentiles;
/// A single block fee rates data point with percentiles.
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BlockFeeRatesEntry {
pub avg_height: u32,
pub timestamp: u32,
pub avg_height: Height,
pub timestamp: Timestamp,
#[serde(flatten)]
pub percentiles: FeeRatePercentiles,
}
+55
View File
@@ -0,0 +1,55 @@
use schemars::JsonSchema;
use serde::Deserialize;
/// Range parameters for slicing data
#[derive(Default, Debug, Deserialize, JsonSchema)]
pub struct DataRange {
/// Inclusive starting index, if negative will be from the end
#[serde(default, alias = "f")]
from: Option<i64>,
/// Exclusive ending index, if negative will be from the end, overrides 'count'
#[serde(default, alias = "t")]
to: Option<i64>,
/// Number of values requested
#[serde(default, alias = "c")]
count: Option<usize>,
}
impl DataRange {
pub fn set_from(mut self, from: i64) -> Self {
self.from.replace(from);
self
}
pub fn set_to(mut self, to: i64) -> Self {
self.to.replace(to);
self
}
pub fn set_count(mut self, count: usize) -> Self {
self.count.replace(count);
self
}
pub fn from(&self) -> Option<i64> {
self.from
}
pub fn to(&self) -> Option<i64> {
if self.to.is_none()
&& let Some(c) = self.count
{
let c = c as i64;
if let Some(f) = self.from {
if f >= 0 || f.abs() > c {
return Some(f + c);
}
} else {
return Some(c);
}
}
self.to
}
}
+45
View File
@@ -0,0 +1,45 @@
use std::ops::Deref;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{DataRange, Format};
/// Data range with output format for API query parameters
#[derive(Default, Debug, Deserialize, JsonSchema)]
pub struct DataRangeFormat {
#[serde(flatten)]
pub range: DataRange,
/// Format of the output
#[serde(default)]
format: Format,
}
impl DataRangeFormat {
pub fn format(&self) -> Format {
self.format
}
pub fn set_from(mut self, from: i64) -> Self {
self.range = self.range.set_from(from);
self
}
pub fn set_to(mut self, to: i64) -> Self {
self.range = self.range.set_to(to);
self
}
pub fn set_count(mut self, count: usize) -> Self {
self.range = self.range.set_count(count);
self
}
}
impl Deref for DataRangeFormat {
type Target = DataRange;
fn deref(&self) -> &Self::Target {
&self.range
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ use serde::Serialize;
use super::Index;
#[derive(Serialize, JsonSchema)]
#[derive(Clone, Copy, Serialize, JsonSchema)]
/// Information about an available index and its query aliases
pub struct IndexInfo {
/// The canonical index name
+20 -6
View File
@@ -18,20 +18,20 @@ mod blkmetadata;
mod blkposition;
mod block;
mod blockcountpath;
mod blockfeesentry;
mod blockferatesentry;
mod blockhash;
mod blockhashpath;
mod blockhashprefix;
mod blockhashstartindexpath;
mod blockhashtxindexpath;
mod blockfeesentry;
mod blockferatesentry;
mod blockinfo;
mod blockrewardsentry;
mod blocksizeentry;
mod blocksizesweights;
mod blockstatus;
mod blockweightentry;
mod blocktimestamp;
mod blockweightentry;
mod bytes;
mod cents;
mod date;
@@ -62,9 +62,14 @@ mod loadedaddressindex;
mod mempoolblock;
mod mempoolentryinfo;
mod mempoolinfo;
mod datarange;
mod datarangeformat;
mod metric;
mod metriccount;
mod metrics;
mod metricselection;
mod metricselectionlegacy;
mod metricspaginated;
mod monthindex;
mod ohlc;
mod opreturnindex;
@@ -87,6 +92,8 @@ mod p2wpkhaddressindex;
mod p2wpkhbytes;
mod p2wshaddressindex;
mod p2wshbytes;
mod pagination;
mod paginationindex;
mod pool;
mod pooldetail;
mod poolinfo;
@@ -157,20 +164,20 @@ pub use blkmetadata::*;
pub use blkposition::*;
pub use block::*;
pub use blockcountpath::*;
pub use blockfeesentry::*;
pub use blockferatesentry::*;
pub use blockhash::*;
pub use blockhashpath::*;
pub use blockhashprefix::*;
pub use blockhashstartindexpath::*;
pub use blockhashtxindexpath::*;
pub use blockfeesentry::*;
pub use blockferatesentry::*;
pub use blockinfo::*;
pub use blockrewardsentry::*;
pub use blocksizeentry::*;
pub use blocksizesweights::*;
pub use blockstatus::*;
pub use blockweightentry::*;
pub use blocktimestamp::*;
pub use blockweightentry::*;
pub use bytes::*;
pub use cents::*;
pub use date::*;
@@ -201,9 +208,14 @@ pub use loadedaddressindex::*;
pub use mempoolblock::*;
pub use mempoolentryinfo::*;
pub use mempoolinfo::*;
pub use datarange::*;
pub use datarangeformat::*;
pub use metric::*;
pub use metriccount::*;
pub use metrics::*;
pub use metricselection::*;
pub use metricselectionlegacy::*;
pub use metricspaginated::*;
pub use monthindex::*;
pub use ohlc::*;
pub use opreturnindex::*;
@@ -226,6 +238,8 @@ pub use p2wpkhaddressindex::*;
pub use p2wpkhbytes::*;
pub use p2wshaddressindex::*;
pub use p2wshbytes::*;
pub use pagination::*;
pub use paginationindex::*;
pub use pool::*;
pub use pooldetail::*;
pub use poolinfo::*;
+50
View File
@@ -0,0 +1,50 @@
use std::ops::Deref;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{DataRangeFormat, Index, Metric, Metrics};
/// Selection of metrics to query
#[derive(Debug, Deserialize, JsonSchema)]
pub struct MetricSelection {
/// Requested metrics
#[serde(alias = "m")]
pub metrics: Metrics,
/// Index to query
#[serde(alias = "i")]
pub index: Index,
#[serde(flatten)]
pub range: DataRangeFormat,
}
impl Deref for MetricSelection {
type Target = DataRangeFormat;
fn deref(&self) -> &Self::Target {
&self.range
}
}
impl From<(Index, Metric, DataRangeFormat)> for MetricSelection {
#[inline]
fn from((index, metric, range): (Index, Metric, DataRangeFormat)) -> Self {
Self {
index,
metrics: Metrics::from(metric),
range,
}
}
}
impl From<(Index, Metrics, DataRangeFormat)> for MetricSelection {
#[inline]
fn from((index, metrics, range): (Index, Metrics, DataRangeFormat)) -> Self {
Self {
index,
metrics,
range,
}
}
}
@@ -0,0 +1,26 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{DataRangeFormat, Index, MetricSelection, Metrics};
/// Legacy metric selection parameters (deprecated)
#[derive(Debug, Deserialize, JsonSchema)]
pub struct MetricSelectionLegacy {
#[serde(alias = "i")]
pub index: Index,
#[serde(alias = "v")]
pub ids: Metrics,
#[serde(flatten)]
pub range: DataRangeFormat,
}
impl From<MetricSelectionLegacy> for MetricSelection {
#[inline]
fn from(value: MetricSelectionLegacy) -> Self {
MetricSelection {
index: value.index,
metrics: value.ids,
range: value.range,
}
}
}
+15
View File
@@ -0,0 +1,15 @@
use schemars::JsonSchema;
use serde::Serialize;
/// A paginated list of available metric names (1000 per page)
#[derive(Debug, Serialize, JsonSchema)]
pub struct PaginatedMetrics {
/// Current page number (0-indexed)
#[schemars(example = 0)]
pub current_page: usize,
/// Maximum valid page index (0-indexed)
#[schemars(example = 21)]
pub max_page: usize,
/// List of metric names (max 1000 per page)
pub metrics: &'static [&'static str],
}
+25
View File
@@ -0,0 +1,25 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct Pagination {
/// Pagination index
#[serde(default, alias = "p")]
pub page: Option<usize>,
}
impl Pagination {
pub const PER_PAGE: usize = 1_000;
pub fn page(&self) -> usize {
self.page.unwrap_or_default()
}
pub fn start(&self, len: usize) -> usize {
(self.page() * Self::PER_PAGE).clamp(0, len)
}
pub fn end(&self, len: usize) -> usize {
((self.page() + 1) * Self::PER_PAGE).clamp(0, len)
}
}
+13
View File
@@ -0,0 +1,13 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{Index, Pagination};
/// Pagination parameters with an index filter
#[derive(Debug, Deserialize, JsonSchema)]
pub struct PaginationIndex {
/// The index to filter by
pub index: Index,
#[serde(flatten)]
pub pagination: Pagination,
}