mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-26 02:08:10 -07:00
server: api + doc
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::{Address, Network, PublicKey, ScriptBuf};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_structs::{
|
||||
AddressBytes, AddressBytesHash, AddressInfo, AddressPath, AnyAddressDataIndexEnum, Bitcoin,
|
||||
OutputType,
|
||||
};
|
||||
use vecdb::{AnyIterableVec, VecIterator};
|
||||
|
||||
use crate::Interface;
|
||||
|
||||
pub fn get_address_info(
|
||||
AddressPath { address }: AddressPath,
|
||||
interface: &Interface,
|
||||
) -> Result<AddressInfo> {
|
||||
let indexer = interface.indexer();
|
||||
let computer = interface.computer();
|
||||
let stores = &indexer.stores;
|
||||
|
||||
let script = if let Ok(address) = Address::from_str(&address) {
|
||||
if !address.is_valid_for_network(Network::Bitcoin) {
|
||||
return Err(Error::InvalidNetwork);
|
||||
}
|
||||
let address = address.assume_checked();
|
||||
address.script_pubkey()
|
||||
} else if let Ok(pubkey) = PublicKey::from_str(&address) {
|
||||
ScriptBuf::new_p2pk(&pubkey)
|
||||
} else {
|
||||
return Err(Error::InvalidAddress);
|
||||
};
|
||||
|
||||
let type_ = OutputType::from(&script);
|
||||
let Ok(bytes) = AddressBytes::try_from((&script, type_)) else {
|
||||
return Err(Error::Str("Failed to convert the address to bytes"));
|
||||
};
|
||||
let hash = AddressBytesHash::from((&bytes, type_));
|
||||
|
||||
let Ok(Some(type_index)) = stores
|
||||
.addressbyteshash_to_typeindex
|
||||
.get(&hash)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(Error::UnknownAddress);
|
||||
};
|
||||
|
||||
let stateful = &computer.stateful;
|
||||
let price = computer.price.as_ref().map(|v| {
|
||||
*v.timeindexes_to_price_close
|
||||
.dateindex
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.last()
|
||||
.unwrap()
|
||||
.1
|
||||
.into_owned()
|
||||
});
|
||||
|
||||
let any_address_index = match type_ {
|
||||
OutputType::P2PK33 => stateful
|
||||
.p2pk33addressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2PK65 => stateful
|
||||
.p2pk65addressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2PKH => stateful
|
||||
.p2pkhaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2SH => stateful
|
||||
.p2shaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2TR => stateful
|
||||
.p2traddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2WPKH => stateful
|
||||
.p2wpkhaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2WSH => stateful
|
||||
.p2wshaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2A => stateful
|
||||
.p2aaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
t => {
|
||||
return Err(Error::UnsupportedType(t.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let address_data = match any_address_index.to_enum() {
|
||||
AnyAddressDataIndexEnum::Loaded(index) => stateful
|
||||
.loadedaddressindex_to_loadedaddressdata
|
||||
.iter()
|
||||
.unwrap_get_inner(index),
|
||||
AnyAddressDataIndexEnum::Empty(index) => stateful
|
||||
.emptyaddressindex_to_emptyaddressdata
|
||||
.iter()
|
||||
.unwrap_get_inner(index)
|
||||
.into(),
|
||||
};
|
||||
|
||||
let balance = address_data.balance();
|
||||
|
||||
Ok(AddressInfo {
|
||||
address: address.to_string(),
|
||||
r#type: type_,
|
||||
type_index,
|
||||
utxo_count: address_data.utxo_count,
|
||||
total_sent: address_data.sent,
|
||||
total_received: address_data.received,
|
||||
balance,
|
||||
balance_usd: price.map(|p| p * Bitcoin::from(balance)),
|
||||
estimated_total_invested: price.map(|_| address_data.realized_cap),
|
||||
estimated_avg_entry_price: price.map(|_| address_data.realized_price()),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod addresses;
|
||||
mod transactions;
|
||||
|
||||
pub use addresses::*;
|
||||
pub use transactions::*;
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitcoin::{Transaction, consensus::Decodable};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_parser::XORIndex;
|
||||
use brk_structs::{TransactionInfo, Txid, TxidPath, TxidPrefix};
|
||||
use vecdb::VecIterator;
|
||||
|
||||
use crate::Interface;
|
||||
|
||||
pub fn get_transaction_info(
|
||||
TxidPath { txid }: TxidPath,
|
||||
interface: &Interface,
|
||||
) -> Result<TransactionInfo> {
|
||||
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
|
||||
return Err(Error::InvalidTxid);
|
||||
};
|
||||
|
||||
let txid = Txid::from(txid);
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let indexer = interface.indexer();
|
||||
let Ok(Some(index)) = indexer
|
||||
.stores
|
||||
.txidprefix_to_txindex
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(Error::UnknownTxid);
|
||||
};
|
||||
|
||||
let txid = indexer.vecs.txindex_to_txid.iter().unwrap_get_inner(index);
|
||||
|
||||
let parser = interface.parser();
|
||||
let computer = interface.computer();
|
||||
|
||||
let position = computer
|
||||
.blks
|
||||
.txindex_to_position
|
||||
.iter()
|
||||
.unwrap_get_inner(index);
|
||||
let len = indexer
|
||||
.vecs
|
||||
.txindex_to_total_size
|
||||
.iter()
|
||||
.unwrap_get_inner(index);
|
||||
|
||||
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 {
|
||||
return Err(Error::Str("Failed to get the correct blk file"));
|
||||
};
|
||||
|
||||
let mut xori = XORIndex::default();
|
||||
xori.add_assign(position.offset() as usize);
|
||||
|
||||
let Ok(mut file) = File::open(blk_path) else {
|
||||
return Err(Error::Str("Failed to open blk file"));
|
||||
};
|
||||
|
||||
if file
|
||||
.seek(SeekFrom::Start(position.offset() as u64))
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::Str("Failed to seek position in file"));
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; *len as usize];
|
||||
if file.read_exact(&mut buffer).is_err() {
|
||||
return Err(Error::Str("Failed to read the transaction (read exact)"));
|
||||
}
|
||||
xori.bytes(&mut buffer, parser.xor_bytes());
|
||||
|
||||
let mut reader = Cursor::new(buffer);
|
||||
let Ok(_) = Transaction::consensus_decode(&mut reader) else {
|
||||
return Err(Error::Str("Failed decode the transaction"));
|
||||
};
|
||||
|
||||
Ok(TransactionInfo {
|
||||
txid,
|
||||
index,
|
||||
// tx
|
||||
})
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
#[serde(alias = "json")]
|
||||
JSON,
|
||||
#[serde(alias = "csv")]
|
||||
CSV,
|
||||
}
|
||||
|
||||
impl From<Option<String>> for Format {
|
||||
fn from(value: Option<String>) -> Self {
|
||||
if let Some(value) = value {
|
||||
let value = value.to_lowercase();
|
||||
let value = value.as_str();
|
||||
if value == "csv" {
|
||||
return Self::CSV;
|
||||
}
|
||||
}
|
||||
Self::JSON
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,10 @@ use brk_computer::Computer;
|
||||
use brk_error::{Error, Result};
|
||||
use brk_indexer::Indexer;
|
||||
use brk_parser::Parser;
|
||||
use brk_structs::{Height, Index, IndexInfo};
|
||||
use brk_structs::{
|
||||
AddressInfo, AddressPath, Format, Height, Index, IndexInfo, MetricCount, TransactionInfo,
|
||||
TxidPath,
|
||||
};
|
||||
use brk_traversable::TreeNode;
|
||||
use nucleo_matcher::{
|
||||
Config, Matcher,
|
||||
@@ -15,23 +18,22 @@ use nucleo_matcher::{
|
||||
use quick_cache::sync::Cache;
|
||||
use vecdb::{AnyCollectableVec, AnyStoredVec};
|
||||
|
||||
mod count;
|
||||
mod chain;
|
||||
mod deser;
|
||||
mod format;
|
||||
mod metrics;
|
||||
mod output;
|
||||
mod pagination;
|
||||
mod params;
|
||||
mod vecs;
|
||||
|
||||
pub use count::*;
|
||||
pub use format::Format;
|
||||
pub use output::{Output, Value};
|
||||
pub use metrics::{Output, Value};
|
||||
pub use pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam};
|
||||
pub use params::{Params, ParamsDeprec, ParamsOpt};
|
||||
use vecs::Vecs;
|
||||
|
||||
use crate::vecs::{IndexToVec, MetricToVec};
|
||||
use crate::{
|
||||
chain::{get_address_info, get_transaction_info},
|
||||
vecs::{IndexToVec, MetricToVec},
|
||||
};
|
||||
|
||||
pub fn cached_errors() -> &'static Cache<String, String> {
|
||||
static CACHE: OnceLock<Cache<String, String>> = OnceLock::new();
|
||||
@@ -65,6 +67,14 @@ impl<'a> Interface<'a> {
|
||||
Height::from(self.indexer.vecs.height_to_blockhash.stamp())
|
||||
}
|
||||
|
||||
pub fn get_address_info(&self, address: AddressPath) -> Result<AddressInfo> {
|
||||
get_address_info(address, self)
|
||||
}
|
||||
|
||||
pub fn get_transaction_info(&self, txid: TxidPath) -> Result<TransactionInfo> {
|
||||
get_transaction_info(txid, self)
|
||||
}
|
||||
|
||||
pub fn search(&self, params: &Params) -> Result<Vec<(String, &&dyn AnyCollectableVec)>> {
|
||||
let metrics = ¶ms.metrics;
|
||||
let index = params.index;
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::fmt;
|
||||
use derive_deref::Deref;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
mod output;
|
||||
|
||||
pub use output::*;
|
||||
|
||||
#[derive(Debug, Deref, JsonSchema)]
|
||||
pub struct MaybeMetrics(Vec<String>);
|
||||
@@ -33,7 +36,7 @@ impl<'de> Deserialize<'de> for MaybeMetrics {
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
|
||||
if let Some(str) = value.as_str() {
|
||||
if str.len() <= MAX_STRING_SIZE {
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::Format;
|
||||
use brk_structs::Format;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Output {
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use brk_structs::Index;
|
||||
use brk_structs::{Format, Index};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
Format,
|
||||
deser::{de_unquote_i64, de_unquote_usize},
|
||||
metrics::MaybeMetrics,
|
||||
};
|
||||
|
||||
@@ -86,8 +86,8 @@ impl<'a> Vecs<'a> {
|
||||
this.catalog.replace(
|
||||
TreeNode::Branch(
|
||||
[
|
||||
("indexer".to_string(), indexer.vecs.to_tree_node()),
|
||||
("computer".to_string(), computer.to_tree_node()),
|
||||
("indexed".to_string(), indexer.vecs.to_tree_node()),
|
||||
("computed".to_string(), computer.to_tree_node()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
|
||||
Reference in New Issue
Block a user