diff --git a/crates/brk_indexer/src/stores.rs b/crates/brk_indexer/src/stores.rs index e5026ce27..efcf9b815 100644 --- a/crates/brk_indexer/src/stores.rs +++ b/crates/brk_indexer/src/stores.rs @@ -40,7 +40,7 @@ impl Stores { Ok(database) => database, Err(_) => { fs::remove_dir_all(path)?; - return Self::forced_import(path, version); + return Self::forced_import(parent, version); } }; diff --git a/crates/brk_query/examples/main.rs b/crates/brk_query/examples/main.rs deleted file mode 100644 index d8fbead1b..000000000 --- a/crates/brk_query/examples/main.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{env, fs, path::Path}; - -use brk_computer::Computer; -use brk_error::Result; -use brk_indexer::Indexer; -use brk_query::Query; -use brk_reader::Reader; -use brk_rpc::{Auth, Client}; -use brk_types::{DataRangeFormat, Index, MetricSelection}; -use vecdb::Exit; - -pub fn main() -> Result<()> { - let bitcoin_dir = Client::default_bitcoin_path(); - // let bitcoin_dir = Path::new("/Volumes/WD_BLACK1/bitcoin"); - - let blocks_dir = bitcoin_dir.join("blocks"); - - let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk"); - fs::create_dir_all(&outputs_dir)?; - // let outputs_dir = Path::new("/Volumes/WD_BLACK1/brk"); - - let client = Client::new( - Client::default_url(), - Auth::CookieFile(bitcoin_dir.join(".cookie")), - )?; - - let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk"); - // let outputs_dir = Path::new("../../_outputs"); - - let exit = Exit::new(); - exit.set_ctrlc_handler(); - - let reader = Reader::new(blocks_dir, &client); - - let indexer = Indexer::forced_import(&outputs_dir)?; - - let computer = Computer::forced_import(&outputs_dir, &indexer, None)?; - - let query = Query::build(&reader, &indexer, &computer, None); - - dbg!(query.search_and_format(MetricSelection { - index: Index::Height, - metrics: vec!["date"].into(), - range: DataRangeFormat::default().set_from(-1), - })?); - dbg!(query.search_and_format(MetricSelection { - index: Index::Height, - metrics: vec!["date", "timestamp"].into(), - range: DataRangeFormat::default().set_from(-10).set_count(5), - })?); - - Ok(()) -} diff --git a/crates/brk_query/examples/query.rs b/crates/brk_query/examples/query.rs new file mode 100644 index 000000000..1caefa19c --- /dev/null +++ b/crates/brk_query/examples/query.rs @@ -0,0 +1,93 @@ +use std::{env, fs, path::Path, thread}; + +use brk_computer::Computer; +use brk_error::Result; +use brk_indexer::Indexer; +use brk_mempool::Mempool; +use brk_query::Query; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; +use brk_types::{Address, DataRangeFormat, Index, MetricSelection, OutputType}; +use vecdb::Exit; + +pub fn main() -> Result<()> { + // Can't increase main thread's stack size, thus we need to use another thread + thread::Builder::new() + .stack_size(512 * 1024 * 1024) + .spawn(run)? + .join() + .unwrap() +} + +fn run() -> Result<()> { + let bitcoin_dir = Client::default_bitcoin_path(); + // let bitcoin_dir = Path::new("/Volumes/WD_BLACK1/bitcoin"); + + let blocks_dir = bitcoin_dir.join("blocks"); + + let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk"); + fs::create_dir_all(&outputs_dir)?; + // let outputs_dir = Path::new("/Volumes/WD_BLACK1/brk"); + + let client = Client::new( + Client::default_url(), + Auth::CookieFile(bitcoin_dir.join(".cookie")), + )?; + + let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk"); + // let outputs_dir = Path::new("../../_outputs"); + + let exit = Exit::new(); + exit.set_ctrlc_handler(); + + let reader = Reader::new(blocks_dir, &client); + + let indexer = Indexer::forced_import(&outputs_dir)?; + + let computer = Computer::forced_import(&outputs_dir, &indexer, None)?; + + let mempool = Mempool::new(&client); + let mempool_clone = mempool.clone(); + thread::spawn(move || { + mempool_clone.start(); + }); + + let query = Query::build(&reader, &indexer, &computer, Some(mempool)); + + dbg!( + indexer + .stores + .addresstype_to_addresshash_to_addressindex + .get_unwrap(OutputType::P2WSH) + .approximate_len() + ); + + dbg!(query.address(Address { + address: "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38".to_string(), + })); + + dbg!(query.address_txids( + Address { + address: "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38".to_string(), + }, + None, + 25 + )); + + dbg!(query.address_utxos(Address { + address: "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38".to_string(), + })); + + // dbg!(query.search_and_format(MetricSelection { + // index: Index::Height, + // metrics: vec!["date"].into(), + // range: DataRangeFormat::default().set_from(-1), + // })?); + // dbg!(query.search_and_format(MetricSelection { + // index: Index::Height, + // metrics: vec!["date", "timestamp"].into(), + // range: DataRangeFormat::default().set_from(-10).set_count(5), + // })?); + + Ok(()) +} diff --git a/crates/brk_query/src/impl/address.rs b/crates/brk_query/src/impl/address.rs index d4ae2f8fd..12ec6f7a2 100644 --- a/crates/brk_query/src/impl/address.rs +++ b/crates/brk_query/src/impl/address.rs @@ -32,17 +32,20 @@ impl Query { return Err(Error::InvalidAddress); }; + dbg!(&script); + let outputtype = OutputType::from(&script); + dbg!(outputtype); 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); + dbg!(hash); let Ok(Some(type_index)) = stores .addresstype_to_addresshash_to_addressindex - .get(addresstype) - .unwrap() + .get_unwrap(addresstype) .get(&hash) .map(|opt| opt.map(|cow| cow.into_owned())) else { diff --git a/crates/brk_server/src/api/openapi.rs b/crates/brk_server/src/api/openapi.rs index 190da48a7..754fbe5c7 100644 --- a/crates/brk_server/src/api/openapi.rs +++ b/crates/brk_server/src/api/openapi.rs @@ -27,11 +27,11 @@ pub fn create_openapi() -> OpenApi { let tags = vec![ Tag { - name: "Addresses".to_string(), + name: "Metrics".to_string(), description: Some( - "Query Bitcoin address data including balances, transaction history, and UTXOs. \ - Supports all address types: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR." - .to_string() + "Access Bitcoin network metrics and time-series data. Query historical statistics \ + across various indexes with JSON or CSV output." + .to_string(), ), ..Default::default() }, @@ -40,42 +40,7 @@ pub fn create_openapi() -> OpenApi { description: Some( "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() - }, - Tag { - name: "Metrics".to_string(), - description: Some( - "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() - }, - Tag { - name: "Mining".to_string(), - description: Some( - "Mining statistics including pool distribution, hashrate, difficulty adjustments, \ - block rewards, and fee rates across configurable time periods." - .to_string() - ), - ..Default::default() - }, - Tag { - name: "Server".to_string(), - description: Some( - "API metadata and health monitoring. Version information and service status." - .to_string() + .to_string(), ), ..Default::default() }, @@ -84,7 +49,42 @@ pub fn create_openapi() -> OpenApi { description: Some( "Retrieve transaction data by txid. Access full transaction details, confirmation \ status, raw hex, and output spend information." - .to_string() + .to_string(), + ), + ..Default::default() + }, + Tag { + name: "Addresses".to_string(), + description: Some( + "Query Bitcoin address data including balances, transaction history, and UTXOs. \ + Supports all address types: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR." + .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() + }, + Tag { + name: "Mining".to_string(), + description: Some( + "Mining statistics including pool distribution, hashrate, difficulty adjustments, \ + block rewards, and fee rates across configurable time periods." + .to_string(), + ), + ..Default::default() + }, + Tag { + name: "Server".to_string(), + description: Some( + "API metadata and health monitoring. Version information and service status." + .to_string(), ), ..Default::default() }, diff --git a/crates/brk_server/src/lib.rs b/crates/brk_server/src/lib.rs index 5d973f15a..d36da938a 100644 --- a/crates/brk_server/src/lib.rs +++ b/crates/brk_server/src/lib.rs @@ -1,15 +1,11 @@ #![doc = include_str!("../README.md")] -#![doc = "\n## Example\n\n```rust"] -#![doc = include_str!("../examples/server.rs")] -#![doc = "```"] -use std::{ops::Deref, path::PathBuf, sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use aide::axum::ApiRouter; -use api::ApiRoutes; use axum::{ Extension, - body::{Body, Bytes}, + body::Body, http::{Request, Response, StatusCode, Uri}, middleware::Next, response::Redirect, @@ -20,7 +16,6 @@ use brk_error::Result; use brk_logger::OwoColorize; use brk_mcp::route::MCPRoutes; use brk_query::AsyncQuery; -use files::FilesRoutes; use log::{error, info}; use quick_cache::sync::Cache; use tokio::net::TcpListener; @@ -31,89 +26,13 @@ mod api; pub mod cache; mod extended; mod files; +mod state; use api::*; pub use cache::{CacheParams, CacheStrategy}; use extended::*; - -#[derive(Clone)] -pub struct AppState { - query: AsyncQuery, - path: Option, - cache: Arc>, -} - -impl Deref for AppState { - type Target = AsyncQuery; - fn deref(&self) -> &Self::Target { - &self.query - } -} - -impl AppState { - /// JSON response with caching - pub async fn cached_json( - &self, - headers: &axum::http::HeaderMap, - strategy: CacheStrategy, - f: F, - ) -> axum::http::Response - where - T: serde::Serialize + Send + 'static, - F: FnOnce(&brk_query::Query) -> brk_error::Result + 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::::to_json_response(Err(e), params.etag_str()), - } - } - - /// Text response with caching - pub async fn cached_text( - &self, - headers: &axum::http::HeaderMap, - strategy: CacheStrategy, - f: F, - ) -> axum::http::Response - where - T: AsRef + Send + 'static, - F: FnOnce(&brk_query::Query) -> brk_error::Result + 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::::to_text_response(Err(e), params.etag_str()), - } - } - - /// Binary response with caching - pub async fn cached_bytes( - &self, - headers: &axum::http::HeaderMap, - strategy: CacheStrategy, - f: F, - ) -> axum::http::Response - where - T: Into> + Send + 'static, - F: FnOnce(&brk_query::Query) -> brk_error::Result + 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::::to_bytes_response(Err(e), params.etag_str()), - } - } -} +use files::FilesRoutes; +use state::*; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/brk_server/src/state.rs b/crates/brk_server/src/state.rs new file mode 100644 index 000000000..a6c3244fe --- /dev/null +++ b/crates/brk_server/src/state.rs @@ -0,0 +1,93 @@ +use std::{ops::Deref, path::PathBuf, sync::Arc}; + +use axum::{ + body::{Body, Bytes}, + http::{HeaderMap, Response}, +}; +use brk_query::AsyncQuery; +use quick_cache::sync::Cache; +use serde::Serialize; + +use crate::{ + CacheParams, CacheStrategy, + extended::{ResponseExtended, ResultExtended}, +}; + +#[derive(Clone)] +pub struct AppState { + pub query: AsyncQuery, + pub path: Option, + pub cache: Arc>, +} + +impl Deref for AppState { + type Target = AsyncQuery; + fn deref(&self) -> &Self::Target { + &self.query + } +} + +impl AppState { + /// JSON response with caching + pub async fn cached_json( + &self, + headers: &HeaderMap, + strategy: CacheStrategy, + f: F, + ) -> Response + where + T: Serialize + Send + 'static, + F: FnOnce(&brk_query::Query) -> brk_error::Result + 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::::to_json_response(Err(e), params.etag_str()), + } + } + + /// Text response with caching + pub async fn cached_text( + &self, + headers: &HeaderMap, + strategy: CacheStrategy, + f: F, + ) -> Response + where + T: AsRef + Send + 'static, + F: FnOnce(&brk_query::Query) -> brk_error::Result + 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::::to_text_response(Err(e), params.etag_str()), + } + } + + /// Binary response with caching + pub async fn cached_bytes( + &self, + headers: &HeaderMap, + strategy: CacheStrategy, + f: F, + ) -> Response + where + T: Into> + Send + 'static, + F: FnOnce(&brk_query::Query) -> brk_error::Result + 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::::to_bytes_response(Err(e), params.etag_str()), + } + } +} diff --git a/crates/brk_store/src/lib.rs b/crates/brk_store/src/lib.rs index 17701370b..d4f8eeace 100644 --- a/crates/brk_store/src/lib.rs +++ b/crates/brk_store/src/lib.rs @@ -232,6 +232,10 @@ where .map(|(k, v)| (K::from(ByteView::from(&*k)), V::from(ByteView::from(&*v)))) } + pub fn approximate_len(&self) -> usize { + self.keyspace.approximate_len() + } + #[inline] fn has(&self, height: Height) -> bool { self.meta.has(height)