mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
server: snapshot
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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, ¶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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
use files::FilesRoutes;
|
||||
use state::*;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
|
||||
@@ -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, ¶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: &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(), ¶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: &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(), ¶ms),
|
||||
Err(e) => ResultExtended::<T>::to_bytes_response(Err(e), params.etag_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user