server: documentation part 1

This commit is contained in:
nym21
2025-10-06 22:53:50 +02:00
parent db344749b6
commit 7ff79c3164
23 changed files with 670 additions and 183 deletions
Generated
+43 -5
View File
@@ -514,6 +514,27 @@ dependencies = [
"brk_structs",
]
[[package]]
name = "brk-aide"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25c11ed19c06e037fb0f6ab12f218ec79ed53c7daa7a5b55865112e2320097f"
dependencies = [
"axum",
"bytes",
"cfg-if",
"http",
"indexmap 2.11.4",
"schemars 1.0.4",
"serde",
"serde_json",
"serde_qs",
"thiserror 2.0.17",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "brk-file-id"
version = "0.2.3"
@@ -710,7 +731,7 @@ dependencies = [
name = "brk_mcp"
version = "0.0.111"
dependencies = [
"axum",
"brk-aide",
"brk_interface",
"brk_rmcp",
"log",
@@ -1132,6 +1153,7 @@ dependencies = [
"axum",
"bitcoin",
"bitcoincore-rpc",
"brk-aide",
"brk_computer",
"brk_error",
"brk_fetcher",
@@ -1144,6 +1166,7 @@ dependencies = [
"jiff",
"log",
"quick_cache",
"schemars 1.0.4",
"serde",
"sonic-rs 0.5.5",
"tokio",
@@ -1193,6 +1216,7 @@ dependencies = [
"num_enum",
"rapidhash",
"ryu",
"schemars 1.0.4",
"serde",
"serde_bytes",
"strum",
@@ -1939,9 +1963,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04bcaeafafdd3cd1cb5d986ff32096ad1136630207c49b9091e3ae541090d938"
checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
dependencies = [
"crc32fast",
"libz-rs-sys",
@@ -4136,6 +4160,7 @@ checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
dependencies = [
"chrono",
"dyn-clone",
"indexmap 2.11.4",
"ref-cast",
"schemars_derive",
"serde",
@@ -4305,6 +4330,19 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_qs"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6"
dependencies = [
"axum",
"futures",
"percent-encoding",
"serde",
"thiserror 2.0.17",
]
[[package]]
name = "serde_spanned"
version = "1.0.2"
@@ -5042,9 +5080,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
+1
View File
@@ -41,6 +41,7 @@ panic = "abort"
debug-assertions = false
[workspace.dependencies]
aide = { version = "0.15.1", features = ["axum-json"], package = "brk-aide" }
allocative = { version = "0.3.4", features = ["parking_lot"] }
axum = "0.8.6"
bitcoin = { version = "0.32.7", features = ["serde"] }
+2 -2
View File
@@ -1342,7 +1342,7 @@ impl Vecs {
.height_to_supply
.into_iter()
.unwrap_get_inner(prev_height);
state.supply.utxos = *self
state.supply.utxo_count = *self
.height_to_utxo_count
.into_iter()
.unwrap_get_inner(prev_height);
@@ -1579,7 +1579,7 @@ impl Vecs {
self.height_to_utxo_count.forced_push_at(
height,
StoredU64::from(state.supply.utxos),
StoredU64::from(state.supply.utxo_count),
exit,
)?;
+3 -3
View File
@@ -1899,7 +1899,7 @@ impl AddressTypeToVec<(TypeIndex, Sats)> {
let addressdata = addressdata_withsource.deref_mut();
let prev_amount = addressdata.amount();
let prev_amount = addressdata.balance();
let amount = prev_amount + value;
@@ -2000,11 +2000,11 @@ impl HeightToAddressTypeToVec<(TypeIndex, Sats)> {
let addressdata = addressdata_withsource.deref_mut();
let prev_amount = addressdata.amount();
let prev_amount = addressdata.balance();
let amount = prev_amount.checked_sub(value).unwrap();
let will_be_empty = addressdata.utxos - 1 == 0;
let will_be_empty = addressdata.utxo_count - 1 == 0;
if will_be_empty
|| vecs.amount_range.get_mut(amount).0.clone()
@@ -44,19 +44,22 @@ impl AddressCohortState {
let prev_realized_price = compute_price.then(|| addressdata.realized_price());
let prev_supply_state = SupplyState {
utxos: addressdata.utxos as u64,
value: addressdata.amount(),
utxo_count: addressdata.utxo_count as u64,
value: addressdata.balance(),
};
addressdata.send(value, prev_price)?;
let supply_state = SupplyState {
utxos: addressdata.utxos as u64,
value: addressdata.amount(),
utxo_count: addressdata.utxo_count as u64,
value: addressdata.balance(),
};
self.inner.send_(
&SupplyState { utxos: 1, value },
&SupplyState {
utxo_count: 1,
value,
},
current_price,
prev_price,
blocks_old,
@@ -79,19 +82,22 @@ impl AddressCohortState {
let prev_realized_price = compute_price.then(|| address_data.realized_price());
let prev_supply_state = SupplyState {
utxos: address_data.utxos as u64,
value: address_data.amount(),
utxo_count: address_data.utxo_count as u64,
value: address_data.balance(),
};
address_data.receive(value, price);
let supply_state = SupplyState {
utxos: address_data.utxos as u64,
value: address_data.amount(),
utxo_count: address_data.utxo_count as u64,
value: address_data.balance(),
};
self.inner.receive_(
&SupplyState { utxos: 1, value },
&SupplyState {
utxo_count: 1,
value,
},
price,
compute_price.then(|| (address_data.realized_price(), &supply_state)),
prev_realized_price.map(|prev_price| (prev_price, &prev_supply_state)),
@@ -204,7 +204,7 @@ impl CohortState {
price_to_amount_increment: Option<(Dollars, &SupplyState)>,
price_to_amount_decrement: Option<(Dollars, &SupplyState)>,
) {
if supply_state.utxos == 0 {
if supply_state.utxo_count == 0 {
return;
}
+7 -7
View File
@@ -6,7 +6,7 @@ use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
#[derive(Debug, Default, Clone, FromBytes, Immutable, IntoBytes, KnownLayout, Serialize)]
pub struct SupplyState {
pub utxos: u64,
pub utxo_count: u64,
pub value: Sats,
}
@@ -14,7 +14,7 @@ impl Add<SupplyState> for SupplyState {
type Output = Self;
fn add(self, rhs: SupplyState) -> Self::Output {
Self {
utxos: self.utxos + rhs.utxos,
utxo_count: self.utxo_count + rhs.utxo_count,
value: self.value + rhs.value,
}
}
@@ -28,14 +28,14 @@ impl AddAssign<SupplyState> for SupplyState {
impl AddAssign<&SupplyState> for SupplyState {
fn add_assign(&mut self, rhs: &Self) {
self.utxos += rhs.utxos;
self.utxo_count += rhs.utxo_count;
self.value += rhs.value;
}
}
impl SubAssign<&SupplyState> for SupplyState {
fn sub_assign(&mut self, rhs: &Self) {
self.utxos = self.utxos.checked_sub(rhs.utxos).unwrap();
self.utxo_count = self.utxo_count.checked_sub(rhs.utxo_count).unwrap();
self.value = self.value.checked_sub(rhs.value).unwrap();
}
}
@@ -43,14 +43,14 @@ impl SubAssign<&SupplyState> for SupplyState {
impl From<&LoadedAddressData> for SupplyState {
fn from(value: &LoadedAddressData) -> Self {
Self {
utxos: value.utxos as u64,
value: value.amount(),
utxo_count: value.utxo_count as u64,
value: value.balance(),
}
}
}
impl std::fmt::Display for SupplyState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "utxos: {}, value: {}", self.utxos, self.value)
write!(f, "utxos: {}, value: {}", self.utxo_count, self.value)
}
}
+4 -1
View File
@@ -14,7 +14,10 @@ pub struct Transacted {
impl Transacted {
#[allow(clippy::inconsistent_digit_grouping)]
pub fn iterate(&mut self, value: Sats, _type: OutputType) {
let supply = SupplyState { utxos: 1, value };
let supply = SupplyState {
utxo_count: 1,
value,
};
*self.by_type.get_mut(_type) += &supply;
+1 -1
View File
@@ -10,7 +10,7 @@ rust-version.workspace = true
build = "build.rs"
[dependencies]
axum = { workspace = true }
aide = { workspace = true }
brk_interface = { workspace = true }
brk_rmcp = { version = "0.8.0", features = [
"transport-worker",
+2 -2
View File
@@ -1,4 +1,4 @@
use axum::Router;
use aide::axum::ApiRouter;
use brk_interface::Interface;
use brk_rmcp::transport::{
StreamableHttpServerConfig,
@@ -13,7 +13,7 @@ pub trait MCPRoutes {
fn add_mcp_routes(self, interface: &'static Interface<'static>, mcp: bool) -> Self;
}
impl<T> MCPRoutes for Router<T>
impl<T> MCPRoutes for ApiRouter<T>
where
T: Clone + Send + Sync + 'static,
{
+3
View File
@@ -10,6 +10,7 @@ rust-version.workspace = true
build = "build.rs"
[dependencies]
aide = { workspace = true }
axum = { workspace = true }
bitcoin = { workspace = true }
bitcoincore-rpc = { workspace = true }
@@ -26,7 +27,9 @@ vecdb = { workspace = true }
jiff = { workspace = true }
log = { workspace = true }
quick_cache = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
# serde_json = { workspace = true }
sonic-rs = { workspace = true }
tokio = { workspace = true }
tower-http = { version = "0.6.6", features = ["compression-full", "trace"] }
+244 -119
View File
@@ -4,19 +4,27 @@ use std::{
str::FromStr,
};
use aide::{
axum::{ApiRouter, IntoApiResponse, routing::get_with},
transform::TransformOperation,
};
use axum::{
Json, Router,
Json,
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
};
use bitcoin::{Address, Network, Transaction, consensus::Decodable};
use bitcoin::{
Address as BitcoinAddress, Network, Transaction as BitcoinTransaction, consensus::Decodable,
};
use brk_parser::XORIndex;
use brk_structs::{
AddressBytesHash, AnyAddressDataIndexEnum, Bitcoin, OutputType, TxIndex, Txid, TxidPrefix,
AddressBytesHash, AnyAddressDataIndexEnum, Bitcoin, Dollars, OutputType, Sats, TxIndex, Txid,
TxidPrefix, TypeIndex,
};
use serde::Serialize;
use sonic_rs::{Number, Value};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{AnyIterableVec, VecIterator};
use super::AppState;
@@ -25,127 +33,244 @@ pub trait ApiExplorerRoutes {
fn add_api_explorer_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: Transaction,
tx: BitcoinTransaction,
}
impl ApiExplorerRoutes for Router<AppState> {
#[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.route(
self.api_route(
"/api/chain/address/{address}",
get(
async |Path(address): Path<String>, state: State<AppState>| -> Response {
let Ok(address) = Address::from_str(&address) else {
return "Invalid address".into_response();
};
if !address.is_valid_for_network(Network::Bitcoin) {
return "Invalid address".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 Ok(Some(addri)) = stores
.addressbyteshash_to_typeindex
.get(&hash)
.map(|opt| opt.map(|cow| cow.into_owned()))
else {
return "Unknown address".into_response();
};
let output_type = OutputType::from(&address);
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 anyaddri = match output_type {
OutputType::P2PK33 => stateful
.p2pk33addressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
OutputType::P2PK65 => stateful
.p2pk65addressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
OutputType::P2PKH => stateful
.p2pkhaddressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
OutputType::P2SH => stateful
.p2shaddressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
OutputType::P2TR => stateful
.p2traddressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
OutputType::P2WPKH => stateful
.p2wpkhaddressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
OutputType::P2WSH => stateful
.p2wshaddressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
OutputType::P2A => stateful
.p2aaddressindex_to_anyaddressindex
.iter()
.unwrap_get_inner(addri.into()),
_ => unreachable!(),
};
let addr_data = match anyaddri.to_enum() {
AnyAddressDataIndexEnum::Loaded(loadedi) => stateful
.loadedaddressindex_to_loadedaddressdata
.iter()
.unwrap_get_inner(loadedi),
AnyAddressDataIndexEnum::Empty(emptyi) => stateful
.emptyaddressindex_to_emptyaddressdata
.iter()
.unwrap_get_inner(emptyi)
.into(),
};
let amount = addr_data.amount();
Json(sonic_rs::json!({
"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
}))
.into_response()
},
),
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}",
@@ -215,7 +340,7 @@ impl ApiExplorerRoutes for Router<AppState> {
xori.bytes(&mut buffer, parser.xor_bytes());
let mut reader = Cursor::new(buffer);
let Ok(tx) = Transaction::consensus_decode(&mut reader) else {
let Ok(tx) = BitcoinTransaction::consensus_decode(&mut reader) else {
return "Error decoding transaction".into_response();
};
+3 -2
View File
@@ -1,5 +1,6 @@
use aide::axum::ApiRouter;
use axum::{
Json, Router,
Json,
extract::{Path, Query, State},
http::{HeaderMap, Uri},
response::{IntoResponse, Response},
@@ -22,7 +23,7 @@ pub trait ApiMetricsRoutes {
const TO_SEPARATOR: &str = "_to_";
impl ApiMetricsRoutes for Router<AppState> {
impl ApiMetricsRoutes for ApiRouter<AppState> {
fn add_api_metrics_routes(self) -> Self {
self.route(
"/api/metrics/count",
+4 -10
View File
@@ -1,4 +1,5 @@
use axum::{Router, response::Redirect, routing::get};
use aide::axum::ApiRouter;
use axum::{response::Html, routing::get};
use crate::api::{chain::ApiExplorerRoutes, metrics::ApiMetricsRoutes};
@@ -11,17 +12,10 @@ pub trait ApiRoutes {
fn add_api_routes(self) -> Self;
}
impl ApiRoutes for Router<AppState> {
impl ApiRoutes for ApiRouter<AppState> {
fn add_api_routes(self) -> Self {
self.add_api_explorer_routes()
.add_api_metrics_routes()
.route(
"/api",
get(|| async {
Redirect::temporary(
"https://github.com/bitcoinresearchkit/brk/tree/main/crates/brk_server#api",
)
}),
)
.route("/api", get(Html::from(include_str!("./scalar.html"))))
}
}
+26
View File
@@ -0,0 +1,26 @@
<!doctype html>
<html>
<head>
<title>Scalar API Reference</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
<!-- https://app.unpkg.com/@scalar/api-reference@1.37.0/files/dist/browser/standalone.js -->
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
<script>
Scalar.createApiReference("#app", {
url: "/api.json",
metaData: {
title: "BRK API",
description: "Bitcoin Research Kit's API documentation via Scalar",
hideClientButton: true,
},
});
</script>
</body>
</html>
+3 -2
View File
@@ -1,6 +1,7 @@
use std::path::PathBuf;
use axum::{Router, routing::get};
use aide::axum::ApiRouter;
use axum::routing::get;
use super::AppState;
@@ -12,7 +13,7 @@ pub trait FilesRoutes {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self;
}
impl FilesRoutes for Router<AppState> {
impl FilesRoutes for ApiRouter<AppState> {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self {
if path.is_some() {
self.route("/{*path}", get(file_handler))
+33 -4
View File
@@ -5,9 +5,13 @@
use std::{path::PathBuf, sync::Arc, time::Duration};
use aide::{
axum::{ApiRouter, IntoApiResponse},
openapi::{Info, OpenApi},
};
use api::ApiRoutes;
use axum::{
Json, Router,
Extension, Json,
body::{Body, Bytes},
http::{Request, Response, StatusCode, Uri},
middleware::Next,
@@ -94,11 +98,11 @@ impl Server {
.on_failure(())
.on_eos(());
let router = Router::new()
let router = ApiRouter::new()
.add_api_routes()
.add_files_routes(state.path.as_ref())
.add_mcp_routes(state.interface, mcp)
.route("/version", get(Json(VERSION)))
.api_route("/version", aide::axum::routing::get(version))
.route(
"/health",
get(Json(sonic_rs::json!({
@@ -126,6 +130,7 @@ 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)
@@ -142,12 +147,36 @@ 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();
serve(listener, router).await?;
serve(
listener,
router
.finish_api(&mut api)
.layer(Extension(Arc::new(api)))
.into_make_service(),
)
.await?;
Ok(())
}
}
async fn serve_api(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoApiResponse {
Json(api)
}
async fn version() -> impl IntoApiResponse {
Json(VERSION)
}
+1
View File
@@ -22,6 +22,7 @@ jiff = { workspace = true }
num_enum = "0.7.4"
rapidhash = "4.1.0"
ryu = "1.0.20"
schemars = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
strum = { version = "0.27", features = ["derive"] }
@@ -7,6 +7,7 @@ use std::{
use allocative::Allocative;
use derive_deref::Deref;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, StoredCompressed};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
@@ -29,6 +30,7 @@ use super::{Bitcoin, Cents, Close, High, Sats, StoredF32, StoredF64};
Deserialize,
StoredCompressed,
Allocative,
JsonSchema,
)]
pub struct Dollars(f64);
@@ -11,23 +11,23 @@ pub struct LoadedAddressData {
pub sent: Sats,
pub received: Sats,
pub realized_cap: Dollars,
pub utxos: u32,
pub utxo_count: u32,
#[serde(skip)]
padding: u32,
}
impl LoadedAddressData {
pub fn amount(&self) -> Sats {
pub fn balance(&self) -> Sats {
(u64::from(self.received) - u64::from(self.sent)).into()
}
pub fn realized_price(&self) -> Dollars {
let p = (self.realized_cap / Bitcoin::from(self.amount())).round_to(4);
let p = (self.realized_cap / Bitcoin::from(self.balance())).round_to(4);
if p.is_negative() {
dbg!((
self.realized_cap,
self.amount(),
Bitcoin::from(self.amount()),
self.balance(),
Bitcoin::from(self.balance()),
p
));
panic!("");
@@ -37,17 +37,17 @@ impl LoadedAddressData {
#[inline]
pub fn has_0_sats(&self) -> bool {
self.amount() == Sats::ZERO
self.balance() == Sats::ZERO
}
#[inline]
pub fn has_0_utxos(&self) -> bool {
self.utxos == 0
self.utxo_count == 0
}
pub fn receive(&mut self, amount: Sats, price: Option<Dollars>) {
self.received += amount;
self.utxos += 1;
self.utxo_count += 1;
if let Some(price) = price {
let added = price * amount;
self.realized_cap += added;
@@ -59,11 +59,11 @@ impl LoadedAddressData {
}
pub fn send(&mut self, amount: Sats, previous_price: Option<Dollars>) -> Result<()> {
if self.amount() < amount {
if self.balance() < amount {
return Err(Error::Str("Previous_amount smaller than sent amount"));
}
self.sent += amount;
self.utxos -= 1;
self.utxo_count -= 1;
if let Some(previous_price) = previous_price {
let subtracted = previous_price * amount;
let realized_cap = self.realized_cap.checked_sub(subtracted).unwrap();
@@ -96,7 +96,7 @@ impl From<&EmptyAddressData> for LoadedAddressData {
sent: value.transfered,
received: value.transfered,
realized_cap: Dollars::ZERO,
utxos: 0,
utxo_count: 0,
padding: 0,
}
}
@@ -107,7 +107,7 @@ impl std::fmt::Display for LoadedAddressData {
write!(
f,
"sent: {}, received: {}, realized_cap: {}, utxos: {}",
self.sent, self.received, self.realized_cap, self.utxos
self.sent, self.received, self.realized_cap, self.utxo_count
)
}
}
+248 -1
View File
@@ -1,6 +1,7 @@
use bitcoin::{Address, AddressType, ScriptBuf, opcodes::all::OP_PUSHBYTES_2};
use brk_error::Error;
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum::Display;
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
@@ -18,10 +19,12 @@ use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
IntoBytes,
KnownLayout,
Serialize,
JsonSchema,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[repr(u8)]
/// Type (P2PKH, P2WPKH, P2SH, P2TR, etc.)
pub enum OutputType {
P2PK65,
P2PK33,
@@ -33,249 +36,493 @@ pub enum OutputType {
P2WSH,
P2TR,
P2A,
#[schemars(skip)]
Dummy10,
#[schemars(skip)]
Dummy11,
#[schemars(skip)]
Dummy12,
#[schemars(skip)]
Dummy13,
#[schemars(skip)]
Dummy14,
#[schemars(skip)]
Dummy15,
#[schemars(skip)]
Dummy16,
#[schemars(skip)]
Dummy17,
#[schemars(skip)]
Dummy18,
#[schemars(skip)]
Dummy19,
#[schemars(skip)]
Dummy20,
#[schemars(skip)]
Dummy21,
#[schemars(skip)]
Dummy22,
#[schemars(skip)]
Dummy23,
#[schemars(skip)]
Dummy24,
#[schemars(skip)]
Dummy25,
#[schemars(skip)]
Dummy26,
#[schemars(skip)]
Dummy27,
#[schemars(skip)]
Dummy28,
#[schemars(skip)]
Dummy29,
#[schemars(skip)]
Dummy30,
#[schemars(skip)]
Dummy31,
#[schemars(skip)]
Dummy32,
#[schemars(skip)]
Dummy33,
#[schemars(skip)]
Dummy34,
#[schemars(skip)]
Dummy35,
#[schemars(skip)]
Dummy36,
#[schemars(skip)]
Dummy37,
#[schemars(skip)]
Dummy38,
#[schemars(skip)]
Dummy39,
#[schemars(skip)]
Dummy40,
#[schemars(skip)]
Dummy41,
#[schemars(skip)]
Dummy42,
#[schemars(skip)]
Dummy43,
#[schemars(skip)]
Dummy44,
#[schemars(skip)]
Dummy45,
#[schemars(skip)]
Dummy46,
#[schemars(skip)]
Dummy47,
#[schemars(skip)]
Dummy48,
#[schemars(skip)]
Dummy49,
#[schemars(skip)]
Dummy50,
#[schemars(skip)]
Dummy51,
#[schemars(skip)]
Dummy52,
#[schemars(skip)]
Dummy53,
#[schemars(skip)]
Dummy54,
#[schemars(skip)]
Dummy55,
#[schemars(skip)]
Dummy56,
#[schemars(skip)]
Dummy57,
#[schemars(skip)]
Dummy58,
#[schemars(skip)]
Dummy59,
#[schemars(skip)]
Dummy60,
#[schemars(skip)]
Dummy61,
#[schemars(skip)]
Dummy62,
#[schemars(skip)]
Dummy63,
#[schemars(skip)]
Dummy64,
#[schemars(skip)]
Dummy65,
#[schemars(skip)]
Dummy66,
#[schemars(skip)]
Dummy67,
#[schemars(skip)]
Dummy68,
#[schemars(skip)]
Dummy69,
#[schemars(skip)]
Dummy70,
#[schemars(skip)]
Dummy71,
#[schemars(skip)]
Dummy72,
#[schemars(skip)]
Dummy73,
#[schemars(skip)]
Dummy74,
#[schemars(skip)]
Dummy75,
#[schemars(skip)]
Dummy76,
#[schemars(skip)]
Dummy77,
#[schemars(skip)]
Dummy78,
#[schemars(skip)]
Dummy79,
#[schemars(skip)]
Dummy80,
#[schemars(skip)]
Dummy81,
#[schemars(skip)]
Dummy82,
#[schemars(skip)]
Dummy83,
#[schemars(skip)]
Dummy84,
#[schemars(skip)]
Dummy85,
#[schemars(skip)]
Dummy86,
#[schemars(skip)]
Dummy87,
#[schemars(skip)]
Dummy88,
#[schemars(skip)]
Dummy89,
#[schemars(skip)]
Dummy90,
#[schemars(skip)]
Dummy91,
#[schemars(skip)]
Dummy92,
#[schemars(skip)]
Dummy93,
#[schemars(skip)]
Dummy94,
#[schemars(skip)]
Dummy95,
#[schemars(skip)]
Dummy96,
#[schemars(skip)]
Dummy97,
#[schemars(skip)]
Dummy98,
#[schemars(skip)]
Dummy99,
#[schemars(skip)]
Dummy100,
#[schemars(skip)]
Dummy101,
#[schemars(skip)]
Dummy102,
#[schemars(skip)]
Dummy103,
#[schemars(skip)]
Dummy104,
#[schemars(skip)]
Dummy105,
#[schemars(skip)]
Dummy106,
#[schemars(skip)]
Dummy107,
#[schemars(skip)]
Dummy108,
#[schemars(skip)]
Dummy109,
#[schemars(skip)]
Dummy110,
#[schemars(skip)]
Dummy111,
#[schemars(skip)]
Dummy112,
#[schemars(skip)]
Dummy113,
#[schemars(skip)]
Dummy114,
#[schemars(skip)]
Dummy115,
#[schemars(skip)]
Dummy116,
#[schemars(skip)]
Dummy117,
#[schemars(skip)]
Dummy118,
#[schemars(skip)]
Dummy119,
#[schemars(skip)]
Dummy120,
#[schemars(skip)]
Dummy121,
#[schemars(skip)]
Dummy122,
#[schemars(skip)]
Dummy123,
#[schemars(skip)]
Dummy124,
#[schemars(skip)]
Dummy125,
#[schemars(skip)]
Dummy126,
#[schemars(skip)]
Dummy127,
#[schemars(skip)]
Dummy128,
#[schemars(skip)]
Dummy129,
#[schemars(skip)]
Dummy130,
#[schemars(skip)]
Dummy131,
#[schemars(skip)]
Dummy132,
#[schemars(skip)]
Dummy133,
#[schemars(skip)]
Dummy134,
#[schemars(skip)]
Dummy135,
#[schemars(skip)]
Dummy136,
#[schemars(skip)]
Dummy137,
#[schemars(skip)]
Dummy138,
#[schemars(skip)]
Dummy139,
#[schemars(skip)]
Dummy140,
#[schemars(skip)]
Dummy141,
#[schemars(skip)]
Dummy142,
#[schemars(skip)]
Dummy143,
#[schemars(skip)]
Dummy144,
#[schemars(skip)]
Dummy145,
#[schemars(skip)]
Dummy146,
#[schemars(skip)]
Dummy147,
#[schemars(skip)]
Dummy148,
#[schemars(skip)]
Dummy149,
#[schemars(skip)]
Dummy150,
#[schemars(skip)]
Dummy151,
#[schemars(skip)]
Dummy152,
#[schemars(skip)]
Dummy153,
#[schemars(skip)]
Dummy154,
#[schemars(skip)]
Dummy155,
#[schemars(skip)]
Dummy156,
#[schemars(skip)]
Dummy157,
#[schemars(skip)]
Dummy158,
#[schemars(skip)]
Dummy159,
#[schemars(skip)]
Dummy160,
#[schemars(skip)]
Dummy161,
#[schemars(skip)]
Dummy162,
#[schemars(skip)]
Dummy163,
#[schemars(skip)]
Dummy164,
#[schemars(skip)]
Dummy165,
#[schemars(skip)]
Dummy166,
#[schemars(skip)]
Dummy167,
#[schemars(skip)]
Dummy168,
#[schemars(skip)]
Dummy169,
#[schemars(skip)]
Dummy170,
#[schemars(skip)]
Dummy171,
#[schemars(skip)]
Dummy172,
#[schemars(skip)]
Dummy173,
#[schemars(skip)]
Dummy174,
#[schemars(skip)]
Dummy175,
#[schemars(skip)]
Dummy176,
#[schemars(skip)]
Dummy177,
#[schemars(skip)]
Dummy178,
#[schemars(skip)]
Dummy179,
#[schemars(skip)]
Dummy180,
#[schemars(skip)]
Dummy181,
#[schemars(skip)]
Dummy182,
#[schemars(skip)]
Dummy183,
#[schemars(skip)]
Dummy184,
#[schemars(skip)]
Dummy185,
#[schemars(skip)]
Dummy186,
#[schemars(skip)]
Dummy187,
#[schemars(skip)]
Dummy188,
#[schemars(skip)]
Dummy189,
#[schemars(skip)]
Dummy190,
#[schemars(skip)]
Dummy191,
#[schemars(skip)]
Dummy192,
#[schemars(skip)]
Dummy193,
#[schemars(skip)]
Dummy194,
#[schemars(skip)]
Dummy195,
#[schemars(skip)]
Dummy196,
#[schemars(skip)]
Dummy197,
#[schemars(skip)]
Dummy198,
#[schemars(skip)]
Dummy199,
#[schemars(skip)]
Dummy200,
#[schemars(skip)]
Dummy201,
#[schemars(skip)]
Dummy202,
#[schemars(skip)]
Dummy203,
#[schemars(skip)]
Dummy204,
#[schemars(skip)]
Dummy205,
#[schemars(skip)]
Dummy206,
#[schemars(skip)]
Dummy207,
#[schemars(skip)]
Dummy208,
#[schemars(skip)]
Dummy209,
#[schemars(skip)]
Dummy210,
#[schemars(skip)]
Dummy211,
#[schemars(skip)]
Dummy212,
#[schemars(skip)]
Dummy213,
#[schemars(skip)]
Dummy214,
#[schemars(skip)]
Dummy215,
#[schemars(skip)]
Dummy216,
#[schemars(skip)]
Dummy217,
#[schemars(skip)]
Dummy218,
#[schemars(skip)]
Dummy219,
#[schemars(skip)]
Dummy220,
#[schemars(skip)]
Dummy221,
#[schemars(skip)]
Dummy222,
#[schemars(skip)]
Dummy223,
#[schemars(skip)]
Dummy224,
#[schemars(skip)]
Dummy225,
#[schemars(skip)]
Dummy226,
#[schemars(skip)]
Dummy227,
#[schemars(skip)]
Dummy228,
#[schemars(skip)]
Dummy229,
#[schemars(skip)]
Dummy230,
#[schemars(skip)]
Dummy231,
#[schemars(skip)]
Dummy232,
#[schemars(skip)]
Dummy233,
#[schemars(skip)]
Dummy234,
#[schemars(skip)]
Dummy235,
#[schemars(skip)]
Dummy236,
#[schemars(skip)]
Dummy237,
#[schemars(skip)]
Dummy238,
#[schemars(skip)]
Dummy239,
#[schemars(skip)]
Dummy240,
#[schemars(skip)]
Dummy241,
#[schemars(skip)]
Dummy242,
#[schemars(skip)]
Dummy243,
#[schemars(skip)]
Dummy244,
#[schemars(skip)]
Dummy245,
#[schemars(skip)]
Dummy246,
#[schemars(skip)]
Dummy247,
#[schemars(skip)]
Dummy248,
#[schemars(skip)]
Dummy249,
#[schemars(skip)]
Dummy250,
#[schemars(skip)]
Dummy251,
#[schemars(skip)]
Dummy252,
#[schemars(skip)]
Dummy253,
Empty = 254,
Unknown = 255,
+2
View File
@@ -6,6 +6,7 @@ use std::{
use allocative::Allocative;
use bitcoin::Amount;
use derive_deref::Deref;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, StoredCompressed};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
@@ -32,6 +33,7 @@ use super::{Bitcoin, Cents, Dollars, Height};
Deserialize,
StoredCompressed,
Allocative,
JsonSchema,
)]
pub struct Sats(u64);
+9 -1
View File
@@ -1,13 +1,15 @@
use std::ops::Add;
use byteview::ByteView;
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, StoredCompressed};
use zerocopy::IntoBytes;
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::copy_first_4bytes;
/// Index within its type (e.g., 0 for first P2WPKH address)
#[derive(
Debug,
PartialEq,
@@ -22,11 +24,17 @@ use crate::copy_first_4bytes;
IntoBytes,
KnownLayout,
Serialize,
Deserialize,
StoredCompressed,
JsonSchema,
)]
pub struct TypeIndex(u32);
impl TypeIndex {
pub fn new(i: u32) -> Self {
Self(i)
}
pub fn increment(&mut self) {
self.0 += 1;
}