mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-28 16:49:58 -07:00
server: api + doc
This commit is contained in:
@@ -1,239 +1,16 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
transform::TransformOperation,
|
||||
};
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
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 brk_structs::{AddressInfo, AddressPath};
|
||||
|
||||
use crate::extended::TransformResponseExtended;
|
||||
use crate::extended::{ResponseExtended, ResultExtended, TransformResponseExtended};
|
||||
|
||||
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
|
||||
#[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")]
|
||||
address: String,
|
||||
}
|
||||
|
||||
async fn get_address_info(
|
||||
Path(AddressPath { address }): Path<AddressPath>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Json<AddressInfo>, (StatusCode, Json<&'static str>)> {
|
||||
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,
|
||||
Json("The provided address isn't the Bitcoin Network."),
|
||||
));
|
||||
}
|
||||
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,
|
||||
Json("The provided address is invalid."),
|
||||
));
|
||||
};
|
||||
|
||||
let type_ = OutputType::from(&script);
|
||||
let Ok(bytes) = AddressBytes::try_from((&script, type_)) else {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("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((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json("Address not found in the blockchain (no transaction history)"),
|
||||
));
|
||||
};
|
||||
|
||||
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,
|
||||
Json("The provided address uses an unsupported type"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
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.).")
|
||||
.with_ok_response::<AddressInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
}
|
||||
|
||||
pub trait AddressesRoutes {
|
||||
fn add_addresses_routes(self) -> Self;
|
||||
}
|
||||
@@ -242,7 +19,24 @@ 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),
|
||||
get_with(async |Path(address): Path<AddressPath>,
|
||||
State(app_state): State<AppState>|
|
||||
-> Result<Response, (StatusCode, Json<String>)> {
|
||||
let address_info = app_state.interface.get_address_info(address).to_server_result()?;
|
||||
|
||||
let bytes = sonic_rs::to_vec(&address_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
}, |op| 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.).")
|
||||
.with_ok_response::<AddressInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +1,16 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
transform::TransformOperation,
|
||||
};
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
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 brk_structs::{TransactionInfo, TxidPath};
|
||||
|
||||
use crate::extended::{ResponseExtended, TransformResponseExtended};
|
||||
use crate::extended::{ResponseExtended, ResultExtended, TransformResponseExtended};
|
||||
|
||||
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
|
||||
#[schemars(example = &"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")]
|
||||
txid: String,
|
||||
}
|
||||
|
||||
async fn get_transaction_info(
|
||||
Path(TxidPath { txid }): Path<TxidPath>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Response, (StatusCode, Json<&'static str>)> {
|
||||
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("The provided TXID appears to be invalid."),
|
||||
));
|
||||
};
|
||||
|
||||
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,
|
||||
Json("Failed to found the TXID in the blockchain."),
|
||||
));
|
||||
};
|
||||
|
||||
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,
|
||||
Json("Failed to read the transaction (get blk's path)"),
|
||||
));
|
||||
};
|
||||
|
||||
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,
|
||||
Json("Failed to read the transaction (open file)"),
|
||||
));
|
||||
};
|
||||
|
||||
if file
|
||||
.seek(SeekFrom::Start(position.offset() as u64))
|
||||
.is_err()
|
||||
{
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to read the transaction (file seek)"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; *len as usize];
|
||||
if file.read_exact(&mut buffer).is_err() {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to read the transaction (read exact)"),
|
||||
));
|
||||
}
|
||||
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,
|
||||
Json("Failed decode the transaction"),
|
||||
));
|
||||
};
|
||||
|
||||
let tx_info = TransactionInfo { txid, index, tx };
|
||||
|
||||
let bytes = sonic_rs::to_vec(&tx_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
}
|
||||
|
||||
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.",
|
||||
)
|
||||
.with_ok_response::<TransactionInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
}
|
||||
|
||||
pub trait TransactionsRoutes {
|
||||
fn add_transactions_routes(self) -> Self;
|
||||
}
|
||||
@@ -164,7 +19,28 @@ 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),
|
||||
get_with(
|
||||
async |Path(txid): Path<TxidPath>,
|
||||
State(app_state): State<AppState>|
|
||||
-> Result<Response, (StatusCode, Json<String>)> {
|
||||
let tx_info = app_state.interface.get_transaction_info(txid).to_server_result()?;
|
||||
|
||||
let bytes = sonic_rs::to_vec(&tx_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
},
|
||||
|op| 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.",
|
||||
)
|
||||
.with_ok_response::<TransactionInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_interface::{Format, Output, Params};
|
||||
use brk_interface::{Output, Params};
|
||||
use brk_structs::Format;
|
||||
use quick_cache::sync::GuardResult;
|
||||
use vecdb::Stamp;
|
||||
|
||||
|
||||
@@ -6,13 +6,9 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use brk_interface::{
|
||||
MetricCount, PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt,
|
||||
};
|
||||
use brk_structs::{Index, IndexInfo};
|
||||
use brk_interface::{PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt};
|
||||
use brk_structs::{Index, IndexInfo, MetricCount, MetricPath};
|
||||
use brk_traversable::TreeNode;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
@@ -27,13 +23,6 @@ pub trait ApiMetricsRoutes {
|
||||
fn add_metrics_routes(self) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct MetricPath {
|
||||
/// Metric name
|
||||
#[schemars(example = &"price_close", example = &"market_cap", example = &"realized_price")]
|
||||
metric: String,
|
||||
}
|
||||
|
||||
const TO_SEPARATOR: &str = "_to_";
|
||||
|
||||
impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
|
||||
@@ -10,8 +10,7 @@ use axum::{
|
||||
response::{Html, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use brk_structs::Health;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
@@ -31,14 +30,6 @@ 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_chain_routes()
|
||||
@@ -62,7 +53,7 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
async || -> Json<Health> {
|
||||
Json(Health {
|
||||
status: "healthy".to_string(),
|
||||
service: "brk-server".to_string(),
|
||||
service: "brk".to_string(),
|
||||
timestamp: jiff::Timestamp::now().to_string(),
|
||||
})
|
||||
},
|
||||
@@ -101,5 +92,9 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
),
|
||||
)
|
||||
.route("/api", get(Html::from(include_str!("./scalar.html"))))
|
||||
.route(
|
||||
"/api/{*path}",
|
||||
get(|| async { Redirect::permanent("/api") }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,17 @@ use aide::openapi::{Info, OpenApi, Tag};
|
||||
use crate::VERSION;
|
||||
|
||||
pub fn create_openapi() -> OpenApi {
|
||||
let info = Info {
|
||||
title: "Bitcoin Research Kit".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](/install) or use the [hosting service](/service)."
|
||||
.to_string(),
|
||||
),
|
||||
version: format!("v{VERSION}"),
|
||||
..Info::default()
|
||||
};
|
||||
|
||||
let tags = vec![
|
||||
Tag {
|
||||
name: "Chain".to_string(),
|
||||
@@ -44,16 +55,7 @@ pub fn create_openapi() -> OpenApi {
|
||||
];
|
||||
|
||||
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()
|
||||
},
|
||||
info,
|
||||
tags,
|
||||
..OpenApi::default()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user