mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-11 02:58:14 -07:00
server: api doc part 3
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use brk_error::Error;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::PrintableIndex;
|
||||
|
||||
use super::{
|
||||
DateIndex, DecadeIndex, DifficultyEpoch, EmptyAddressIndex, EmptyOutputIndex, HalvingEpoch,
|
||||
Height, InputIndex, LoadedAddressIndex, MonthIndex, OpReturnIndex, OutputIndex,
|
||||
P2AAddressIndex, P2MSOutputIndex, P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex,
|
||||
P2SHAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex, QuarterIndex,
|
||||
SemesterIndex, TxIndex, UnknownOutputIndex, WeekIndex, YearIndex,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[schemars(example = Index::DateIndex)]
|
||||
/// Aggregation dimension for querying Bitcoin blockchain data
|
||||
pub enum Index {
|
||||
/// Date/day index
|
||||
DateIndex,
|
||||
/// Decade index
|
||||
DecadeIndex,
|
||||
/// Difficulty epoch index (equivalent to ~2 weeks)
|
||||
DifficultyEpoch,
|
||||
/// Empty output index
|
||||
EmptyOutputIndex,
|
||||
/// Halving epoch index (equivalent to ~4 years)
|
||||
HalvingEpoch,
|
||||
/// Height/block index
|
||||
Height,
|
||||
/// Transaction input index (based on total)
|
||||
InputIndex,
|
||||
/// Month index
|
||||
MonthIndex,
|
||||
/// Op return index
|
||||
OpReturnIndex,
|
||||
/// Transaction output index (based on total)
|
||||
OutputIndex,
|
||||
/// Index of P2A address
|
||||
P2AAddressIndex,
|
||||
/// Index of P2MS output
|
||||
P2MSOutputIndex,
|
||||
/// Index of P2PK (33 bytes) address
|
||||
P2PK33AddressIndex,
|
||||
/// Index of P2PK (65 bytes) address
|
||||
P2PK65AddressIndex,
|
||||
/// Index of P2PKH address
|
||||
P2PKHAddressIndex,
|
||||
/// Index of P2SH address
|
||||
P2SHAddressIndex,
|
||||
/// Index of P2TR address
|
||||
P2TRAddressIndex,
|
||||
/// Index of P2WPKH address
|
||||
P2WPKHAddressIndex,
|
||||
/// Index of P2WSH address
|
||||
P2WSHAddressIndex,
|
||||
/// Quarter index
|
||||
QuarterIndex,
|
||||
/// Semester index
|
||||
SemesterIndex,
|
||||
/// Transaction index
|
||||
TxIndex,
|
||||
/// Unknown output index
|
||||
UnknownOutputIndex,
|
||||
/// Week index
|
||||
WeekIndex,
|
||||
/// Year index
|
||||
YearIndex,
|
||||
/// Loaded Address Index
|
||||
LoadedAddressIndex,
|
||||
/// Empty Address Index
|
||||
EmptyAddressIndex,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub const fn all() -> [Self; 27] {
|
||||
[
|
||||
Self::DateIndex,
|
||||
Self::DecadeIndex,
|
||||
Self::DifficultyEpoch,
|
||||
Self::EmptyOutputIndex,
|
||||
Self::HalvingEpoch,
|
||||
Self::Height,
|
||||
Self::InputIndex,
|
||||
Self::MonthIndex,
|
||||
Self::OpReturnIndex,
|
||||
Self::OutputIndex,
|
||||
Self::P2AAddressIndex,
|
||||
Self::P2MSOutputIndex,
|
||||
Self::P2PK33AddressIndex,
|
||||
Self::P2PK65AddressIndex,
|
||||
Self::P2PKHAddressIndex,
|
||||
Self::P2SHAddressIndex,
|
||||
Self::P2TRAddressIndex,
|
||||
Self::P2WPKHAddressIndex,
|
||||
Self::P2WSHAddressIndex,
|
||||
Self::QuarterIndex,
|
||||
Self::SemesterIndex,
|
||||
Self::TxIndex,
|
||||
Self::UnknownOutputIndex,
|
||||
Self::WeekIndex,
|
||||
Self::YearIndex,
|
||||
Self::LoadedAddressIndex,
|
||||
Self::EmptyAddressIndex,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn possible_values(&self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::DateIndex => DateIndex::to_possible_strings(),
|
||||
Self::DecadeIndex => DecadeIndex::to_possible_strings(),
|
||||
Self::DifficultyEpoch => DifficultyEpoch::to_possible_strings(),
|
||||
Self::EmptyOutputIndex => EmptyOutputIndex::to_possible_strings(),
|
||||
Self::HalvingEpoch => HalvingEpoch::to_possible_strings(),
|
||||
Self::Height => Height::to_possible_strings(),
|
||||
Self::InputIndex => InputIndex::to_possible_strings(),
|
||||
Self::MonthIndex => MonthIndex::to_possible_strings(),
|
||||
Self::OpReturnIndex => OpReturnIndex::to_possible_strings(),
|
||||
Self::OutputIndex => OutputIndex::to_possible_strings(),
|
||||
Self::P2AAddressIndex => P2AAddressIndex::to_possible_strings(),
|
||||
Self::P2MSOutputIndex => P2MSOutputIndex::to_possible_strings(),
|
||||
Self::P2PK33AddressIndex => P2PK33AddressIndex::to_possible_strings(),
|
||||
Self::P2PK65AddressIndex => P2PK65AddressIndex::to_possible_strings(),
|
||||
Self::P2PKHAddressIndex => P2PKHAddressIndex::to_possible_strings(),
|
||||
Self::P2SHAddressIndex => P2SHAddressIndex::to_possible_strings(),
|
||||
Self::P2TRAddressIndex => P2TRAddressIndex::to_possible_strings(),
|
||||
Self::P2WPKHAddressIndex => P2WPKHAddressIndex::to_possible_strings(),
|
||||
Self::P2WSHAddressIndex => P2WSHAddressIndex::to_possible_strings(),
|
||||
Self::QuarterIndex => QuarterIndex::to_possible_strings(),
|
||||
Self::SemesterIndex => SemesterIndex::to_possible_strings(),
|
||||
Self::TxIndex => TxIndex::to_possible_strings(),
|
||||
Self::UnknownOutputIndex => UnknownOutputIndex::to_possible_strings(),
|
||||
Self::WeekIndex => WeekIndex::to_possible_strings(),
|
||||
Self::YearIndex => YearIndex::to_possible_strings(),
|
||||
Self::LoadedAddressIndex => LoadedAddressIndex::to_possible_strings(),
|
||||
Self::EmptyAddressIndex => EmptyAddressIndex::to_possible_strings(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_possible_values() -> Vec<&'static str> {
|
||||
Self::all()
|
||||
.into_iter()
|
||||
.flat_map(|i| i.possible_values().iter().cloned())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn serialize_short(&self) -> &'static str {
|
||||
self.possible_values()
|
||||
.iter()
|
||||
.find(|str| str.len() > 1)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn serialize_long(&self) -> &'static str {
|
||||
self.possible_values().last().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Index {
|
||||
type Error = Error;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Ok(match value.to_lowercase().as_str() {
|
||||
v if (Self::DateIndex).possible_values().contains(&v) => Self::DateIndex,
|
||||
v if (Self::DecadeIndex).possible_values().contains(&v) => Self::DecadeIndex,
|
||||
v if (Self::DifficultyEpoch).possible_values().contains(&v) => Self::DifficultyEpoch,
|
||||
v if (Self::EmptyOutputIndex).possible_values().contains(&v) => Self::EmptyOutputIndex,
|
||||
v if (Self::HalvingEpoch).possible_values().contains(&v) => Self::HalvingEpoch,
|
||||
v if (Self::Height).possible_values().contains(&v) => Self::Height,
|
||||
v if (Self::InputIndex).possible_values().contains(&v) => Self::InputIndex,
|
||||
v if (Self::MonthIndex).possible_values().contains(&v) => Self::MonthIndex,
|
||||
v if (Self::OpReturnIndex).possible_values().contains(&v) => Self::OpReturnIndex,
|
||||
v if (Self::OutputIndex).possible_values().contains(&v) => Self::OutputIndex,
|
||||
v if (Self::P2AAddressIndex).possible_values().contains(&v) => Self::P2AAddressIndex,
|
||||
v if (Self::P2MSOutputIndex).possible_values().contains(&v) => Self::P2MSOutputIndex,
|
||||
v if (Self::P2PK33AddressIndex).possible_values().contains(&v) => {
|
||||
Self::P2PK33AddressIndex
|
||||
}
|
||||
v if (Self::P2PK65AddressIndex).possible_values().contains(&v) => {
|
||||
Self::P2PK65AddressIndex
|
||||
}
|
||||
v if (Self::P2PKHAddressIndex).possible_values().contains(&v) => {
|
||||
Self::P2PKHAddressIndex
|
||||
}
|
||||
v if (Self::P2SHAddressIndex).possible_values().contains(&v) => Self::P2SHAddressIndex,
|
||||
v if (Self::P2TRAddressIndex).possible_values().contains(&v) => Self::P2TRAddressIndex,
|
||||
v if (Self::P2WPKHAddressIndex).possible_values().contains(&v) => {
|
||||
Self::P2WPKHAddressIndex
|
||||
}
|
||||
v if (Self::P2WSHAddressIndex).possible_values().contains(&v) => {
|
||||
Self::P2WSHAddressIndex
|
||||
}
|
||||
v if (Self::QuarterIndex).possible_values().contains(&v) => Self::QuarterIndex,
|
||||
v if (Self::SemesterIndex).possible_values().contains(&v) => Self::SemesterIndex,
|
||||
v if (Self::TxIndex).possible_values().contains(&v) => Self::TxIndex,
|
||||
v if (Self::WeekIndex).possible_values().contains(&v) => Self::WeekIndex,
|
||||
v if (Self::YearIndex).possible_values().contains(&v) => Self::YearIndex,
|
||||
v if (Self::UnknownOutputIndex).possible_values().contains(&v) => {
|
||||
Self::UnknownOutputIndex
|
||||
}
|
||||
v if (Self::LoadedAddressIndex).possible_values().contains(&v) => {
|
||||
Self::LoadedAddressIndex
|
||||
}
|
||||
v if (Self::EmptyAddressIndex).possible_values().contains(&v) => {
|
||||
Self::EmptyAddressIndex
|
||||
}
|
||||
_ => return Err(Error::Str("Bad index")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Index {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Index {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let str = String::deserialize(deserializer)?;
|
||||
if let Ok(index) = Index::try_from(str.as_str()) {
|
||||
// dbg!(index);
|
||||
Ok(index)
|
||||
} else {
|
||||
Err(serde::de::Error::custom("Bad index"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 halvingepoch;
|
||||
mod height;
|
||||
mod index;
|
||||
mod indexinfo;
|
||||
mod inputindex;
|
||||
mod loadedaddressdata;
|
||||
mod loadedaddressindex;
|
||||
@@ -55,6 +57,7 @@ mod stored_u32;
|
||||
mod stored_u64;
|
||||
mod stored_u8;
|
||||
mod timestamp;
|
||||
mod treenode;
|
||||
mod txid;
|
||||
mod txidprefix;
|
||||
mod txindex;
|
||||
@@ -90,6 +93,8 @@ pub use emptyoutputindex::*;
|
||||
pub use feerate::*;
|
||||
pub use halvingepoch::*;
|
||||
pub use height::*;
|
||||
pub use index::*;
|
||||
pub use indexinfo::*;
|
||||
pub use inputindex::*;
|
||||
pub use loadedaddressdata::*;
|
||||
pub use loadedaddressindex::*;
|
||||
@@ -124,6 +129,7 @@ pub use stored_u16::*;
|
||||
pub use stored_u32::*;
|
||||
pub use stored_u64::*;
|
||||
pub use timestamp::*;
|
||||
pub use treenode::*;
|
||||
pub use txid::*;
|
||||
pub use txidprefix::*;
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user