mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
server: snapshot
This commit is contained in:
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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}"),
|
||||
},
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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())
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mod hex;
|
||||
mod outspend;
|
||||
mod status;
|
||||
mod tx;
|
||||
|
||||
pub use hex::*;
|
||||
pub use outspend::*;
|
||||
pub use status::*;
|
||||
pub use tx::*;
|
||||
@@ -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),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod info;
|
||||
mod raw;
|
||||
mod status;
|
||||
mod timestamp;
|
||||
mod txs;
|
||||
|
||||
pub const BLOCK_TXS_PAGE_SIZE: usize = 25;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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 = ¶ms.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(¶ms)?, ¶ms.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(¶ms);
|
||||
|
||||
if vecs.is_empty() {
|
||||
return Ok(Output::default(params.range.format()));
|
||||
}
|
||||
|
||||
self.format(vecs.iter().collect(), ¶ms.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(¶ms);
|
||||
|
||||
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(), ¶ms.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
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -1,238 +1,39 @@
|
||||
# brk_server
|
||||
|
||||
HTTP server providing REST API access to Bitcoin analytics data
|
||||
HTTP server for the Bitcoin Research Kit.
|
||||
|
||||
[](https://crates.io/crates/brk_server)
|
||||
[](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<()>
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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(¶ms)?;
|
||||
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, ¶ms.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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,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()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms.cache_control);
|
||||
if let Some(etag) = ¶ms.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(¶ms.cache_control);
|
||||
if let Some(etag) = ¶ms.etag {
|
||||
headers.insert_etag(etag);
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
fn new_bytes_cached(value: Vec<u8>, params: &CacheParams) -> Self {
|
||||
let mut response = Response::builder().body(value.into()).unwrap();
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
headers.insert_content_type_octet_stream();
|
||||
headers.insert_cache_control(¶ms.cache_control);
|
||||
if let Some(etag) = ¶ms.etag {
|
||||
headers.insert_etag(etag);
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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>>,
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¶ms),
|
||||
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(), ¶ms),
|
||||
Err(e) => ResultExtended::<T>::to_text_response(Err(e), params.etag_str()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary response with caching
|
||||
pub async fn cached_bytes<T, F>(
|
||||
&self,
|
||||
headers: &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(), ¶ms),
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user