server: api doc part 3

This commit is contained in:
nym21
2025-10-08 17:48:15 +02:00
parent a53f89c849
commit 114228e8eb
29 changed files with 645 additions and 319 deletions
Generated
+2
View File
@@ -1163,6 +1163,7 @@ dependencies = [
"brk_mcp", "brk_mcp",
"brk_parser", "brk_parser",
"brk_structs", "brk_structs",
"brk_traversable",
"jiff", "jiff",
"log", "log",
"quick_cache", "quick_cache",
@@ -1231,6 +1232,7 @@ name = "brk_traversable"
version = "0.0.111" version = "0.0.111"
dependencies = [ dependencies = [
"brk_traversable_derive", "brk_traversable_derive",
"schemars 1.0.4",
"serde", "serde",
"vecdb", "vecdb",
] ]
+1 -1
View File
@@ -41,7 +41,7 @@ panic = "abort"
debug-assertions = false debug-assertions = false
[workspace.dependencies] [workspace.dependencies]
aide = { version = "0.15.1", features = ["axum-json"], package = "brk-aide" } aide = { version = "0.15.1", features = ["axum-json", "axum-query"], package = "brk-aide" }
allocative = { version = "0.3.4", features = ["parking_lot"] } allocative = { version = "0.3.4", features = ["parking_lot"] }
axum = "0.8.6" axum = "0.8.6"
bitcoin = { version = "0.32.7", features = ["serde"] } bitcoin = { version = "0.32.7", features = ["serde"] }
+2 -2
View File
@@ -4,8 +4,8 @@ use std::{
path::Path, path::Path,
}; };
use brk_interface::{Index, Interface}; use brk_interface::Interface;
use brk_structs::pools; use brk_structs::{Index, pools};
use super::VERSION; use super::VERSION;
@@ -150,22 +150,33 @@ where
T: ComputedType, T: ComputedType,
{ {
fn to_tree_node(&self) -> brk_traversable::TreeNode { fn to_tree_node(&self) -> brk_traversable::TreeNode {
brk_traversable::TreeNode::List( let dateindex_extra_node = self.dateindex_extra.to_tree_node();
brk_traversable::TreeNode::Branch(
[ [
self.dateindex.as_ref().map(|nested| nested.to_tree_node()), self.dateindex
Some(self.dateindex_extra.to_tree_node()), .as_ref()
Some(self.weekindex.to_tree_node()), .map(|nested| ("dateindex".to_string(), nested.to_tree_node())),
Some(self.monthindex.to_tree_node()), if dateindex_extra_node.is_empty() {
Some(self.quarterindex.to_tree_node()), None
Some(self.semesterindex.to_tree_node()), } else {
Some(self.yearindex.to_tree_node()), Some(("dateindex_extra".to_string(), dateindex_extra_node))
Some(self.decadeindex.to_tree_node()), },
Some(("weekindex".to_string(), self.weekindex.to_tree_node())),
Some(("monthindex".to_string(), self.monthindex.to_tree_node())),
Some(("quarterindex".to_string(), self.quarterindex.to_tree_node())),
Some((
"semesterindex".to_string(),
self.semesterindex.to_tree_node(),
)),
Some(("yearindex".to_string(), self.yearindex.to_tree_node())),
Some(("decadeindex".to_string(), self.decadeindex.to_tree_node())),
] ]
.into_iter() .into_iter()
.flatten() .flatten()
.collect(), .collect(),
) )
.collect_unique_leaves() .merge_branches()
.unwrap()
} }
fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> { fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> {
+26 -12
View File
@@ -206,24 +206,38 @@ where
T: ComputedType, T: ComputedType,
{ {
fn to_tree_node(&self) -> brk_traversable::TreeNode { fn to_tree_node(&self) -> brk_traversable::TreeNode {
brk_traversable::TreeNode::List( let height_extra_node = self.height_extra.to_tree_node();
brk_traversable::TreeNode::Branch(
[ [
self.height.as_ref().map(|nested| nested.to_tree_node()), self.height
Some(self.height_extra.to_tree_node()), .as_ref()
Some(self.dateindex.to_tree_node()), .map(|nested| ("height".to_string(), nested.to_tree_node())),
Some(self.weekindex.to_tree_node()), if height_extra_node.is_empty() {
Some(self.difficultyepoch.to_tree_node()), None
Some(self.monthindex.to_tree_node()), } else {
Some(self.quarterindex.to_tree_node()), Some(("height_extra".to_string(), height_extra_node))
Some(self.semesterindex.to_tree_node()), },
Some(self.yearindex.to_tree_node()), Some(("dateindex".to_string(), self.dateindex.to_tree_node())),
Some(self.decadeindex.to_tree_node()), Some(("weekindex".to_string(), self.weekindex.to_tree_node())),
Some((
"difficultyepoch".to_string(),
self.difficultyepoch.to_tree_node(),
)),
Some(("monthindex".to_string(), self.monthindex.to_tree_node())),
Some(("quarterindex".to_string(), self.quarterindex.to_tree_node())),
Some((
"semesterindex".to_string(),
self.semesterindex.to_tree_node(),
)),
Some(("yearindex".to_string(), self.yearindex.to_tree_node())),
Some(("decadeindex".to_string(), self.decadeindex.to_tree_node())),
] ]
.into_iter() .into_iter()
.flatten() .flatten()
.collect(), .collect(),
) )
.collect_unique_leaves() .merge_branches()
.unwrap()
} }
fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> { fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> {
@@ -89,17 +89,26 @@ where
T: ComputedType, T: ComputedType,
{ {
fn to_tree_node(&self) -> brk_traversable::TreeNode { fn to_tree_node(&self) -> brk_traversable::TreeNode {
brk_traversable::TreeNode::List( let height_extra_node = self.height_extra.to_tree_node();
brk_traversable::TreeNode::Branch(
[ [
Some(self.height.to_tree_node()), Some(("height".to_string(), self.height.to_tree_node())),
Some(self.height_extra.to_tree_node()), if height_extra_node.is_empty() {
Some(self.difficultyepoch.to_tree_node()), None
} else {
Some(("height_extra".to_string(), height_extra_node))
},
Some((
"difficultyepoch".to_string(),
self.difficultyepoch.to_tree_node(),
)),
] ]
.into_iter() .into_iter()
.flatten() .flatten()
.collect(), .collect(),
) )
.collect_unique_leaves() .merge_branches()
.unwrap()
} }
fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> { fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> {
let mut regular_iter: Box<dyn Iterator<Item = &dyn vecdb::AnyCollectableVec>> = let mut regular_iter: Box<dyn Iterator<Item = &dyn vecdb::AnyCollectableVec>> =
+21 -12
View File
@@ -593,24 +593,33 @@ where
T: ComputedType, T: ComputedType,
{ {
fn to_tree_node(&self) -> brk_traversable::TreeNode { fn to_tree_node(&self) -> brk_traversable::TreeNode {
brk_traversable::TreeNode::List( brk_traversable::TreeNode::Branch(
[ [
self.txindex.as_ref().map(|nested| nested.to_tree_node()), self.txindex
Some(self.height.to_tree_node()), .as_ref()
Some(self.dateindex.to_tree_node()), .map(|nested| ("txindex".to_string(), nested.to_tree_node())),
Some(self.weekindex.to_tree_node()), Some(("height".to_string(), self.height.to_tree_node())),
Some(self.difficultyepoch.to_tree_node()), Some(("dateindex".to_string(), self.dateindex.to_tree_node())),
Some(self.monthindex.to_tree_node()), Some(("weekindex".to_string(), self.weekindex.to_tree_node())),
Some(self.quarterindex.to_tree_node()), Some((
Some(self.semesterindex.to_tree_node()), "difficultyepoch".to_string(),
Some(self.yearindex.to_tree_node()), self.difficultyepoch.to_tree_node(),
Some(self.decadeindex.to_tree_node()), )),
Some(("monthindex".to_string(), self.monthindex.to_tree_node())),
Some(("quarterindex".to_string(), self.quarterindex.to_tree_node())),
Some((
"semesterindex".to_string(),
self.semesterindex.to_tree_node(),
)),
Some(("yearindex".to_string(), self.yearindex.to_tree_node())),
Some(("decadeindex".to_string(), self.decadeindex.to_tree_node())),
] ]
.into_iter() .into_iter()
.flatten() .flatten()
.collect(), .collect(),
) )
.collect_unique_leaves() .merge_branches()
.unwrap()
} }
fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> { fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn vecdb::AnyCollectableVec> {
+2 -1
View File
@@ -3,8 +3,9 @@ use std::{fs, path::Path};
use brk_computer::Computer; use brk_computer::Computer;
use brk_error::Result; use brk_error::Result;
use brk_indexer::Indexer; use brk_indexer::Indexer;
use brk_interface::{Index, Interface, Params, ParamsOpt}; use brk_interface::{Interface, Params, ParamsOpt};
use brk_parser::Parser; use brk_parser::Parser;
use brk_structs::Index;
use vecdb::Exit; use vecdb::Exit;
pub fn main() -> Result<()> { pub fn main() -> Result<()> {
+13
View File
@@ -0,0 +1,13 @@
use schemars::JsonSchema;
use serde::Serialize;
#[derive(Debug, Serialize, JsonSchema)]
/// Metric count statistics - distinct metrics and total metric-index combinations
pub struct MetricCount {
#[schemars(example = 3141)]
/// Number of unique metrics available (e.g., realized_price, market_cap)
pub distinct_metrics: usize,
#[schemars(example = 21000)]
/// Total number of metric-index combinations across all timeframes
pub total_endpoints: usize,
}
+13 -7
View File
@@ -6,7 +6,7 @@ use brk_computer::Computer;
use brk_error::{Error, Result}; use brk_error::{Error, Result};
use brk_indexer::Indexer; use brk_indexer::Indexer;
use brk_parser::Parser; use brk_parser::Parser;
use brk_structs::Height; use brk_structs::{Height, Index, IndexInfo};
use brk_traversable::TreeNode; use brk_traversable::TreeNode;
use nucleo_matcher::{ use nucleo_matcher::{
Config, Matcher, Config, Matcher,
@@ -15,21 +15,20 @@ use nucleo_matcher::{
use quick_cache::sync::Cache; use quick_cache::sync::Cache;
use vecdb::{AnyCollectableVec, AnyStoredVec}; use vecdb::{AnyCollectableVec, AnyStoredVec};
mod count;
mod deser; mod deser;
mod format; mod format;
mod index;
mod metrics; mod metrics;
mod output; mod output;
mod pagination; mod pagination;
mod params; mod params;
mod vecs; mod vecs;
pub use count::*;
pub use format::Format; pub use format::Format;
pub use index::*;
pub use output::{Output, Value}; pub use output::{Output, Value};
pub use pagination::{PaginatedIndexParam, PaginationParam}; pub use pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam};
pub use params::{Params, ParamsDeprec, ParamsOpt}; pub use params::{Params, ParamsDeprec, ParamsOpt};
pub use vecs::PaginatedMetrics;
use vecs::Vecs; use vecs::Vecs;
use crate::vecs::{IndexToVec, MetricToVec}; use crate::vecs::{IndexToVec, MetricToVec};
@@ -229,6 +228,13 @@ impl<'a> Interface<'a> {
&self.vecs.index_to_metric_to_vec &self.vecs.index_to_metric_to_vec
} }
pub fn metric_count(&self) -> MetricCount {
MetricCount {
distinct_metrics: self.distinct_metric_count(),
total_endpoints: self.total_metric_count(),
}
}
pub fn distinct_metric_count(&self) -> usize { pub fn distinct_metric_count(&self) -> usize {
self.vecs.distinct_metric_count self.vecs.distinct_metric_count
} }
@@ -237,7 +243,7 @@ impl<'a> Interface<'a> {
self.vecs.total_metric_count self.vecs.total_metric_count
} }
pub fn get_indexes(&self) -> &Indexes { pub fn get_indexes(&self) -> &[IndexInfo] {
&self.vecs.indexes &self.vecs.indexes
} }
@@ -253,7 +259,7 @@ impl<'a> Interface<'a> {
self.vecs.index_to_ids(paginated_index) self.vecs.index_to_ids(paginated_index)
} }
pub fn metric_to_indexes(&self, metric: String) -> Option<&Vec<&'static str>> { pub fn metric_to_indexes(&self, metric: String) -> Option<&Vec<Index>> {
self.vecs.metric_to_indexes(metric) self.vecs.metric_to_indexes(metric)
} }
+16 -1
View File
@@ -1,7 +1,8 @@
use brk_structs::Index;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{Index, deser::de_unquote_usize}; use crate::deser::de_unquote_usize;
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct PaginationParam { pub struct PaginationParam {
@@ -28,3 +29,17 @@ pub struct PaginatedIndexParam {
#[serde(flatten)] #[serde(flatten)]
pub pagination: PaginationParam, 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],
}
+8 -13
View File
@@ -1,22 +1,23 @@
use std::ops::Deref; use std::ops::Deref;
use brk_structs::Index;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Format, Index, Format,
deser::{de_unquote_i64, de_unquote_usize}, deser::{de_unquote_i64, de_unquote_usize},
metrics::MaybeMetrics, metrics::MaybeMetrics,
}; };
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
pub struct Params { pub struct Params {
/// Requested metrics
#[serde(alias = "m")] #[serde(alias = "m")]
#[schemars(description = "Requested metrics")]
pub metrics: MaybeMetrics, pub metrics: MaybeMetrics,
/// Requested index
#[serde(alias = "i")] #[serde(alias = "i")]
#[schemars(description = "Requested index")]
pub index: Index, pub index: Index,
#[serde(flatten)] #[serde(flatten)]
@@ -42,26 +43,20 @@ impl From<((Index, String), ParamsOpt)> for Params {
#[derive(Default, Debug, Deserialize, JsonSchema)] #[derive(Default, Debug, Deserialize, JsonSchema)]
pub struct ParamsOpt { pub struct ParamsOpt {
#[serde(default, alias = "f", deserialize_with = "de_unquote_i64")]
/// Inclusive starting index, if negative will be from the end /// Inclusive starting index, if negative will be from the end
#[schemars(description = "Inclusive starting index, if negative will be from the end")] #[serde(default, alias = "f", deserialize_with = "de_unquote_i64")]
from: Option<i64>, from: Option<i64>,
#[serde(default, alias = "t", deserialize_with = "de_unquote_i64")]
/// Exclusive ending index, if negative will be from the end, overrides 'count' /// Exclusive ending index, if negative will be from the end, overrides 'count'
#[schemars( #[serde(default, alias = "t", deserialize_with = "de_unquote_i64")]
description = "Exclusive ending index, if negative will be from the end, overrides 'count'"
)]
to: Option<i64>, to: Option<i64>,
#[serde(default, alias = "c", deserialize_with = "de_unquote_usize")]
/// Number of values requested /// Number of values requested
#[schemars(description = "Number of values requested")] #[serde(default, alias = "c", deserialize_with = "de_unquote_usize")]
count: Option<usize>, count: Option<usize>,
#[serde(default)]
/// Format of the output /// Format of the output
#[schemars(description = "Format of the output")] #[serde(default)]
format: Format, format: Format,
} }
+16 -41
View File
@@ -2,29 +2,23 @@ use std::collections::BTreeMap;
use brk_computer::Computer; use brk_computer::Computer;
use brk_indexer::Indexer; use brk_indexer::Indexer;
use brk_structs::{Index, IndexInfo};
use brk_traversable::{Traversable, TreeNode}; use brk_traversable::{Traversable, TreeNode};
use derive_deref::{Deref, DerefMut}; use derive_deref::{Deref, DerefMut};
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::AnyCollectableVec; use vecdb::AnyCollectableVec;
use crate::{ use crate::pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam};
index::Indexes,
pagination::{PaginatedIndexParam, PaginationParam},
};
use super::index::Index;
#[derive(Default)] #[derive(Default)]
pub struct Vecs<'a> { pub struct Vecs<'a> {
pub metric_to_index_to_vec: BTreeMap<&'a str, IndexToVec<'a>>, pub metric_to_index_to_vec: BTreeMap<&'a str, IndexToVec<'a>>,
pub index_to_metric_to_vec: BTreeMap<Index, MetricToVec<'a>>, pub index_to_metric_to_vec: BTreeMap<Index, MetricToVec<'a>>,
pub metrics: Vec<&'a str>, pub metrics: Vec<&'a str>,
pub indexes: Indexes, pub indexes: Vec<IndexInfo>,
pub distinct_metric_count: usize, pub distinct_metric_count: usize,
pub total_metric_count: usize, pub total_metric_count: usize,
pub catalog: Option<TreeNode>, pub catalog: Option<TreeNode>,
metric_to_indexes: BTreeMap<&'a str, Vec<&'static str>>, metric_to_indexes: BTreeMap<&'a str, Vec<Index>>,
index_to_metrics: BTreeMap<Index, Vec<&'a str>>, index_to_metrics: BTreeMap<Index, Vec<&'a str>>,
} }
@@ -67,24 +61,19 @@ impl<'a> Vecs<'a> {
.values() .values()
.map(|tree| tree.len()) .map(|tree| tree.len())
.sum::<usize>(); .sum::<usize>();
this.indexes = Indexes::new( this.indexes = this
this.index_to_metric_to_vec .index_to_metric_to_vec
.keys() .keys()
.map(|i| (*i, i.possible_values())) .map(|i| IndexInfo {
.collect::<BTreeMap<_, _>>(), index: *i,
); aliases: i.possible_values(),
})
.collect();
this.metric_to_indexes = this this.metric_to_indexes = this
.metric_to_index_to_vec .metric_to_index_to_vec
.iter() .iter()
.map(|(id, index_to_vec)| { .map(|(id, index_to_vec)| (*id, index_to_vec.keys().copied().collect::<Vec<_>>()))
(
*id,
index_to_vec
.keys()
.map(|i| i.serialize_long())
.collect::<Vec<_>>(),
)
})
.collect(); .collect();
this.index_to_metrics = this this.index_to_metrics = this
.index_to_metric_to_vec .index_to_metric_to_vec
@@ -142,12 +131,12 @@ impl<'a> Vecs<'a> {
PaginatedMetrics { PaginatedMetrics {
current_page: pagination.page.unwrap_or_default(), current_page: pagination.page.unwrap_or_default(),
total_pages: len / PaginationParam::PER_PAGE, max_page: len.div_ceil(PaginationParam::PER_PAGE).saturating_sub(1),
metrics: &self.metrics[start..end], metrics: &self.metrics[start..end],
} }
} }
pub fn metric_to_indexes(&self, metric: String) -> Option<&Vec<&'static str>> { pub fn metric_to_indexes(&self, metric: String) -> Option<&Vec<Index>> {
self.metric_to_indexes self.metric_to_indexes
.get(metric.replace("-", "_").as_str()) .get(metric.replace("-", "_").as_str())
} }
@@ -171,17 +160,3 @@ pub struct IndexToVec<'a>(BTreeMap<Index, &'a dyn AnyCollectableVec>);
#[derive(Default, Deref, DerefMut)] #[derive(Default, Deref, DerefMut)]
pub struct MetricToVec<'a>(BTreeMap<&'a str, &'a dyn AnyCollectableVec>); pub struct MetricToVec<'a>(BTreeMap<&'a str, &'a dyn AnyCollectableVec>);
/// 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)]
current_page: usize,
/// Total number of pages available
#[schemars(example = 21000)]
total_pages: usize,
/// List of metric names (max 1000 per page)
#[schemars(example = ["price_open", "price_close", "realized_price", "..."])]
metrics: &'static [&'static str],
}
+2 -1
View File
@@ -10,7 +10,7 @@ rust-version.workspace = true
build = "build.rs" build = "build.rs"
[dependencies] [dependencies]
aide = { workspace = true , features = ["axum-json", "axum-query"] } aide = { workspace = true }
axum = { workspace = true } axum = { workspace = true }
bitcoin = { workspace = true } bitcoin = { workspace = true }
bitcoincore-rpc = { workspace = true } bitcoincore-rpc = { workspace = true }
@@ -23,6 +23,7 @@ brk_logger = { workspace = true }
brk_mcp = { workspace = true } brk_mcp = { workspace = true }
brk_parser = { workspace = true } brk_parser = { workspace = true }
brk_structs = { workspace = true } brk_structs = { workspace = true }
brk_traversable = { workspace = true }
vecdb = { workspace = true } vecdb = { workspace = true }
jiff = { workspace = true } jiff = { workspace = true }
log = { workspace = true } log = { workspace = true }
+31 -12
View File
@@ -18,6 +18,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vecdb::{AnyIterableVec, VecIterator}; use vecdb::{AnyIterableVec, VecIterator};
use crate::extended::TransformResponseExtended;
use super::AppState; use super::AppState;
#[derive(Debug, Serialize, JsonSchema)] #[derive(Debug, Serialize, JsonSchema)]
@@ -88,13 +90,14 @@ struct AddressInfo {
#[derive(Deserialize, JsonSchema)] #[derive(Deserialize, JsonSchema)]
struct AddressPath { struct AddressPath {
/// Bitcoin address string /// Bitcoin address string
#[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")]
address: String, address: String,
} }
async fn get_address_info( async fn get_address_info(
Path(AddressPath { address }): Path<AddressPath>, Path(AddressPath { address }): Path<AddressPath>,
state: State<AppState>, state: State<AppState>,
) -> Result<Json<AddressInfo>, StatusCode> { ) -> Result<Json<AddressInfo>, (StatusCode, Json<&'static str>)> {
let interface = state.interface; let interface = state.interface;
let indexer = interface.indexer(); let indexer = interface.indexer();
let computer = interface.computer(); let computer = interface.computer();
@@ -102,19 +105,28 @@ async fn get_address_info(
let script = if let Ok(address) = BitcoinAddress::from_str(&address) { let script = if let Ok(address) = BitcoinAddress::from_str(&address) {
if !address.is_valid_for_network(Network::Bitcoin) { if !address.is_valid_for_network(Network::Bitcoin) {
return Err(StatusCode::BAD_REQUEST); return Err((
StatusCode::BAD_REQUEST,
Json("The provided address isn't the Bitcoin Network."),
));
} }
let address = address.assume_checked(); let address = address.assume_checked();
address.script_pubkey() address.script_pubkey()
} else if let Ok(pubkey) = PublicKey::from_str(&address) { } else if let Ok(pubkey) = PublicKey::from_str(&address) {
ScriptBuf::new_p2pk(&pubkey) ScriptBuf::new_p2pk(&pubkey)
} else { } else {
return Err(StatusCode::BAD_REQUEST); return Err((
StatusCode::BAD_REQUEST,
Json("The provided address is invalid."),
));
}; };
let type_ = OutputType::from(&script); let type_ = OutputType::from(&script);
let Ok(bytes) = AddressBytes::try_from((&script, type_)) else { let Ok(bytes) = AddressBytes::try_from((&script, type_)) else {
return Err(StatusCode::BAD_REQUEST); return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json("Failed to convert the address to bytes"),
));
}; };
let hash = AddressBytesHash::from((&bytes, type_)); let hash = AddressBytesHash::from((&bytes, type_));
@@ -123,7 +135,10 @@ async fn get_address_info(
.get(&hash) .get(&hash)
.map(|opt| opt.map(|cow| cow.into_owned())) .map(|opt| opt.map(|cow| cow.into_owned()))
else { else {
return Err(StatusCode::NOT_FOUND); return Err((
StatusCode::NOT_FOUND,
Json("Address not found in the blockchain (no transaction history)"),
));
}; };
let stateful = &computer.stateful; let stateful = &computer.stateful;
@@ -172,7 +187,12 @@ async fn get_address_info(
.p2aaddressindex_to_anyaddressindex .p2aaddressindex_to_anyaddressindex
.iter() .iter()
.unwrap_get_inner(type_index.into()), .unwrap_get_inner(type_index.into()),
_ => return Err(StatusCode::BAD_REQUEST), _ => {
return Err((
StatusCode::BAD_REQUEST,
Json("The provided address uses an unsupported type"),
));
}
}; };
let address_data = match any_address_index.to_enum() { let address_data = match any_address_index.to_enum() {
@@ -207,12 +227,11 @@ fn get_address_info_docs(op: TransformOperation) -> TransformOperation {
op.tag("Chain") op.tag("Chain")
.summary("Address information") .summary("Address information")
.description("Retrieve comprehensive information about a Bitcoin address including balance, transaction history, UTXOs, and estimated investment metrics. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, etc.).") .description("Retrieve comprehensive information about a Bitcoin address including balance, transaction history, UTXOs, and estimated investment metrics. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, etc.).")
.response_with::<400, (), _>(|res| .with_ok_response::<AddressInfo, _>(|res| res)
res.description("Invalid address format or unsupported address type") .with_not_modified()
) .with_bad_request()
.response_with::<404, (), _>(|res| .with_not_found()
res.description("Address not found in the blockchain (no transaction history)") .with_server_error()
)
} }
pub trait AddressesRoutes { pub trait AddressesRoutes {
+4 -1
View File
@@ -1,4 +1,5 @@
use aide::axum::ApiRouter; use aide::axum::ApiRouter;
use axum::{response::Redirect, routing::get};
use crate::api::chain::{addresses::AddressesRoutes, transactions::TransactionsRoutes}; use crate::api::chain::{addresses::AddressesRoutes, transactions::TransactionsRoutes};
@@ -13,6 +14,8 @@ pub trait ChainRoutes {
impl ChainRoutes for ApiRouter<AppState> { impl ChainRoutes for ApiRouter<AppState> {
fn add_chain_routes(self) -> Self { fn add_chain_routes(self) -> Self {
self.add_addresses_routes().add_transactions_routes() self.route("/api/chain", get(Redirect::temporary("/api#tag/chain")))
.add_addresses_routes()
.add_transactions_routes()
} }
} }
+37 -22
View File
@@ -21,6 +21,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vecdb::VecIterator; use vecdb::VecIterator;
use crate::extended::TransformResponseExtended;
use super::AppState; use super::AppState;
#[derive(Serialize, JsonSchema)] #[derive(Serialize, JsonSchema)]
@@ -41,15 +43,19 @@ struct TransactionInfo {
#[derive(Deserialize, JsonSchema)] #[derive(Deserialize, JsonSchema)]
struct TxidPath { struct TxidPath {
/// Bitcoin transaction id /// Bitcoin transaction id
#[schemars(example = &"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")]
txid: String, txid: String,
} }
async fn get_transaction_info( async fn get_transaction_info(
Path(TxidPath { txid }): Path<TxidPath>, Path(TxidPath { txid }): Path<TxidPath>,
state: State<AppState>, state: State<AppState>,
) -> Result<Response, StatusCode> { ) -> Result<Response, (StatusCode, Json<&'static str>)> {
let Ok(txid) = bitcoin::Txid::from_str(&txid) else { let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
return Err(StatusCode::BAD_REQUEST); return Err((
StatusCode::BAD_REQUEST,
Json("The provided TXID appears to be invalid."),
));
}; };
let txid = Txid::from(txid); let txid = Txid::from(txid);
@@ -62,7 +68,10 @@ async fn get_transaction_info(
.get(&prefix) .get(&prefix)
.map(|opt| opt.map(|cow| cow.into_owned())) .map(|opt| opt.map(|cow| cow.into_owned()))
else { else {
return Err(StatusCode::NOT_FOUND); return Err((
StatusCode::NOT_FOUND,
Json("Failed to found the TXID in the blockchain."),
));
}; };
let txid = indexer.vecs.txindex_to_txid.iter().unwrap_get_inner(index); let txid = indexer.vecs.txindex_to_txid.iter().unwrap_get_inner(index);
@@ -84,32 +93,47 @@ async fn get_transaction_info(
let blk_index_to_blk_path = parser.blk_index_to_blk_path(); let blk_index_to_blk_path = parser.blk_index_to_blk_path();
let Some(blk_path) = blk_index_to_blk_path.get(&position.blk_index()) else { let Some(blk_path) = blk_index_to_blk_path.get(&position.blk_index()) else {
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json("Failed to read the transaction (get blk's path)"),
));
}; };
let mut xori = XORIndex::default(); let mut xori = XORIndex::default();
xori.add_assign(position.offset() as usize); xori.add_assign(position.offset() as usize);
let Ok(mut file) = File::open(blk_path) else { let Ok(mut file) = File::open(blk_path) else {
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json("Failed to read the transaction (open file)"),
));
}; };
if file if file
.seek(SeekFrom::Start(position.offset() as u64)) .seek(SeekFrom::Start(position.offset() as u64))
.is_err() .is_err()
{ {
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json("Failed to read the transaction (file seek)"),
));
} }
let mut buffer = vec![0u8; *len as usize]; let mut buffer = vec![0u8; *len as usize];
if file.read_exact(&mut buffer).is_err() { if file.read_exact(&mut buffer).is_err() {
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json("Failed to read the transaction (read exact)"),
));
} }
xori.bytes(&mut buffer, parser.xor_bytes()); xori.bytes(&mut buffer, parser.xor_bytes());
let mut reader = Cursor::new(buffer); let mut reader = Cursor::new(buffer);
let Ok(tx) = BitcoinTransaction::consensus_decode(&mut reader) else { let Ok(tx) = BitcoinTransaction::consensus_decode(&mut reader) else {
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json("Failed decode the transaction"),
));
}; };
let tx_info = TransactionInfo { txid, index, tx }; let tx_info = TransactionInfo { txid, index, tx };
@@ -128,20 +152,11 @@ fn get_transaction_info_docs(op: TransformOperation) -> TransformOperation {
.description( .description(
"Retrieve complete transaction data by transaction ID (txid). Returns the full transaction details including inputs, outputs, and metadata. The transaction data is read directly from the blockchain data files.", "Retrieve complete transaction data by transaction ID (txid). Returns the full transaction details including inputs, outputs, and metadata. The transaction data is read directly from the blockchain data files.",
) )
.response_with::<200, Json<TransactionInfo>, _>(|res| res) .with_ok_response::<TransactionInfo, _>(|res| res)
.response_with::<400, (), _>(|res| { .with_not_modified()
res.description( .with_bad_request()
"Invalid transaction ID format (must be a valid 64-character hex string)", .with_not_found()
) .with_server_error()
})
.response_with::<404, (), _>(|res| {
res.description("Transaction not found in the blockchain")
})
.response_with::<500, (), _>(|res| {
res.description(
"Internal server error while reading transaction data from blockchain files",
)
})
} }
pub trait TransactionsRoutes { pub trait TransactionsRoutes {
+71 -38
View File
@@ -2,19 +2,21 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{ use axum::{
Json, Json,
extract::{Path, Query, State}, extract::{Path, Query, State},
http::{HeaderMap, Uri}, http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response}, response::{IntoResponse, Redirect, Response},
routing::get, routing::get,
}; };
use brk_interface::{ use brk_interface::{
Index, Indexes, PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt, MetricCount, PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt,
}; };
use brk_structs::{Index, IndexInfo};
use brk_traversable::TreeNode;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Serialize; use serde::Deserialize;
use crate::{ use crate::{
VERSION, VERSION,
extended::{HeaderMapExtended, ResponseExtended}, extended::{HeaderMapExtended, ResponseExtended, TransformResponseExtended},
}; };
use super::AppState; use super::AppState;
@@ -22,50 +24,51 @@ use super::AppState;
mod data; mod data;
pub trait ApiMetricsRoutes { pub trait ApiMetricsRoutes {
fn add_api_metrics_routes(self) -> Self; fn add_metrics_routes(self) -> Self;
}
#[derive(Deserialize, JsonSchema)]
struct MetricPath {
/// Metric name
#[schemars(example = &"price_close", example = &"market_cap", example = &"realized_price")]
metric: String,
} }
const TO_SEPARATOR: &str = "_to_"; const TO_SEPARATOR: &str = "_to_";
#[derive(Debug, Serialize, JsonSchema)]
/// Metric count statistics - distinct metrics and total metric-index combinations
struct MetricCount {
#[schemars(example = 3141)]
/// Number of unique metrics available (e.g., realized_price, market_cap)
distinct_metrics: usize,
#[schemars(example = 21000)]
/// Total number of metric-index combinations across all timeframes
total_endpoints: usize,
}
impl ApiMetricsRoutes for ApiRouter<AppState> { impl ApiMetricsRoutes for ApiRouter<AppState> {
fn add_api_metrics_routes(self) -> Self { fn add_metrics_routes(self) -> Self {
self.api_route( self
.route("/api/metrics", get(Redirect::temporary("/api#tag/metrics")))
.api_route(
"/api/metrics/count", "/api/metrics/count",
get_with( get_with(
async |State(app_state): State<AppState>| -> Json<MetricCount> { async |State(app_state): State<AppState>| {
Json(MetricCount { Json(app_state.interface.metric_count())
distinct_metrics: app_state.interface.distinct_metric_count(),
total_endpoints: app_state.interface.total_metric_count(),
})
}, },
|op| { |op| {
op.tag("Metrics") op.tag("Metrics")
.summary("Metric count") .summary("Metric count")
.description("Current metric count") .description("Current metric count")
.with_ok_response::<Vec<MetricCount>, _>(|res| res)
.with_not_modified()
}, },
), ),
) )
.api_route( .api_route(
"/api/metrics/indexes", "/api/metrics/indexes",
get_with( get_with(
async |State(app_state): State<AppState>| -> Json<&Indexes> { async |State(app_state): State<AppState>| {
Json(app_state.interface.get_indexes()) Json(app_state.interface.get_indexes())
}, },
|op| { |op| {
op.tag("Metrics") op.tag("Metrics")
.summary("Metric indexes") .summary("List available indexes")
.description("Available metric indexes and their accepted variants") .description(
"Returns all available indexes with their accepted query aliases. Use any alias when querying metrics."
)
.with_ok_response::<Vec<IndexInfo>, _>(|res| res)
.with_not_modified()
}, },
), ),
) )
@@ -73,20 +76,21 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
"/api/metrics/list", "/api/metrics/list",
get_with( get_with(
async |State(app_state): State<AppState>, async |State(app_state): State<AppState>,
Query(pagination): Query<PaginationParam>| Query(pagination): Query<PaginationParam>| {
-> Json<PaginatedMetrics> {
Json(app_state.interface.get_metrics(pagination)) Json(app_state.interface.get_metrics(pagination))
}, },
|op| { |op| {
op.tag("Metrics") op.tag("Metrics")
.summary("Metrics list") .summary("Metrics list")
.description("Paginated list of available metrics") .description("Paginated list of available metrics")
.with_ok_response::<PaginatedMetrics, _>(|res| res)
.with_not_modified()
}, },
), ),
) )
.route( .api_route(
"/api/metrics/catalog", "/api/metrics/catalog",
get( get_with(
async |headers: HeaderMap, State(app_state): State<AppState>| -> Response { async |headers: HeaderMap, State(app_state): State<AppState>| -> Response {
let etag = VERSION; let etag = VERSION;
@@ -97,8 +101,12 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
return Response::new_not_modified(); return Response::new_not_modified();
} }
let mut response = let bytes = sonic_rs::to_vec(&app_state.interface.get_metrics_catalog()).unwrap();
Json(app_state.interface.get_metrics_catalog()).into_response();
let mut response = Response::builder()
.header("content-type", "application/json")
.body(bytes.into())
.unwrap();
let headers = response.headers_mut(); let headers = response.headers_mut();
headers.insert_cors(); headers.insert_cors();
@@ -106,6 +114,15 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
response response
}, },
|op| {
op.tag("Metrics")
.summary("Metrics catalog")
.description(
"Returns the complete hierarchical catalog of available metrics organized as a tree structure. Metrics are grouped by categories and subcategories. Best viewed in an interactive JSON viewer (e.g., Firefox's built-in JSON viewer) for easy navigation of the nested structure."
)
.with_ok_response::<TreeNode, _>(|res| res)
.with_not_modified()
},
), ),
) )
// TODO: // TODO:
@@ -119,12 +136,28 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
// }, // },
// ), // ),
// ) // )
.route( .api_route(
"/api/metrics/{metric}", "/api/metrics/{metric}",
get( get_with(
async |State(app_state): State<AppState>, Path(metric): Path<String>| -> Response { async |
// If not found do fuzzy search but here or in interface ? State(app_state): State<AppState>,
Json(app_state.interface.metric_to_indexes(metric)).into_response() Path(MetricPath { metric }): Path<MetricPath>
| {
match app_state.interface.metric_to_indexes(metric) {
Some(indexes) => Json(indexes).into_response(),
None => StatusCode::NOT_FOUND.into_response()
}
},
|op| {
op.tag("Metrics")
.summary("Get supported indexes for a metric")
.description(
"Returns the list of indexes are supported by the specified metric. \
For example, `realized_price` might be available on dateindex, weekindex, and monthindex."
)
.with_ok_response::<Vec<Index>, _>(|res| res)
.with_not_modified()
.with_not_found()
}, },
), ),
) )
+14 -48
View File
@@ -2,21 +2,29 @@ use std::sync::Arc;
use aide::{ use aide::{
axum::{ApiRouter, routing::get_with}, axum::{ApiRouter, routing::get_with},
openapi::{Info, OpenApi, Tag}, openapi::OpenApi,
};
use axum::{
Extension, Json,
response::{Html, Redirect},
routing::get,
}; };
use axum::{Extension, Json, response::Html, routing::get};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
VERSION, VERSION,
api::{chain::ChainRoutes, metrics::ApiMetricsRoutes}, api::{chain::ChainRoutes, metrics::ApiMetricsRoutes},
extended::TransformResponseExtended,
}; };
use super::AppState; use super::AppState;
mod chain; mod chain;
mod metrics; mod metrics;
mod openapi;
pub use openapi::*;
pub trait ApiRoutes { pub trait ApiRoutes {
fn add_api_routes(self) -> Self; fn add_api_routes(self) -> Self;
@@ -33,7 +41,8 @@ struct Health {
impl ApiRoutes for ApiRouter<AppState> { impl ApiRoutes for ApiRouter<AppState> {
fn add_api_routes(self) -> Self { fn add_api_routes(self) -> Self {
self.add_chain_routes() self.add_chain_routes()
.add_api_metrics_routes() .add_metrics_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
.api_route( .api_route(
"/version", "/version",
get_with( get_with(
@@ -42,6 +51,7 @@ impl ApiRoutes for ApiRouter<AppState> {
op.tag("Server") op.tag("Server")
.summary("API version") .summary("API version")
.description("Returns the current version of the API server") .description("Returns the current version of the API server")
.with_ok_response::<String, _>(|res| res)
}, },
), ),
) )
@@ -59,6 +69,7 @@ impl ApiRoutes for ApiRouter<AppState> {
op.tag("Server") op.tag("Server")
.summary("Health check") .summary("Health check")
.description("Returns the health status of the API server") .description("Returns the health status of the API server")
.with_ok_response::<Health, _>(|res| res)
}, },
), ),
) )
@@ -73,48 +84,3 @@ impl ApiRoutes for ApiRouter<AppState> {
.route("/api", get(Html::from(include_str!("./scalar.html")))) .route("/api", get(Html::from(include_str!("./scalar.html"))))
} }
} }
pub fn create_openapi() -> OpenApi {
let tags = vec![
Tag {
name: "Chain".to_string(),
description: Some(
"Explore Bitcoin blockchain data: addresses, transactions, blocks, balances, and UTXOs."
.to_string()
),
..Default::default()
},
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."
.to_string()
),
..Default::default()
},
Tag {
name: "Server".to_string(),
description: Some(
"Metadata and utility endpoints for API status, health checks, and system information."
.to_string()
),
..Default::default()
},
];
OpenApi {
info: Info {
title: "Bitcoin Research Kit API".to_string(),
description: Some(
"API for querying Bitcoin blockchain data including addresses, transactions, and chain statistics. This API provides low-level access to indexed blockchain data with advanced analytics capabilities.\n\n\
⚠️ **Early Development**: This API is in very early stages of development. Breaking changes may occur without notice. For a more stable experience, self-host or use the hosting service."
.to_string(),
),
version: format!("v{VERSION}"),
..Info::default()
},
tags,
..OpenApi::default()
}
}
+60
View File
@@ -0,0 +1,60 @@
use aide::openapi::{Info, OpenApi, Tag};
//
// https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html
//
// Scalar:
// - Documentation: https://guides.scalar.com/scalar/scalar-api-references
// - Configuration: https://guides.scalar.com/scalar/scalar-api-references/configuration
// - Examples:
// - https://docs.machines.dev/
// - https://tailscale.com/api
// - https://api.supabase.com/api/v1
//
use crate::VERSION;
pub fn create_openapi() -> OpenApi {
let tags = vec![
Tag {
name: "Chain".to_string(),
description: Some(
"Explore Bitcoin blockchain data: addresses, transactions, blocks, balances, and UTXOs."
.to_string()
),
..Default::default()
},
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."
.to_string()
),
..Default::default()
},
Tag {
name: "Server".to_string(),
description: Some(
"Metadata and utility endpoints for API status, health checks, and system information."
.to_string()
),
..Default::default()
},
];
OpenApi {
info: Info {
title: "Bitcoin Research Kit API".to_string(),
description: Some(
"API for querying Bitcoin blockchain data including addresses, transactions, and chain statistics. This API provides low-level access to indexed blockchain data with advanced analytics capabilities.\n\n\
⚠️ **Early Development**: This API is in very early stages of development. Breaking changes may occur without notice. For a more stable experience, self-host or use the hosting service."
.to_string(),
),
version: format!("v{VERSION}"),
..Info::default()
},
tags,
..OpenApi::default()
}
}
+2
View File
@@ -1,5 +1,7 @@
mod header_map; mod header_map;
mod response; mod response;
mod transform_operation;
pub use header_map::*; pub use header_map::*;
pub use response::*; pub use response::*;
pub use transform_operation::*;
@@ -0,0 +1,49 @@
use aide::transform::{TransformOperation, TransformResponse};
use axum::Json;
use schemars::JsonSchema;
pub trait TransformResponseExtended<'t> {
/// 200
fn with_ok_response<R, F>(self, f: F) -> Self
where
R: JsonSchema,
F: FnOnce(TransformResponse<'_, R>) -> TransformResponse<'_, R>;
/// 400
fn with_bad_request(self) -> Self;
/// 404
fn with_not_found(self) -> Self;
/// 304
fn with_not_modified(self) -> Self;
/// 500
fn with_server_error(self) -> Self;
}
impl<'t> TransformResponseExtended<'t> for TransformOperation<'t> {
fn with_ok_response<R, F>(self, f: F) -> Self
where
R: JsonSchema,
F: FnOnce(TransformResponse<'_, R>) -> TransformResponse<'_, R>,
{
self.response_with::<200, Json<R>, _>(|res| f(res.description("Successful response")))
}
fn with_bad_request(self) -> Self {
self.response_with::<400, Json<String>, _>(|res| {
res.description("Invalid request parameters")
})
}
fn with_not_found(self) -> Self {
self.response_with::<404, Json<String>, _>(|res| res.description("Resource not found"))
}
fn with_not_modified(self) -> Self {
self.response_with::<304, (), _>(|res| {
res.description("Not modified - content unchanged since last request")
})
}
fn with_server_error(self) -> Self {
self.response_with::<500, Json<String>, _>(|res| res.description("Internal server error"))
}
}
@@ -1,90 +1,81 @@
use std::{ use std::fmt::{self, Debug};
collections::BTreeMap,
fmt::{self, Debug},
};
use brk_error::Error; use brk_error::Error;
use brk_structs::{ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::PrintableIndex;
use super::{
DateIndex, DecadeIndex, DifficultyEpoch, EmptyAddressIndex, EmptyOutputIndex, HalvingEpoch, DateIndex, DecadeIndex, DifficultyEpoch, EmptyAddressIndex, EmptyOutputIndex, HalvingEpoch,
Height, InputIndex, LoadedAddressIndex, MonthIndex, OpReturnIndex, OutputIndex, Height, InputIndex, LoadedAddressIndex, MonthIndex, OpReturnIndex, OutputIndex,
P2AAddressIndex, P2MSOutputIndex, P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex, P2AAddressIndex, P2MSOutputIndex, P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex,
P2SHAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex, PrintableIndex, P2SHAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex, QuarterIndex,
QuarterIndex, SemesterIndex, TxIndex, UnknownOutputIndex, WeekIndex, YearIndex, SemesterIndex, TxIndex, UnknownOutputIndex, WeekIndex, YearIndex,
}; };
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, JsonSchema)]
/// Indexes and their accepted variants
pub struct Indexes(BTreeMap<Index, &'static [&'static str]>);
impl Indexes {
pub fn new(tree: BTreeMap<Index, &'static [&'static str]>) -> Self {
Self(tree)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[schemars(example = Index::DateIndex)]
/// Aggregation dimension for querying Bitcoin blockchain data
pub enum Index { pub enum Index {
#[schemars(description = "Date/day index")] /// Date/day index
DateIndex, DateIndex,
#[schemars(description = "Decade index")] /// Decade index
DecadeIndex, DecadeIndex,
#[schemars(description = "Difficulty epoch index (equivalent to ~2 weeks)")] /// Difficulty epoch index (equivalent to ~2 weeks)
DifficultyEpoch, DifficultyEpoch,
#[schemars(description = "Empty output index")] /// Empty output index
EmptyOutputIndex, EmptyOutputIndex,
#[schemars(description = "Halving epoch index (equivalent to ~4 years)")] /// Halving epoch index (equivalent to ~4 years)
HalvingEpoch, HalvingEpoch,
#[schemars(description = "Height/block index")] /// Height/block index
Height, Height,
#[schemars(description = "Transaction input index (based on total)")] /// Transaction input index (based on total)
InputIndex, InputIndex,
#[schemars(description = "Month index")] /// Month index
MonthIndex, MonthIndex,
#[schemars(description = "Op return index")] /// Op return index
OpReturnIndex, OpReturnIndex,
#[schemars(description = "Transaction output index (based on total)")] /// Transaction output index (based on total)
OutputIndex, OutputIndex,
#[schemars(description = "Index of P2A address")] /// Index of P2A address
P2AAddressIndex, P2AAddressIndex,
#[schemars(description = "Index of P2MS output")] /// Index of P2MS output
P2MSOutputIndex, P2MSOutputIndex,
#[schemars(description = "Index of P2PK (33 bytes) address")] /// Index of P2PK (33 bytes) address
P2PK33AddressIndex, P2PK33AddressIndex,
#[schemars(description = "Index of P2PK (65 bytes) address")] /// Index of P2PK (65 bytes) address
P2PK65AddressIndex, P2PK65AddressIndex,
#[schemars(description = "Index of P2PKH address")] /// Index of P2PKH address
P2PKHAddressIndex, P2PKHAddressIndex,
#[schemars(description = "Index of P2SH address")] /// Index of P2SH address
P2SHAddressIndex, P2SHAddressIndex,
#[schemars(description = "Index of P2TR address")] /// Index of P2TR address
P2TRAddressIndex, P2TRAddressIndex,
#[schemars(description = "Index of P2WPKH address")] /// Index of P2WPKH address
P2WPKHAddressIndex, P2WPKHAddressIndex,
#[schemars(description = "Index of P2WSH address")] /// Index of P2WSH address
P2WSHAddressIndex, P2WSHAddressIndex,
#[schemars(description = "Quarter index")] /// Quarter index
QuarterIndex, QuarterIndex,
#[schemars(description = "Semester index")] /// Semester index
SemesterIndex, SemesterIndex,
#[schemars(description = "Transaction index")] /// Transaction index
TxIndex, TxIndex,
#[schemars(description = "Unknown output index")] /// Unknown output index
UnknownOutputIndex, UnknownOutputIndex,
#[schemars(description = "Week index")] /// Week index
WeekIndex, WeekIndex,
#[schemars(description = "Year index")] /// Year index
YearIndex, YearIndex,
#[schemars(description = "Loaded Address Index")] /// Loaded Address Index
LoadedAddressIndex, LoadedAddressIndex,
#[schemars(description = "Empty Address Index")] /// Empty Address Index
EmptyAddressIndex, EmptyAddressIndex,
} }
impl Index { impl Index {
pub fn all() -> [Self; 27] { pub const fn all() -> [Self; 27] {
[ [
Self::DateIndex, Self::DateIndex,
Self::DecadeIndex, Self::DecadeIndex,
@@ -0,0 +1,15 @@
use schemars::JsonSchema;
use serde::Serialize;
use super::Index;
#[derive(Serialize, JsonSchema)]
/// Information about an available index and its query aliases
pub struct IndexInfo {
/// The canonical index name
pub index: Index,
/// All Accepted query aliases
#[schemars(example = vec!["d", "date", "dateindex"])]
pub aliases: &'static [&'static str],
}
+6
View File
@@ -21,6 +21,8 @@ mod emptyoutputindex;
mod feerate; mod feerate;
mod halvingepoch; mod halvingepoch;
mod height; mod height;
mod index;
mod indexinfo;
mod inputindex; mod inputindex;
mod loadedaddressdata; mod loadedaddressdata;
mod loadedaddressindex; mod loadedaddressindex;
@@ -55,6 +57,7 @@ mod stored_u32;
mod stored_u64; mod stored_u64;
mod stored_u8; mod stored_u8;
mod timestamp; mod timestamp;
mod treenode;
mod txid; mod txid;
mod txidprefix; mod txidprefix;
mod txindex; mod txindex;
@@ -90,6 +93,8 @@ pub use emptyoutputindex::*;
pub use feerate::*; pub use feerate::*;
pub use halvingepoch::*; pub use halvingepoch::*;
pub use height::*; pub use height::*;
pub use index::*;
pub use indexinfo::*;
pub use inputindex::*; pub use inputindex::*;
pub use loadedaddressdata::*; pub use loadedaddressdata::*;
pub use loadedaddressindex::*; pub use loadedaddressindex::*;
@@ -124,6 +129,7 @@ pub use stored_u16::*;
pub use stored_u32::*; pub use stored_u32::*;
pub use stored_u64::*; pub use stored_u64::*;
pub use timestamp::*; pub use timestamp::*;
pub use treenode::*;
pub use txid::*; pub use txid::*;
pub use txidprefix::*; pub use txidprefix::*;
pub use txindex::*; pub use txindex::*;
+152
View File
@@ -0,0 +1,152 @@
#[derive(Debug, Clone, Serialize, PartialEq, Eq, JsonSchema)]
#[serde(untagged)]
/// Hierarchical tree node for organizing metrics into categories
pub enum TreeNode {
/// Branch node containing subcategories
Branch(BTreeMap<String, TreeNode>),
/// Leaf node containing the metric name
#[schemars(example = &"price_close", example = &"market_cap", example = &"realized_price")]
Leaf(String),
}
impl TreeNode {
pub fn is_empty(&self) -> bool {
if let Self::Branch(tree) = self {
tree.is_empty()
} else {
false
}
}
/// Merges all first-level branches into a single flattened structure.
/// Root-level leaves are placed under "default" key.
/// Returns None if conflicts are found (same key with incompatible values).
pub fn merge_branches(&self) -> Option<Self> {
let Self::Branch(tree) = self else {
return Some(self.clone());
};
let mut merged: BTreeMap<String, TreeNode> = BTreeMap::new();
for node in tree.values() {
match node {
Self::Leaf(value) => {
Self::merge_node(&mut merged, "base", &Self::Leaf(value.clone()))?;
}
Self::Branch(inner) => {
for (key, inner_node) in inner {
Self::merge_node(&mut merged, key, inner_node)?;
}
}
}
}
let result = Self::Branch(merged);
// Check if all leaves have the same value
if let Some(common_value) = result.all_leaves_same() {
Some(Self::Leaf(common_value))
} else {
Some(result)
}
}
/// Checks if all leaves in the tree have the same value.
/// Returns Some(value) if all leaves are identical, None otherwise.
fn all_leaves_same(&self) -> Option<String> {
match self {
Self::Leaf(value) => Some(value.clone()),
Self::Branch(map) => {
let mut common_value: Option<String> = None;
for node in map.values() {
let node_value = node.all_leaves_same()?;
match &common_value {
None => common_value = Some(node_value),
Some(existing) if existing != &node_value => return None,
_ => {}
}
}
common_value
}
}
}
/// Merges a node into the target map at the given key.
/// Returns None if there's a conflict.
fn merge_node(
target: &mut BTreeMap<String, TreeNode>,
key: &str,
node: &TreeNode,
) -> Option<()> {
match target.get_mut(key) {
None => {
target.insert(key.to_string(), node.clone());
Some(())
}
Some(existing) => match (existing, node) {
// Same leaf values: ok
(Self::Leaf(a), Self::Leaf(b)) if a == b => Some(()),
// Different leaf values: conflict
(Self::Leaf(_), Self::Leaf(_)) => None,
// Leaf vs branch: conflict
(Self::Leaf(_), Self::Branch(_)) | (Self::Branch(_), Self::Leaf(_)) => {
// dbg!((&existing, &node));
None
}
// Both branches: merge recursively
(Self::Branch(existing_inner), Self::Branch(new_inner)) => {
for (k, v) in new_inner {
Self::merge_node(existing_inner, k, v)?;
}
Some(())
}
},
}
}
/// List of prefixes to remove during simplification
const PREFIXES: &'static [&'static str] = &[
"indexes_to_",
"chainindexes_to_",
"timeindexes_to_",
"txindex_to_",
"height_to_",
"dateindex_to_",
"weekindex_to_",
"difficultyepoch_to_",
"halvingepoch_to_",
// Add more prefixes here
];
/// Recursively simplifies the tree by removing known prefixes from keys.
/// If multiple keys map to the same simplified name, checks for conflicts.
/// Returns None if there are conflicts (same simplified key, different values).
pub fn simplify(&self) -> Option<Self> {
match self {
Self::Leaf(value) => Some(Self::Leaf(value.clone())),
Self::Branch(map) => {
let mut simplified: BTreeMap<String, TreeNode> = BTreeMap::new();
for (key, node) in map {
// Recursively simplify the child node first
let simplified_node = node.simplify()?;
// Remove prefixes from the key
let simplified_key = Self::PREFIXES
.iter()
.find_map(|prefix| key.strip_prefix(prefix))
.map(String::from)
.unwrap_or_else(|| key.clone());
// Try to merge into the result
Self::merge_node(&mut simplified, &simplified_key, &simplified_node)?;
}
Some(Self::Branch(simplified))
}
}
}
}
+2
View File
@@ -13,6 +13,8 @@ build = "build.rs"
derive = ["brk_traversable_derive"] derive = ["brk_traversable_derive"]
[dependencies] [dependencies]
brk_structs = { workspace = true }
brk_traversable_derive = { workspace = true, optional = true } brk_traversable_derive = { workspace = true, optional = true }
schemars = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
vecdb = { workspace = true } vecdb = { workspace = true }
+2 -40
View File
@@ -1,10 +1,8 @@
use std::{ use std::{collections::BTreeMap, fmt::Debug};
collections::{BTreeMap, BTreeSet},
fmt::Debug,
};
#[cfg(feature = "derive")] #[cfg(feature = "derive")]
pub use brk_traversable_derive::Traversable; pub use brk_traversable_derive::Traversable;
use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use vecdb::{ use vecdb::{
AnyCollectableVec, AnyVec, CompressedVec, ComputedVec, EagerVec, LazyVecFrom1, LazyVecFrom2, AnyCollectableVec, AnyVec, CompressedVec, ComputedVec, EagerVec, LazyVecFrom1, LazyVecFrom2,
@@ -16,42 +14,6 @@ pub trait Traversable {
fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn AnyCollectableVec>; fn iter_any_collectable(&self) -> impl Iterator<Item = &dyn AnyCollectableVec>;
} }
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum TreeNode {
Branch(BTreeMap<String, TreeNode>),
List(Vec<TreeNode>),
Leaf(String),
}
impl TreeNode {
pub fn collect_unique_leaves(self) -> TreeNode {
let mut out = BTreeSet::new();
fn recurse(n: TreeNode, out: &mut BTreeSet<String>) {
match n {
TreeNode::Leaf(s) => {
out.insert(s);
}
TreeNode::Branch(map) => {
map.into_values().for_each(|n| recurse(n, out));
}
TreeNode::List(vec) => {
vec.into_iter().for_each(|n| recurse(n, out));
}
}
}
recurse(self, &mut out);
match out.len() {
0 => TreeNode::List(vec![]),
1 => TreeNode::Leaf(out.into_iter().next().unwrap()),
_ => TreeNode::List(out.into_iter().map(TreeNode::Leaf).collect()),
}
}
}
impl<I, T> Traversable for RawVec<I, T> impl<I, T> Traversable for RawVec<I, T>
where where
I: StoredIndex, I: StoredIndex,
+5 -5
View File
@@ -150,11 +150,11 @@ fn generate_field_traversals(fields: &Fields) -> proc_macro2::TokenStream {
.flatten() .flatten()
.collect(); .collect();
return if collected.len() == 1 { // return if collected.len() == 1 {
collected.into_values().next().unwrap() // collected.into_values().next().unwrap()
} else { // } else {
brk_traversable::TreeNode::Branch(collected) brk_traversable::TreeNode::Branch(collected)
}; // };
} }
} }
_ => quote! {}, _ => quote! {},