server: snapshot

This commit is contained in:
nym21
2025-12-15 17:33:49 +01:00
parent 825a4a77c0
commit 692a1889ab
8 changed files with 242 additions and 183 deletions
+1 -1
View File
@@ -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);
}
};
-53
View File
@@ -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(())
}
+93
View File
@@ -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(())
}
+5 -2
View File
@@ -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 {
+41 -41
View File
@@ -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()
},
+5 -86
View File
@@ -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<PathBuf>,
cache: Arc<Cache<String, Bytes>>,
}
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<T, F>(
&self,
headers: &axum::http::HeaderMap,
strategy: CacheStrategy,
f: F,
) -> axum::http::Response<axum::body::Body>
where
T: serde::Serialize + Send + 'static,
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + Send + 'static,
{
let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into()));
if params.matches_etag(headers) {
return ResponseExtended::new_not_modified();
}
match self.run(f).await {
Ok(value) => ResponseExtended::new_json_cached(&value, &params),
Err(e) => ResultExtended::<T>::to_json_response(Err(e), params.etag_str()),
}
}
/// Text response with caching
pub async fn cached_text<T, F>(
&self,
headers: &axum::http::HeaderMap,
strategy: CacheStrategy,
f: F,
) -> axum::http::Response<axum::body::Body>
where
T: AsRef<str> + Send + 'static,
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + Send + 'static,
{
let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into()));
if params.matches_etag(headers) {
return ResponseExtended::new_not_modified();
}
match self.run(f).await {
Ok(value) => ResponseExtended::new_text_cached(value.as_ref(), &params),
Err(e) => ResultExtended::<T>::to_text_response(Err(e), params.etag_str()),
}
}
/// Binary response with caching
pub async fn cached_bytes<T, F>(
&self,
headers: &axum::http::HeaderMap,
strategy: CacheStrategy,
f: F,
) -> axum::http::Response<axum::body::Body>
where
T: Into<Vec<u8>> + Send + 'static,
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + Send + 'static,
{
let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into()));
if params.matches_etag(headers) {
return ResponseExtended::new_not_modified();
}
match self.run(f).await {
Ok(value) => ResponseExtended::new_bytes_cached(value.into(), &params),
Err(e) => ResultExtended::<T>::to_bytes_response(Err(e), params.etag_str()),
}
}
}
use files::FilesRoutes;
use state::*;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
+93
View File
@@ -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<PathBuf>,
pub cache: Arc<Cache<String, Bytes>>,
}
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<T, F>(
&self,
headers: &HeaderMap,
strategy: CacheStrategy,
f: F,
) -> Response<Body>
where
T: Serialize + Send + 'static,
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + Send + 'static,
{
let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into()));
if params.matches_etag(headers) {
return ResponseExtended::new_not_modified();
}
match self.run(f).await {
Ok(value) => ResponseExtended::new_json_cached(&value, &params),
Err(e) => ResultExtended::<T>::to_json_response(Err(e), params.etag_str()),
}
}
/// Text response with caching
pub async fn cached_text<T, F>(
&self,
headers: &HeaderMap,
strategy: CacheStrategy,
f: F,
) -> Response<Body>
where
T: AsRef<str> + Send + 'static,
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + Send + 'static,
{
let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into()));
if params.matches_etag(headers) {
return ResponseExtended::new_not_modified();
}
match self.run(f).await {
Ok(value) => ResponseExtended::new_text_cached(value.as_ref(), &params),
Err(e) => ResultExtended::<T>::to_text_response(Err(e), params.etag_str()),
}
}
/// Binary response with caching
pub async fn cached_bytes<T, F>(
&self,
headers: &HeaderMap,
strategy: CacheStrategy,
f: F,
) -> Response<Body>
where
T: Into<Vec<u8>> + Send + 'static,
F: FnOnce(&brk_query::Query) -> brk_error::Result<T> + Send + 'static,
{
let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into()));
if params.matches_etag(headers) {
return ResponseExtended::new_not_modified();
}
match self.run(f).await {
Ok(value) => ResponseExtended::new_bytes_cached(value.into(), &params),
Err(e) => ResultExtended::<T>::to_bytes_response(Err(e), params.etag_str()),
}
}
}
+4
View File
@@ -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)