mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-26 15:49:58 -07:00
server: api doc part 3
This commit is contained in:
@@ -18,6 +18,8 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::{AnyIterableVec, VecIterator};
|
||||
|
||||
use crate::extended::TransformResponseExtended;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
@@ -88,13 +90,14 @@ struct AddressInfo {
|
||||
#[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> {
|
||||
) -> Result<Json<AddressInfo>, (StatusCode, Json<&'static str>)> {
|
||||
let interface = state.interface;
|
||||
let indexer = interface.indexer();
|
||||
let computer = interface.computer();
|
||||
@@ -102,19 +105,28 @@ async fn get_address_info(
|
||||
|
||||
let script = if let Ok(address) = BitcoinAddress::from_str(&address) {
|
||||
if !address.is_valid_for_network(Network::Bitcoin) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
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);
|
||||
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::BAD_REQUEST);
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to convert the address to bytes"),
|
||||
));
|
||||
};
|
||||
let hash = AddressBytesHash::from((&bytes, type_));
|
||||
|
||||
@@ -123,7 +135,10 @@ async fn get_address_info(
|
||||
.get(&hash)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json("Address not found in the blockchain (no transaction history)"),
|
||||
));
|
||||
};
|
||||
|
||||
let stateful = &computer.stateful;
|
||||
@@ -172,7 +187,12 @@ async fn get_address_info(
|
||||
.p2aaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
_ => return Err(StatusCode::BAD_REQUEST),
|
||||
_ => {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("The provided address uses an unsupported type"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let address_data = match any_address_index.to_enum() {
|
||||
@@ -207,12 +227,11 @@ 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)")
|
||||
)
|
||||
.with_ok_response::<AddressInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
}
|
||||
|
||||
pub trait AddressesRoutes {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use aide::axum::ApiRouter;
|
||||
use axum::{response::Redirect, routing::get};
|
||||
|
||||
use crate::api::chain::{addresses::AddressesRoutes, transactions::TransactionsRoutes};
|
||||
|
||||
@@ -13,6 +14,8 @@ pub trait ChainRoutes {
|
||||
|
||||
impl ChainRoutes for ApiRouter<AppState> {
|
||||
fn add_chain_routes(self) -> Self {
|
||||
self.add_addresses_routes().add_transactions_routes()
|
||||
self.route("/api/chain", get(Redirect::temporary("/api#tag/chain")))
|
||||
.add_addresses_routes()
|
||||
.add_transactions_routes()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::VecIterator;
|
||||
|
||||
use crate::extended::TransformResponseExtended;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
@@ -41,15 +43,19 @@ struct TransactionInfo {
|
||||
#[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> {
|
||||
) -> Result<Response, (StatusCode, Json<&'static str>)> {
|
||||
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("The provided TXID appears to be invalid."),
|
||||
));
|
||||
};
|
||||
|
||||
let txid = Txid::from(txid);
|
||||
@@ -62,7 +68,10 @@ async fn get_transaction_info(
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
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);
|
||||
@@ -84,32 +93,47 @@ async fn get_transaction_info(
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed decode the transaction"),
|
||||
));
|
||||
};
|
||||
|
||||
let tx_info = TransactionInfo { txid, index, tx };
|
||||
@@ -128,20 +152,11 @@ fn get_transaction_info_docs(op: TransformOperation) -> TransformOperation {
|
||||
.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",
|
||||
)
|
||||
})
|
||||
.with_ok_response::<TransactionInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
}
|
||||
|
||||
pub trait TransactionsRoutes {
|
||||
|
||||
@@ -2,19 +2,21 @@ use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use brk_interface::{
|
||||
Index, Indexes, PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt,
|
||||
MetricCount, PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt,
|
||||
};
|
||||
use brk_structs::{Index, IndexInfo};
|
||||
use brk_traversable::TreeNode;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
extended::{HeaderMapExtended, ResponseExtended},
|
||||
extended::{HeaderMapExtended, ResponseExtended, TransformResponseExtended},
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
@@ -22,50 +24,51 @@ use super::AppState;
|
||||
mod data;
|
||||
|
||||
pub trait ApiMetricsRoutes {
|
||||
fn add_api_metrics_routes(self) -> Self;
|
||||
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_";
|
||||
|
||||
#[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.api_route(
|
||||
fn add_metrics_routes(self) -> Self {
|
||||
self
|
||||
.route("/api/metrics", get(Redirect::temporary("/api#tag/metrics")))
|
||||
.api_route(
|
||||
"/api/metrics/count",
|
||||
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(),
|
||||
})
|
||||
async |State(app_state): State<AppState>| {
|
||||
Json(app_state.interface.metric_count())
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metric count")
|
||||
.description("Current metric count")
|
||||
.with_ok_response::<Vec<MetricCount>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/indexes",
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>| -> Json<&Indexes> {
|
||||
async |State(app_state): State<AppState>| {
|
||||
Json(app_state.interface.get_indexes())
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metric indexes")
|
||||
.description("Available metric indexes and their accepted variants")
|
||||
.summary("List available indexes")
|
||||
.description(
|
||||
"Returns all available indexes with their accepted query aliases. Use any alias when querying metrics."
|
||||
)
|
||||
.with_ok_response::<Vec<IndexInfo>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -73,20 +76,21 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
"/api/metrics/list",
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>,
|
||||
Query(pagination): Query<PaginationParam>|
|
||||
-> Json<PaginatedMetrics> {
|
||||
Query(pagination): Query<PaginationParam>| {
|
||||
Json(app_state.interface.get_metrics(pagination))
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metrics list")
|
||||
.description("Paginated list of available metrics")
|
||||
.with_ok_response::<PaginatedMetrics, _>(|res| res)
|
||||
.with_not_modified()
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
.api_route(
|
||||
"/api/metrics/catalog",
|
||||
get(
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(app_state): State<AppState>| -> Response {
|
||||
let etag = VERSION;
|
||||
|
||||
@@ -97,8 +101,12 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
|
||||
let mut response =
|
||||
Json(app_state.interface.get_metrics_catalog()).into_response();
|
||||
let bytes = sonic_rs::to_vec(&app_state.interface.get_metrics_catalog()).unwrap();
|
||||
|
||||
let mut response = Response::builder()
|
||||
.header("content-type", "application/json")
|
||||
.body(bytes.into())
|
||||
.unwrap();
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
@@ -106,6 +114,15 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
|
||||
response
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metrics catalog")
|
||||
.description(
|
||||
"Returns the complete hierarchical catalog of available metrics organized as a tree structure. Metrics are grouped by categories and subcategories. Best viewed in an interactive JSON viewer (e.g., Firefox's built-in JSON viewer) for easy navigation of the nested structure."
|
||||
)
|
||||
.with_ok_response::<TreeNode, _>(|res| res)
|
||||
.with_not_modified()
|
||||
},
|
||||
),
|
||||
)
|
||||
// TODO:
|
||||
@@ -119,12 +136,28 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
.route(
|
||||
.api_route(
|
||||
"/api/metrics/{metric}",
|
||||
get(
|
||||
async |State(app_state): State<AppState>, Path(metric): Path<String>| -> Response {
|
||||
// If not found do fuzzy search but here or in interface ?
|
||||
Json(app_state.interface.metric_to_indexes(metric)).into_response()
|
||||
get_with(
|
||||
async |
|
||||
State(app_state): State<AppState>,
|
||||
Path(MetricPath { metric }): Path<MetricPath>
|
||||
| {
|
||||
match app_state.interface.metric_to_indexes(metric) {
|
||||
Some(indexes) => Json(indexes).into_response(),
|
||||
None => StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Get supported indexes for a metric")
|
||||
.description(
|
||||
"Returns the list of indexes are supported by the specified metric. \
|
||||
For example, `realized_price` might be available on dateindex, weekindex, and monthindex."
|
||||
)
|
||||
.with_ok_response::<Vec<Index>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_not_found()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,21 +2,29 @@ use std::sync::Arc;
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
openapi::{Info, OpenApi, Tag},
|
||||
openapi::OpenApi,
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
response::{Html, Redirect},
|
||||
routing::get,
|
||||
};
|
||||
use axum::{Extension, Json, response::Html, routing::get};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
api::{chain::ChainRoutes, metrics::ApiMetricsRoutes},
|
||||
extended::TransformResponseExtended,
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
mod chain;
|
||||
mod metrics;
|
||||
mod openapi;
|
||||
|
||||
pub use openapi::*;
|
||||
|
||||
pub trait ApiRoutes {
|
||||
fn add_api_routes(self) -> Self;
|
||||
@@ -33,7 +41,8 @@ struct Health {
|
||||
impl ApiRoutes for ApiRouter<AppState> {
|
||||
fn add_api_routes(self) -> Self {
|
||||
self.add_chain_routes()
|
||||
.add_api_metrics_routes()
|
||||
.add_metrics_routes()
|
||||
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
|
||||
.api_route(
|
||||
"/version",
|
||||
get_with(
|
||||
@@ -42,6 +51,7 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
op.tag("Server")
|
||||
.summary("API version")
|
||||
.description("Returns the current version of the API server")
|
||||
.with_ok_response::<String, _>(|res| res)
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -59,6 +69,7 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
op.tag("Server")
|
||||
.summary("Health check")
|
||||
.description("Returns the health status of the API server")
|
||||
.with_ok_response::<Health, _>(|res| res)
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -73,48 +84,3 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
60
crates/brk_server/src/api/openapi.rs
Normal file
60
crates/brk_server/src/api/openapi.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use aide::openapi::{Info, OpenApi, Tag};
|
||||
|
||||
//
|
||||
// https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html
|
||||
//
|
||||
// Scalar:
|
||||
// - Documentation: https://guides.scalar.com/scalar/scalar-api-references
|
||||
// - Configuration: https://guides.scalar.com/scalar/scalar-api-references/configuration
|
||||
// - Examples:
|
||||
// - https://docs.machines.dev/
|
||||
// - https://tailscale.com/api
|
||||
// - https://api.supabase.com/api/v1
|
||||
//
|
||||
|
||||
use crate::VERSION;
|
||||
|
||||
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,5 +1,7 @@
|
||||
mod header_map;
|
||||
mod response;
|
||||
mod transform_operation;
|
||||
|
||||
pub use header_map::*;
|
||||
pub use response::*;
|
||||
pub use transform_operation::*;
|
||||
|
||||
49
crates/brk_server/src/extended/transform_operation.rs
Normal file
49
crates/brk_server/src/extended/transform_operation.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use aide::transform::{TransformOperation, TransformResponse};
|
||||
use axum::Json;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
pub trait TransformResponseExtended<'t> {
|
||||
/// 200
|
||||
fn with_ok_response<R, F>(self, f: F) -> Self
|
||||
where
|
||||
R: JsonSchema,
|
||||
F: FnOnce(TransformResponse<'_, R>) -> TransformResponse<'_, R>;
|
||||
/// 400
|
||||
fn with_bad_request(self) -> Self;
|
||||
/// 404
|
||||
fn with_not_found(self) -> Self;
|
||||
/// 304
|
||||
fn with_not_modified(self) -> Self;
|
||||
/// 500
|
||||
fn with_server_error(self) -> Self;
|
||||
}
|
||||
|
||||
impl<'t> TransformResponseExtended<'t> for TransformOperation<'t> {
|
||||
fn with_ok_response<R, F>(self, f: F) -> Self
|
||||
where
|
||||
R: JsonSchema,
|
||||
F: FnOnce(TransformResponse<'_, R>) -> TransformResponse<'_, R>,
|
||||
{
|
||||
self.response_with::<200, Json<R>, _>(|res| f(res.description("Successful response")))
|
||||
}
|
||||
|
||||
fn with_bad_request(self) -> Self {
|
||||
self.response_with::<400, Json<String>, _>(|res| {
|
||||
res.description("Invalid request parameters")
|
||||
})
|
||||
}
|
||||
|
||||
fn with_not_found(self) -> Self {
|
||||
self.response_with::<404, Json<String>, _>(|res| res.description("Resource not found"))
|
||||
}
|
||||
|
||||
fn with_not_modified(self) -> Self {
|
||||
self.response_with::<304, (), _>(|res| {
|
||||
res.description("Not modified - content unchanged since last request")
|
||||
})
|
||||
}
|
||||
|
||||
fn with_server_error(self) -> Self {
|
||||
self.response_with::<500, Json<String>, _>(|res| res.description("Internal server error"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user