mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
server: api doc part 2
This commit is contained in:
Generated
+1
@@ -1168,6 +1168,7 @@ dependencies = [
|
||||
"quick_cache",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sonic-rs 0.5.5",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::fmt::{self, Debug};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Debug},
|
||||
};
|
||||
|
||||
use brk_error::Error;
|
||||
use brk_structs::{
|
||||
@@ -9,9 +12,19 @@ use brk_structs::{
|
||||
QuarterIndex, SemesterIndex, TxIndex, UnknownOutputIndex, WeekIndex, YearIndex,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, JsonSchema)]
|
||||
#[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)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Index {
|
||||
#[schemars(description = "Date/day index")]
|
||||
|
||||
@@ -25,10 +25,11 @@ mod params;
|
||||
mod vecs;
|
||||
|
||||
pub use format::Format;
|
||||
pub use index::Index;
|
||||
pub use index::*;
|
||||
pub use output::{Output, Value};
|
||||
pub use pagination::{PaginatedIndexParam, PaginationParam};
|
||||
pub use params::{Params, ParamsDeprec, ParamsOpt};
|
||||
pub use vecs::PaginatedMetrics;
|
||||
use vecs::Vecs;
|
||||
|
||||
use crate::vecs::{IndexToVec, MetricToVec};
|
||||
@@ -236,11 +237,11 @@ impl<'a> Interface<'a> {
|
||||
self.vecs.total_metric_count
|
||||
}
|
||||
|
||||
pub fn get_indexes(&self) -> &BTreeMap<&'static str, &'static [&'static str]> {
|
||||
pub fn get_indexes(&self) -> &Indexes {
|
||||
&self.vecs.indexes
|
||||
}
|
||||
|
||||
pub fn get_metrics(&self, pagination: PaginationParam) -> &[&str] {
|
||||
pub fn get_metrics(&'static self, pagination: PaginationParam) -> PaginatedMetrics {
|
||||
self.vecs.metrics(pagination)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Index, deser::de_unquote_usize};
|
||||
|
||||
#[derive(Debug, Default, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PaginationParam {
|
||||
#[schemars(description = "Pagination index")]
|
||||
#[serde(default, alias = "p", deserialize_with = "de_unquote_usize")]
|
||||
@@ -11,7 +11,7 @@ pub struct PaginationParam {
|
||||
}
|
||||
|
||||
impl PaginationParam {
|
||||
const PER_PAGE: usize = 1_000;
|
||||
pub const PER_PAGE: usize = 1_000;
|
||||
|
||||
pub fn start(&self, len: usize) -> usize {
|
||||
(self.page.unwrap_or_default() * Self::PER_PAGE).clamp(0, len)
|
||||
|
||||
@@ -4,9 +4,14 @@ use brk_computer::Computer;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_traversable::{Traversable, TreeNode};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use vecdb::AnyCollectableVec;
|
||||
|
||||
use crate::pagination::{PaginatedIndexParam, PaginationParam};
|
||||
use crate::{
|
||||
index::Indexes,
|
||||
pagination::{PaginatedIndexParam, PaginationParam},
|
||||
};
|
||||
|
||||
use super::index::Index;
|
||||
|
||||
@@ -15,7 +20,7 @@ pub struct Vecs<'a> {
|
||||
pub metric_to_index_to_vec: BTreeMap<&'a str, IndexToVec<'a>>,
|
||||
pub index_to_metric_to_vec: BTreeMap<Index, MetricToVec<'a>>,
|
||||
pub metrics: Vec<&'a str>,
|
||||
pub indexes: BTreeMap<&'static str, &'static [&'static str]>,
|
||||
pub indexes: Indexes,
|
||||
pub distinct_metric_count: usize,
|
||||
pub total_metric_count: usize,
|
||||
pub catalog: Option<TreeNode>,
|
||||
@@ -62,11 +67,12 @@ impl<'a> Vecs<'a> {
|
||||
.values()
|
||||
.map(|tree| tree.len())
|
||||
.sum::<usize>();
|
||||
this.indexes = this
|
||||
.index_to_metric_to_vec
|
||||
.keys()
|
||||
.map(|i| (i.serialize_long(), i.possible_values()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
this.indexes = Indexes::new(
|
||||
this.index_to_metric_to_vec
|
||||
.keys()
|
||||
.map(|i| (*i, i.possible_values()))
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
);
|
||||
this.metric_to_indexes = this
|
||||
.metric_to_index_to_vec
|
||||
.iter()
|
||||
@@ -129,11 +135,16 @@ impl<'a> Vecs<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metrics(&self, pagination: PaginationParam) -> &[&'_ str] {
|
||||
pub fn metrics(&'static self, pagination: PaginationParam) -> PaginatedMetrics {
|
||||
let len = self.metrics.len();
|
||||
let start = pagination.start(len);
|
||||
let end = pagination.end(len);
|
||||
&self.metrics[start..end]
|
||||
|
||||
PaginatedMetrics {
|
||||
current_page: pagination.page.unwrap_or_default(),
|
||||
total_pages: len / PaginationParam::PER_PAGE,
|
||||
metrics: &self.metrics[start..end],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metric_to_indexes(&self, metric: String) -> Option<&Vec<&'static str>> {
|
||||
@@ -160,3 +171,17 @@ pub struct IndexToVec<'a>(BTreeMap<Index, &'a dyn AnyCollectableVec>);
|
||||
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
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"
|
||||
|
||||
[dependencies]
|
||||
aide = { workspace = true }
|
||||
aide = { workspace = true , features = ["axum-json", "axum-query"] }
|
||||
axum = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
bitcoincore-rpc = { workspace = true }
|
||||
@@ -29,7 +29,7 @@ log = { workspace = true }
|
||||
quick_cache = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
# serde_json = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sonic-rs = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tower-http = { version = "0.6.6", features = ["compression-full", "trace"] }
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
transform::TransformOperation,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use bitcoin::{Address as BitcoinAddress, Network, PublicKey, ScriptBuf};
|
||||
use brk_structs::{
|
||||
AddressBytes, AddressBytesHash, AnyAddressDataIndexEnum, Bitcoin, Dollars, OutputType, Sats,
|
||||
TypeIndex,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::{AnyIterableVec, VecIterator};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
/// Address information
|
||||
struct AddressInfo {
|
||||
/// Bitcoin address string
|
||||
#[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")]
|
||||
address: String,
|
||||
|
||||
#[schemars(example = OutputType::P2PK65)]
|
||||
r#type: OutputType,
|
||||
|
||||
#[schemars(example = TypeIndex::new(0))]
|
||||
type_index: TypeIndex,
|
||||
|
||||
/// Total satoshis ever sent from this address
|
||||
#[schemars(example = Sats::new(0))]
|
||||
total_sent: Sats,
|
||||
|
||||
/// Total satoshis ever received by this address
|
||||
#[schemars(example = Sats::new(5001008380))]
|
||||
total_received: Sats,
|
||||
|
||||
/// Number of unspent transaction outputs (UTXOs)
|
||||
#[schemars(example = 10)]
|
||||
utxo_count: u32,
|
||||
|
||||
/// Current spendable balance in satoshis (total_received - total_sent)
|
||||
#[schemars(example = Sats::new(5001008380))]
|
||||
balance: Sats,
|
||||
|
||||
/// Current balance value in USD at current market price
|
||||
#[schemars(example = Some(Dollars::mint(6_157_891.64)))]
|
||||
balance_usd: Option<Dollars>,
|
||||
|
||||
/// Estimated total USD value at time of deposit for coins currently in this address (not including coins that were later sent out). Not suitable for tax calculations
|
||||
#[schemars(example = Some(Dollars::mint(6.2)))]
|
||||
estimated_total_invested: Option<Dollars>,
|
||||
|
||||
/// Estimated average BTC price at time of deposit for coins currently in this address (USD). Not suitable for tax calculations
|
||||
#[schemars(example = Some(Dollars::mint(0.12)))]
|
||||
estimated_avg_entry_price: Option<Dollars>,
|
||||
//
|
||||
// Transaction count?
|
||||
// First/last activity timestamps?
|
||||
// Realized/unrealized gains?
|
||||
// Current value (balance × current price)?
|
||||
// "address": address,
|
||||
// "type": output_type,
|
||||
// "index": addri,
|
||||
// "chain_stats": {
|
||||
// "funded_txo_count": null,
|
||||
// "funded_txo_sum": addr_data.received,
|
||||
// "spent_txo_count": null,
|
||||
// "spent_txo_sum": addr_data.sent,
|
||||
// "utxo_count": addr_data.utxos,
|
||||
// "balance": amount,
|
||||
// "balance_usd": price.map_or(Value::new(), |p| {
|
||||
// Value::from(Number::from_f64(*(p * Bitcoin::from(amount))).unwrap())
|
||||
// }),
|
||||
// "realized_value": addr_data.realized_cap,
|
||||
// "tx_count": null,
|
||||
// "avg_cost_basis": addr_data.realized_price()
|
||||
// },
|
||||
// "mempool_stats": null
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct AddressPath {
|
||||
/// Bitcoin address string
|
||||
address: String,
|
||||
}
|
||||
|
||||
async fn get_address_info(
|
||||
Path(AddressPath { address }): Path<AddressPath>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Json<AddressInfo>, StatusCode> {
|
||||
let interface = state.interface;
|
||||
let indexer = interface.indexer();
|
||||
let computer = interface.computer();
|
||||
let stores = &indexer.stores;
|
||||
|
||||
let script = if let Ok(address) = BitcoinAddress::from_str(&address) {
|
||||
if !address.is_valid_for_network(Network::Bitcoin) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
let address = address.assume_checked();
|
||||
address.script_pubkey()
|
||||
} else if let Ok(pubkey) = PublicKey::from_str(&address) {
|
||||
ScriptBuf::new_p2pk(&pubkey)
|
||||
} else {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
};
|
||||
|
||||
let type_ = OutputType::from(&script);
|
||||
let Ok(bytes) = AddressBytes::try_from((&script, type_)) else {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
};
|
||||
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(StatusCode::NOT_FOUND);
|
||||
};
|
||||
|
||||
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()),
|
||||
_ => return Err(StatusCode::BAD_REQUEST),
|
||||
};
|
||||
|
||||
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(Json(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()),
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_address_info_docs(op: TransformOperation) -> TransformOperation {
|
||||
op.tag("Chain")
|
||||
.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.).")
|
||||
.response_with::<400, (), _>(|res|
|
||||
res.description("Invalid address format or unsupported address type")
|
||||
)
|
||||
.response_with::<404, (), _>(|res|
|
||||
res.description("Address not found in the blockchain (no transaction history)")
|
||||
)
|
||||
}
|
||||
|
||||
pub trait AddressesRoutes {
|
||||
fn add_addresses_routes(self) -> Self;
|
||||
}
|
||||
|
||||
impl AddressesRoutes for ApiRouter<AppState> {
|
||||
fn add_addresses_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/chain/address/{address}",
|
||||
get_with(get_address_info, get_address_info_docs),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,363 +1,18 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
str::FromStr,
|
||||
};
|
||||
use aide::axum::ApiRouter;
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, IntoApiResponse, routing::get_with},
|
||||
transform::TransformOperation,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
use bitcoin::{
|
||||
Address as BitcoinAddress, Network, Transaction as BitcoinTransaction, consensus::Decodable,
|
||||
};
|
||||
use brk_parser::XORIndex;
|
||||
use brk_structs::{
|
||||
AddressBytesHash, AnyAddressDataIndexEnum, Bitcoin, Dollars, OutputType, Sats, TxIndex, Txid,
|
||||
TxidPrefix, TypeIndex,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::{AnyIterableVec, VecIterator};
|
||||
use crate::api::chain::{addresses::AddressesRoutes, transactions::TransactionsRoutes};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub trait ApiExplorerRoutes {
|
||||
fn add_api_explorer_routes(self) -> Self;
|
||||
mod addresses;
|
||||
mod transactions;
|
||||
|
||||
pub trait ChainRoutes {
|
||||
fn add_chain_routes(self) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
struct AddressDetails {
|
||||
/// Bitcoin address string
|
||||
address: String,
|
||||
|
||||
r#type: OutputType,
|
||||
|
||||
type_index: TypeIndex,
|
||||
|
||||
/// Total satoshis ever sent from this address
|
||||
total_sent: Sats,
|
||||
|
||||
/// Total satoshis ever received by this address
|
||||
total_received: Sats,
|
||||
|
||||
/// Number of unspent transaction outputs (UTXOs)
|
||||
utxo_count: u32,
|
||||
|
||||
/// Current spendable balance in satoshis (total_received - total_sent)
|
||||
balance: Sats,
|
||||
|
||||
/// Current balance value in USD at current market price
|
||||
balance_usd: Option<Dollars>,
|
||||
|
||||
/// Estimated total USD value at time of deposit for coins currently in this address (not including coins that were later sent out). Not suitable for tax calculations
|
||||
estimated_total_invested: Option<Dollars>,
|
||||
|
||||
/// Estimated average BTC price at time of deposit for coins currently in this address (USD). Not suitable for tax calculations
|
||||
estimated_avg_entry_price: Option<Dollars>,
|
||||
//
|
||||
// Transaction count?
|
||||
// First/last activity timestamps?
|
||||
// Realized/unrealized gains?
|
||||
// Current value (balance × current price)?
|
||||
// "address": address,
|
||||
// "type": output_type,
|
||||
// "index": addri,
|
||||
// "chain_stats": {
|
||||
// "funded_txo_count": null,
|
||||
// "funded_txo_sum": addr_data.received,
|
||||
// "spent_txo_count": null,
|
||||
// "spent_txo_sum": addr_data.sent,
|
||||
// "utxo_count": addr_data.utxos,
|
||||
// "balance": amount,
|
||||
// "balance_usd": price.map_or(Value::new(), |p| {
|
||||
// Value::from(Number::from_f64(*(p * Bitcoin::from(amount))).unwrap())
|
||||
// }),
|
||||
// "realized_value": addr_data.realized_cap,
|
||||
// "tx_count": null,
|
||||
// "avg_cost_basis": addr_data.realized_price()
|
||||
// },
|
||||
// "mempool_stats": null
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TxResponse {
|
||||
txid: Txid,
|
||||
index: TxIndex,
|
||||
tx: BitcoinTransaction,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct AddressPath {
|
||||
/// Bitcoin address string
|
||||
address: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct TypeIndexPath {
|
||||
/// Index of type
|
||||
index: TypeIndex,
|
||||
}
|
||||
|
||||
async fn get_address_details_from_address(
|
||||
Path(AddressPath { address }): Path<AddressPath>,
|
||||
state: State<AppState>,
|
||||
) -> impl IntoApiResponse {
|
||||
let Ok(address) = BitcoinAddress::from_str(&address) else {
|
||||
return StatusCode::BAD_REQUEST.into_response();
|
||||
};
|
||||
if !address.is_valid_for_network(Network::Bitcoin) {
|
||||
return StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
let address = address.assume_checked();
|
||||
let interface = state.interface;
|
||||
let indexer = interface.indexer();
|
||||
let computer = interface.computer();
|
||||
let stores = &indexer.stores;
|
||||
let hash = AddressBytesHash::from(&address);
|
||||
|
||||
let r#type = OutputType::from(&address);
|
||||
|
||||
let Ok(Some(type_index)) = stores
|
||||
.addressbyteshash_to_typeindex
|
||||
.get(&hash)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
};
|
||||
|
||||
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 r#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()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
Json(AddressDetails {
|
||||
address: address.to_string(),
|
||||
r#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()),
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn get_address_docs(op: TransformOperation) -> TransformOperation {
|
||||
op.tag("Chain")
|
||||
.summary("Get address information")
|
||||
.description("Get Bitcoin address details")
|
||||
.response_with::<200, Json<AddressDetails>, _>(|res| {
|
||||
res.example(AddressDetails {
|
||||
address: "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38"
|
||||
.to_string(),
|
||||
r#type: OutputType::P2WSH,
|
||||
type_index: TypeIndex::new(26158889),
|
||||
total_sent: Sats::new(5498948012620),
|
||||
total_received: Sats::new(5557954331207),
|
||||
utxo_count: 195,
|
||||
balance: Sats::new(59006318587),
|
||||
balance_usd: Some(Dollars::mint(73757839.22)),
|
||||
estimated_total_invested: Some(Dollars::mint(71943052.66)),
|
||||
estimated_avg_entry_price: Some(Dollars::mint(121900.0)),
|
||||
})
|
||||
})
|
||||
.response_with::<400, (), _>(|res| res.description("The address provided was invalid"))
|
||||
.response_with::<404, (), _>(|res| res.description("The address provided was not found"))
|
||||
}
|
||||
|
||||
impl ApiExplorerRoutes for ApiRouter<AppState> {
|
||||
fn add_api_explorer_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/chain/address/{address}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2pk65/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2pk33/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2pkh/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2sh/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2wpkh/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2wsh/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2tr/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.api_route(
|
||||
"/api/chain/address/p2a/{index}",
|
||||
get_with(get_address_details_from_address, get_address_docs),
|
||||
)
|
||||
.route(
|
||||
"/api/chain/tx/{txid}",
|
||||
get(
|
||||
async |Path(txid): Path<String>, state: State<AppState>| -> Response {
|
||||
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
|
||||
return "Invalid txid".into_response();
|
||||
};
|
||||
|
||||
let txid = Txid::from(txid);
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let interface = state.interface;
|
||||
let indexer = interface.indexer();
|
||||
let Ok(Some(txindex)) = indexer
|
||||
.stores
|
||||
.txidprefix_to_txindex
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return "Unknown transaction".into_response();
|
||||
};
|
||||
|
||||
let txid = indexer
|
||||
.vecs
|
||||
.txindex_to_txid
|
||||
.iter()
|
||||
.unwrap_get_inner(txindex);
|
||||
|
||||
let parser = interface.parser();
|
||||
let computer = interface.computer();
|
||||
|
||||
let position = computer
|
||||
.blks
|
||||
.txindex_to_position
|
||||
.iter()
|
||||
.unwrap_get_inner(txindex);
|
||||
let len = indexer
|
||||
.vecs
|
||||
.txindex_to_total_size
|
||||
.iter()
|
||||
.unwrap_get_inner(txindex);
|
||||
|
||||
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 "Unknown blk index".into_response();
|
||||
};
|
||||
|
||||
let mut xori = XORIndex::default();
|
||||
xori.add_assign(position.offset() as usize);
|
||||
|
||||
let Ok(mut file) = File::open(blk_path) else {
|
||||
return "Error opening blk file".into_response();
|
||||
};
|
||||
|
||||
if file
|
||||
.seek(SeekFrom::Start(position.offset() as u64))
|
||||
.is_err()
|
||||
{
|
||||
return "Error seeking position in blk file".into_response();
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; *len as usize];
|
||||
if file.read_exact(&mut buffer).is_err() {
|
||||
return "File fail read exact".into_response();
|
||||
}
|
||||
xori.bytes(&mut buffer, parser.xor_bytes());
|
||||
|
||||
let mut reader = Cursor::new(buffer);
|
||||
let Ok(tx) = BitcoinTransaction::consensus_decode(&mut reader) else {
|
||||
return "Error decoding transaction".into_response();
|
||||
};
|
||||
|
||||
let response = TxResponse {
|
||||
txid,
|
||||
index: txindex,
|
||||
tx,
|
||||
};
|
||||
|
||||
let bytes = sonic_rs::to_vec(&response).unwrap();
|
||||
|
||||
Response::builder()
|
||||
.header("content-type", "application/json")
|
||||
.body(bytes.into())
|
||||
.unwrap()
|
||||
},
|
||||
),
|
||||
)
|
||||
impl ChainRoutes for ApiRouter<AppState> {
|
||||
fn add_chain_routes(self) -> Self {
|
||||
self.add_addresses_routes().add_transactions_routes()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
transform::TransformOperation,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use bitcoin::{Transaction as BitcoinTransaction, consensus::Decodable};
|
||||
use brk_parser::XORIndex;
|
||||
use brk_structs::{TxIndex, Txid, TxidPrefix};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::VecIterator;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
/// Transaction Information
|
||||
struct TransactionInfo {
|
||||
#[schemars(
|
||||
with = "String",
|
||||
example = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
|
||||
)]
|
||||
txid: Txid,
|
||||
#[schemars(example = TxIndex::new(0))]
|
||||
index: TxIndex,
|
||||
#[serde(flatten)]
|
||||
#[schemars(with = "serde_json::Value")]
|
||||
tx: BitcoinTransaction,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct TxidPath {
|
||||
/// Bitcoin transaction id
|
||||
txid: String,
|
||||
}
|
||||
|
||||
async fn get_transaction_info(
|
||||
Path(TxidPath { txid }): Path<TxidPath>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
};
|
||||
|
||||
let txid = Txid::from(txid);
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let interface = state.interface;
|
||||
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(StatusCode::NOT_FOUND);
|
||||
};
|
||||
|
||||
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(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
};
|
||||
|
||||
let mut xori = XORIndex::default();
|
||||
xori.add_assign(position.offset() as usize);
|
||||
|
||||
let Ok(mut file) = File::open(blk_path) else {
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
};
|
||||
|
||||
if file
|
||||
.seek(SeekFrom::Start(position.offset() as u64))
|
||||
.is_err()
|
||||
{
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; *len as usize];
|
||||
if file.read_exact(&mut buffer).is_err() {
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
xori.bytes(&mut buffer, parser.xor_bytes());
|
||||
|
||||
let mut reader = Cursor::new(buffer);
|
||||
let Ok(tx) = BitcoinTransaction::consensus_decode(&mut reader) else {
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
};
|
||||
|
||||
let tx_info = TransactionInfo { txid, index, tx };
|
||||
|
||||
let bytes = sonic_rs::to_vec(&tx_info).unwrap();
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("content-type", "application/json")
|
||||
.body(bytes.into())
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn get_transaction_info_docs(op: TransformOperation) -> TransformOperation {
|
||||
op.tag("Chain")
|
||||
.summary("Transaction information")
|
||||
.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.",
|
||||
)
|
||||
.response_with::<200, Json<TransactionInfo>, _>(|res| res)
|
||||
.response_with::<400, (), _>(|res| {
|
||||
res.description(
|
||||
"Invalid transaction ID format (must be a valid 64-character hex string)",
|
||||
)
|
||||
})
|
||||
.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 {
|
||||
fn add_transactions_routes(self) -> Self;
|
||||
}
|
||||
|
||||
impl TransactionsRoutes for ApiRouter<AppState> {
|
||||
fn add_transactions_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/chain/tx/{txid}",
|
||||
get_with(get_transaction_info, get_transaction_info_docs),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use aide::axum::ApiRouter;
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
@@ -6,7 +6,11 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
use brk_interface::{Index, PaginationParam, Params, ParamsDeprec, ParamsOpt};
|
||||
use brk_interface::{
|
||||
Index, Indexes, PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
@@ -23,23 +27,62 @@ pub trait ApiMetricsRoutes {
|
||||
|
||||
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> {
|
||||
fn add_api_metrics_routes(self) -> Self {
|
||||
self.route(
|
||||
self.api_route(
|
||||
"/api/metrics/count",
|
||||
get(async |State(app_state): State<AppState>| -> Response {
|
||||
Json(sonic_rs::json!({
|
||||
"distinct": app_state.interface.distinct_metric_count(),
|
||||
"total": app_state.interface.total_metric_count(),
|
||||
}))
|
||||
.into_response()
|
||||
}),
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>| -> Json<MetricCount> {
|
||||
Json(MetricCount {
|
||||
distinct_metrics: app_state.interface.distinct_metric_count(),
|
||||
total_endpoints: app_state.interface.total_metric_count(),
|
||||
})
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metric count")
|
||||
.description("Current metric count")
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
.api_route(
|
||||
"/api/metrics/indexes",
|
||||
get(async |State(app_state): State<AppState>| -> Response {
|
||||
Json(app_state.interface.get_indexes()).into_response()
|
||||
}),
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>| -> Json<&Indexes> {
|
||||
Json(app_state.interface.get_indexes())
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metric indexes")
|
||||
.description("Available metric indexes and their accepted variants")
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/list",
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>,
|
||||
Query(pagination): Query<PaginationParam>|
|
||||
-> Json<PaginatedMetrics> {
|
||||
Json(app_state.interface.get_metrics(pagination))
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metrics list")
|
||||
.description("Paginated list of available metrics")
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/metrics/catalog",
|
||||
@@ -65,16 +108,6 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/metrics/list",
|
||||
get(
|
||||
async |State(app_state): State<AppState>,
|
||||
Query(pagination): Query<PaginationParam>|
|
||||
-> Response {
|
||||
Json(app_state.interface.get_metrics(pagination)).into_response()
|
||||
},
|
||||
),
|
||||
)
|
||||
// TODO:
|
||||
// .route(
|
||||
// "/api/metrics/search",
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
use aide::axum::ApiRouter;
|
||||
use axum::{response::Html, routing::get};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::api::{chain::ApiExplorerRoutes, metrics::ApiMetricsRoutes};
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
openapi::{Info, OpenApi, Tag},
|
||||
};
|
||||
use axum::{Extension, Json, response::Html, routing::get};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
api::{chain::ChainRoutes, metrics::ApiMetricsRoutes},
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
@@ -12,10 +22,99 @@ pub trait ApiRoutes {
|
||||
fn add_api_routes(self) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
/// Server health status
|
||||
struct Health {
|
||||
status: String,
|
||||
service: String,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
impl ApiRoutes for ApiRouter<AppState> {
|
||||
fn add_api_routes(self) -> Self {
|
||||
self.add_api_explorer_routes()
|
||||
self.add_chain_routes()
|
||||
.add_api_metrics_routes()
|
||||
.api_route(
|
||||
"/version",
|
||||
get_with(
|
||||
async || -> Json<&'static str> { Json(VERSION) },
|
||||
|op| {
|
||||
op.tag("Server")
|
||||
.summary("API version")
|
||||
.description("Returns the current version of the API server")
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/health",
|
||||
get_with(
|
||||
async || -> Json<Health> {
|
||||
Json(Health {
|
||||
status: "healthy".to_string(),
|
||||
service: "brk-server".to_string(),
|
||||
timestamp: jiff::Timestamp::now().to_string(),
|
||||
})
|
||||
},
|
||||
|op| {
|
||||
op.tag("Server")
|
||||
.summary("Health check")
|
||||
.description("Returns the health status of the API server")
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api.json",
|
||||
get(
|
||||
async |Extension(api): Extension<Arc<OpenApi>>| -> Json<Arc<OpenApi>> {
|
||||
Json(api)
|
||||
},
|
||||
),
|
||||
)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Scalar API Reference</title>
|
||||
<title>BRK API</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Bitcoin Research Kit's API documentation via Scalar"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
@@ -15,11 +19,9 @@
|
||||
<script>
|
||||
Scalar.createApiReference("#app", {
|
||||
url: "/api.json",
|
||||
metaData: {
|
||||
title: "BRK API",
|
||||
description: "Bitcoin Research Kit's API documentation via Scalar",
|
||||
hideClientButton: true,
|
||||
},
|
||||
hideClientButton: true,
|
||||
telemetry: false,
|
||||
// showToolbar: "never",
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -5,13 +5,10 @@
|
||||
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, IntoApiResponse},
|
||||
openapi::{Info, OpenApi},
|
||||
};
|
||||
use aide::axum::ApiRouter;
|
||||
use api::ApiRoutes;
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
Extension,
|
||||
body::{Body, Bytes},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
middleware::Next,
|
||||
@@ -36,6 +33,8 @@ mod files;
|
||||
|
||||
use extended::*;
|
||||
|
||||
use crate::api::create_openapi;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
interface: &'static Interface<'static>,
|
||||
@@ -102,15 +101,6 @@ impl Server {
|
||||
.add_api_routes()
|
||||
.add_files_routes(state.path.as_ref())
|
||||
.add_mcp_routes(state.interface, mcp)
|
||||
.api_route("/version", aide::axum::routing::get(version))
|
||||
.route(
|
||||
"/health",
|
||||
get(Json(sonic_rs::json!({
|
||||
"status": "healthy",
|
||||
"service": "brk-server",
|
||||
"timestamp": jiff::Timestamp::now().to_string()
|
||||
}))),
|
||||
)
|
||||
.route(
|
||||
"/discord",
|
||||
get(Redirect::temporary("https://discord.gg/WACpShCB7M")),
|
||||
@@ -130,7 +120,6 @@ impl Server {
|
||||
get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk?tab=readme-ov-file#hosting-as-a-service")),
|
||||
)
|
||||
.route("/nostr", get(Redirect::temporary("https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44")))
|
||||
.route("/api.json", get(serve_api))
|
||||
.with_state(state)
|
||||
.layer(compression_layer)
|
||||
.layer(response_uri_layer)
|
||||
@@ -147,24 +136,16 @@ impl Server {
|
||||
port += 1;
|
||||
}
|
||||
|
||||
let mut api = OpenApi {
|
||||
info: Info {
|
||||
title: "Bitcoin Research Kit API".to_string(),
|
||||
description: Some("A documentation for using BRK's API".to_string()),
|
||||
..Info::default()
|
||||
},
|
||||
..OpenApi::default()
|
||||
};
|
||||
|
||||
info!("Starting server on port {port}...");
|
||||
|
||||
let listener = listener.unwrap();
|
||||
|
||||
let mut openapi = create_openapi();
|
||||
serve(
|
||||
listener,
|
||||
router
|
||||
.finish_api(&mut api)
|
||||
.layer(Extension(Arc::new(api)))
|
||||
.finish_api(&mut openapi)
|
||||
.layer(Extension(Arc::new(openapi)))
|
||||
.into_make_service(),
|
||||
)
|
||||
.await?;
|
||||
@@ -172,11 +153,3 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_api(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoApiResponse {
|
||||
Json(api)
|
||||
}
|
||||
|
||||
async fn version() -> impl IntoApiResponse {
|
||||
Json(VERSION)
|
||||
}
|
||||
|
||||
@@ -55,12 +55,6 @@ impl AddressBytes {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Address> for AddressBytes {
|
||||
fn from(value: &Address) -> Self {
|
||||
Self::try_from((&value.script_pubkey(), OutputType::from(value))).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(&ScriptBuf, OutputType)> for AddressBytes {
|
||||
type Error = Error;
|
||||
fn try_from(tuple: (&ScriptBuf, OutputType)) -> Result<Self, Self::Error> {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use bitcoin::Address;
|
||||
use byteview::ByteView;
|
||||
use derive_deref::Deref;
|
||||
use zerocopy::{FromBytes, IntoBytes};
|
||||
@@ -22,12 +21,6 @@ use super::{AddressBytes, OutputType};
|
||||
)]
|
||||
pub struct AddressBytesHash([u8; 8]);
|
||||
|
||||
impl From<&Address> for AddressBytesHash {
|
||||
fn from(value: &Address) -> Self {
|
||||
Self::from((&AddressBytes::from(value), OutputType::from(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&AddressBytes, OutputType)> for AddressBytesHash {
|
||||
fn from((address_bytes, outputtype): (&AddressBytes, OutputType)) -> Self {
|
||||
let mut slice = rapidhash::v3::rapidhash_v3(address_bytes.as_slice()).to_le_bytes();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bitcoin::{Address, AddressType, ScriptBuf, opcodes::all::OP_PUSHBYTES_2};
|
||||
use bitcoin::{AddressType, ScriptBuf, opcodes::all::OP_PUSHBYTES_2};
|
||||
use brk_error::Error;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
@@ -632,12 +632,6 @@ impl From<&ScriptBuf> for OutputType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Address> for OutputType {
|
||||
fn from(value: &Address) -> Self {
|
||||
Self::from(&value.script_pubkey())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AddressType> for OutputType {
|
||||
fn from(value: AddressType) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -2,10 +2,13 @@ use std::{fmt, mem};
|
||||
|
||||
use bitcoin::hashes::Hash;
|
||||
use derive_deref::Deref;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Serialize, Serializer};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
#[derive(Debug, Deref, Clone, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)]
|
||||
#[derive(
|
||||
Debug, Deref, Clone, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, JsonSchema,
|
||||
)]
|
||||
pub struct Txid([u8; 32]);
|
||||
|
||||
impl From<bitcoin::Txid> for Txid {
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::ops::{Add, AddAssign};
|
||||
use allocative::Allocative;
|
||||
use byteview::ByteView;
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use vecdb::{CheckedSub, PrintableIndex, StoredCompressed};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
@@ -29,6 +30,7 @@ use super::StoredU32;
|
||||
Serialize,
|
||||
StoredCompressed,
|
||||
Allocative,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub struct TxIndex(u32);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user