mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-28 11:18:17 -07:00
server: api doc part 3
This commit is contained in:
Generated
+2
@@ -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
@@ -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"] }
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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>> =
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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<()> {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
}
|
||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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! {},
|
||||||
|
|||||||
Reference in New Issue
Block a user