server: snapshot

This commit is contained in:
nym21
2025-12-15 16:32:45 +01:00
parent 882a3525af
commit 825a4a77c0
100 changed files with 2677 additions and 3438 deletions

View File

@@ -2,16 +2,12 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, Query, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_query::validate_address;
use brk_types::{Address, AddressStats, AddressTxidsParam, AddressValidation, Txid, Utxo};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -31,11 +27,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<Address>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address(address).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address(address)).await
}, |op| op
.addresses_tag()
.summary("Address information")
@@ -55,11 +47,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_txids(address, params.after_txid, params.limit).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(address, params.after_txid, params.limit)).await
}, |op| op
.addresses_tag()
.summary("Address transaction IDs")
@@ -78,11 +66,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<Address>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_utxos(address).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_utxos(address)).await
}, |op| op
.addresses_tag()
.summary("Address UTXOs")
@@ -101,17 +85,13 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<Address>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_mempool_txids(address).await.to_json_response(&etag)
// Mempool txs for an address - use MaxAge since it's volatile
state.cached_json(&headers, CacheStrategy::MaxAge(5), move |q| q.address_mempool_txids(address)).await
}, |op| op
.addresses_tag()
.summary("Address mempool transactions")
.description("Get unconfirmed transaction IDs for an address from the mempool (up to 50).")
.ok_response::<Vec<Txid>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
@@ -125,11 +105,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_address_txids(address, params.after_txid, 25).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(address, params.after_txid, 25)).await
}, |op| op
.addresses_tag()
.summary("Address confirmed transactions")
@@ -148,11 +124,7 @@ impl AddressRoutes for ApiRouter<AppState> {
Path(address): Path<String>,
State(state): State<AppState>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(validate_address(&address), etag)
state.cached_json(&headers, CacheStrategy::Static, move |_q| Ok(AddressValidation::from_address(&address))).await
}, |op| op
.addresses_tag()
.summary("Validate address")

View File

@@ -2,7 +2,7 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_query::BLOCK_TXS_PAGE_SIZE;
@@ -11,10 +11,7 @@ use brk_types::{
BlockTimestamp, Height, HeightPath, StartHeightPath, TimestampPath, Transaction, Txid,
};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -35,11 +32,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block(path.hash).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block(&path.hash)).await
},
|op| {
op.blocks_tag()
@@ -61,14 +54,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_status(path.hash)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_status(&path.hash)).await
},
|op| {
op.blocks_tag()
@@ -90,14 +76,8 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<HeightPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_by_height(Height::from(path.height))
.await
.to_json_response(&etag)
let height = Height::from(path.height);
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_height(height)).await
},
|op| {
op.blocks_tag()
@@ -119,12 +99,8 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<StartHeightPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
let start_height = path.start_height.map(Height::from);
state.get_blocks(start_height).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(start_height)).await
},
|op| {
op.blocks_tag()
@@ -145,11 +121,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_txids(path.hash).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txids(&path.hash)).await
},
|op| {
op.blocks_tag()
@@ -171,11 +143,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashStartIndexPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_txs(path.hash, path.start_index).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txs(&path.hash, path.start_index)).await
},
|op| {
op.blocks_tag()
@@ -198,11 +166,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashTxIndexPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_txid_at_index(path.hash, path.index).await.to_display_response(&etag)
state.cached_text(&headers, CacheStrategy::Height, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
},
|op| {
op.blocks_tag()
@@ -224,14 +188,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<TimestampPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_by_timestamp(path.timestamp)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_timestamp(path.timestamp)).await
},
|op| {
op.blocks_tag()
@@ -251,11 +208,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashPath>,
State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_block_raw(path.hash).await.to_bytes_response(&etag)
state.cached_bytes(&headers, CacheStrategy::Height, move |q| q.block_raw(&path.hash)).await
},
|op| {
op.blocks_tag()

View File

@@ -2,15 +2,12 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::State,
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -26,18 +23,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/mempool/info",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_mempool_info().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_info()).await
},
|op| {
op.mempool_tag()
.summary("Mempool statistics")
.description("Get current mempool statistics including transaction count, total vsize, and total fees.")
.ok_response::<MempoolInfo>()
.not_modified()
.server_error()
},
),
@@ -46,18 +38,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/mempool/txids",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_mempool_txids().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_txids()).await
},
|op| {
op.mempool_tag()
.summary("Mempool transaction IDs")
.description("Get all transaction IDs currently in the mempool.")
.ok_response::<Vec<Txid>>()
.not_modified()
.server_error()
},
),
@@ -66,18 +53,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/v1/fees/recommended",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_recommended_fees().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(3), |q| q.recommended_fees()).await
},
|op| {
op.mempool_tag()
.summary("Recommended fees")
.description("Get recommended fee rates for different confirmation targets based on current mempool state.")
.ok_response::<RecommendedFees>()
.not_modified()
.server_error()
},
),
@@ -86,18 +68,13 @@ impl MempoolRoutes for ApiRouter<AppState> {
"/api/v1/fees/mempool-blocks",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_mempool_blocks().await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_blocks()).await
},
|op| {
op.mempool_tag()
.summary("Projected mempool blocks")
.description("Get projected blocks from the mempool for fee estimation. Each block contains statistics about transactions that would be included if a block were mined now.")
.ok_response::<Vec<MempoolBlock>>()
.not_modified()
.server_error()
},
),

View File

@@ -1,31 +1,29 @@
use std::time::Duration;
use axum::{
Json,
body::Body,
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::{Error, Result};
use brk_query::{Output, Params};
use brk_query::{MetricSelection, Output};
use brk_types::Format;
use quick_cache::sync::GuardResult;
use vecdb::Stamp;
use crate::{HeaderMapExtended, ResponseExtended};
use crate::{CacheStrategy, cache::CacheParams, extended::HeaderMapExtended};
use super::AppState;
/// Maximum allowed request weight in bytes (650KB)
const MAX_WEIGHT: usize = 65 * 10_000;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<Params>,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state) {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => {
let mut response =
@@ -36,91 +34,64 @@ pub async fn handler(
}
}
fn req_to_response_res(
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<Params>,
AppState {
query: interface,
cache,
..
}: AppState,
) -> Result<Response> {
todo!();
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
) -> brk_error::Result<Response> {
let format = params.format();
let height = query.sync(|q| q.height());
let to = params.to();
// let vecs = interface.search(&params)?;
let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || height.into());
// if vecs.is_empty() {
// return Ok(Json(vec![] as Vec<usize>).into_response());
// }
if cache_params.matches_etag(&headers) {
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
response.headers_mut().insert_cors();
return Ok(response);
}
// let from = params.from();
// let to = params.to();
// let format = params.format();
let cache_key = format!("{}{}{}", uri.path(), uri.query().unwrap_or(""), cache_params.etag_str());
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
// // TODO: From and to should be capped here
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
match query
.run(move |q| q.search_and_format_checked(params, MAX_WEIGHT))
.await?
{
Output::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
}
s.into_response()
}
Output::Json(v) => {
let json = v.to_vec();
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(json.clone().into());
}
json.into_response()
}
}
};
// let weight = vecs
// .iter()
// .map(|(_, v)| v.range_weight(from, to))
// .sum::<usize>();
let headers = response.headers_mut();
headers.insert_cors();
if let Some(etag) = &cache_params.etag {
headers.insert_etag(etag);
}
headers.insert_cache_control(&cache_params.cache_control);
// if weight > MAX_WEIGHT {
// return Err(Error::String(format!(
// "Request is too heavy, max weight is {MAX_WEIGHT} bytes"
// )));
// }
match format {
Format::CSV => {
headers.insert_content_disposition_attachment();
headers.insert_content_type_text_csv()
}
Format::JSON => headers.insert_content_type_application_json(),
}
// // TODO: height should be from vec, but good enough for now
// let etag = vecs
// .first()
// .unwrap()
// .1
// .etag(Stamp::from(interface.get_height()), to);
// if headers.has_etag(etag) {
// return Ok(Response::new_not_modified());
// }
// let guard_res = cache.get_value_or_guard(
// &format!("{}{}{etag}", uri.path(), uri.query().unwrap_or("")),
// Some(Duration::from_millis(50)),
// );
// let mut response = if let GuardResult::Value(v) = guard_res {
// Response::new(Body::from(v))
// } else {
// match interface.format(vecs, &params.rest)? {
// Output::CSV(s) => {
// if let GuardResult::Guard(g) = guard_res {
// let _ = g.insert(s.clone().into());
// }
// s.into_response()
// }
// Output::Json(v) => {
// let json = v.to_vec();
// if let GuardResult::Guard(g) = guard_res {
// let _ = g.insert(json.clone().into());
// }
// json.into_response()
// }
// }
// };
// let headers = response.headers_mut();
// headers.insert_cors();
// headers.insert_etag(&etag);
// headers.insert_cache_control_must_revalidate();
// match format {
// Format::CSV => {
// headers.insert_content_disposition_attachment();
// headers.insert_content_type_text_csv()
// }
// Format::JSON => headers.insert_content_type_application_json(),
// }
// Ok(response)
Ok(response)
}

View File

@@ -1,18 +1,15 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, Query, State},
http::{HeaderMap, StatusCode, Uri},
http::{HeaderMap, Uri},
response::{IntoResponse, Redirect, Response},
routing::get,
};
use brk_query::{PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt};
use brk_query::{DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, Pagination};
use brk_traversable::TreeNode;
use brk_types::{Index, IndexInfo, Limit, Metric, MetricCount, Metrics};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -34,11 +31,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
State(state): State<AppState>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(state.metric_count().await, etag)
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metric_count())).await
},
|op| op
.metrics_tag()
@@ -55,11 +48,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
State(state): State<AppState>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json( state.get_indexes().await, etag)
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.get_indexes().to_vec())).await
},
|op| op
.metrics_tag()
@@ -77,13 +66,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |
headers: HeaderMap,
State(state): State<AppState>,
Query(pagination): Query<PaginationParam>
Query(pagination): Query<Pagination>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(state.get_metrics(pagination).await, etag)
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.get_metrics(pagination))).await
},
|op| op
.metrics_tag()
@@ -96,12 +81,8 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.api_route(
"/api/metrics/catalog",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| -> Response {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(state.get_metrics_catalog().await, etag)
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.get_metrics_catalog().clone())).await
},
|op| op
.metrics_tag()
@@ -122,11 +103,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
Path(metric): Path<Metric>,
Query(limit): Query<Limit>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
state.match_metric(metric, limit).await.to_json_response(etag)
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.match_metric(&metric, limit))).await
},
|op| op
.metrics_tag()
@@ -144,20 +121,18 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(metric): Path<Metric>
| {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
if let Some(indexes) = state.metric_to_indexes(metric.clone()).await {
return Response::new_json(indexes, etag)
}
// REMOVE UNWRAP !!
let value = if let Some(first) = state.match_metric(metric.clone(), Limit::MIN).await.unwrap().first() {
format!("Could not find '{metric}', did you mean '{first}' ?")
} else {
format!("Could not find '{metric}'.")
};
Response::new_json_with(StatusCode::NOT_FOUND, value, etag)
state.cached_json(&headers, CacheStrategy::Static, move |q| {
if let Some(indexes) = q.metric_to_indexes(metric.clone()) {
return Ok(indexes.clone())
}
Err(brk_error::Error::String(
if let Some(first) = q.match_metric(&metric, Limit::MIN).first() {
format!("Could not find '{metric}', did you mean '{first}' ?")
} else {
format!("Could not find '{metric}'.")
}
))
}).await
},
|op| op
.metrics_tag()
@@ -173,7 +148,6 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
)
// WIP
.route("/api/metrics/bulk", get(data::handler))
// WIP
.route(
"/api/metric/{metric}/{index}",
get(
@@ -181,16 +155,15 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
state: State<AppState>,
Path((metric, index)): Path<(Metric, Index)>,
Query(params_opt): Query<ParamsOpt>|
Query(range): Query<DataRangeFormat>|
-> Response {
todo!();
// data::handler(
// uri,
// headers,
// Query(Params::from(((index, metric), params_opt))),
// state,
// )
// .await
data::handler(
uri,
headers,
Query(MetricSelection::from((index, metric, range))),
state,
)
.await
},
),
)
@@ -202,7 +175,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
get(
async |uri: Uri,
headers: HeaderMap,
Query(params): Query<ParamsDeprec>,
Query(params): Query<MetricSelectionLegacy>,
state: State<AppState>|
-> Response {
data::handler(uri, headers, Query(params.into()), state).await
@@ -218,7 +191,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |uri: Uri,
headers: HeaderMap,
Path(variant): Path<String>,
Query(params_opt): Query<ParamsOpt>,
Query(range): Query<DataRangeFormat>,
state: State<AppState>|
-> Response {
let separator = "_to_";
@@ -230,10 +203,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
return format!("Index {ser_index} doesn't exist").into_response();
};
let params = Params::from((
let params = MetricSelection::from((
index,
Metrics::from(split.collect::<Vec<_>>().join(separator)),
params_opt,
range,
));
data::handler(uri, headers, Query(params), state).await
},

View File

@@ -2,7 +2,7 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{
@@ -11,10 +11,7 @@ use brk_types::{
PoolSlugPath, PoolsSummary, RewardStats, TimePeriodPath,
};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -32,14 +29,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/difficulty-adjustment",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_difficulty_adjustment()
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustment()).await
},
|op| {
op.mining_tag()
@@ -55,11 +45,8 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pools",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-pools");
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_all_pools().await.to_json_response(&etag)
// Pool list is static, only changes on code update
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.all_pools())).await
},
|op| {
op.mining_tag()
@@ -72,17 +59,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/pools/:time_period",
"/api/v1/mining/pools/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_mining_pools(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("{:?}", path.time_period)), move |q| q.mining_pools(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -95,17 +75,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/pool/:slug",
"/api/v1/mining/pool/{slug}",
get_with(
async |headers: HeaderMap, Path(path): Path<PoolSlugPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-{}-{:?}", state.get_height().await, path.slug);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_pool_detail(path.slug)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(path.slug), move |q| q.pool_detail(path.slug)).await
},
|op| {
op.mining_tag()
@@ -122,14 +95,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-hashrate-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_hashrate(None)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with("hashrate"), |q| q.hashrate(None)).await
},
|op| {
op.mining_tag()
@@ -142,17 +108,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/hashrate/:time_period",
"/api/v1/mining/hashrate/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-hashrate-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_hashrate(Some(path.time_period))
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("hashrate-{:?}", path.time_period)), move |q| q.hashrate(Some(path.time_period))).await
},
|op| {
op.mining_tag()
@@ -168,14 +127,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/difficulty-adjustments",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
let etag = format!("{VERSION}-diff-adj-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_difficulty_adjustments(None)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with("diff-adj"), |q| q.difficulty_adjustments(None)).await
},
|op| {
op.mining_tag()
@@ -188,17 +140,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/difficulty-adjustments/:time_period",
"/api/v1/mining/difficulty-adjustments/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-diff-adj-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_difficulty_adjustments(Some(path.time_period))
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("diff-adj-{:?}", path.time_period)), move |q| q.difficulty_adjustments(Some(path.time_period))).await
},
|op| {
op.mining_tag()
@@ -211,17 +156,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/fees/:time_period",
"/api/v1/mining/blocks/fees/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-fees-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_fees(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("fees-{:?}", path.time_period)), move |q| q.block_fees(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -234,17 +172,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/rewards/:time_period",
"/api/v1/mining/blocks/rewards/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-rewards-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_rewards(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("rewards-{:?}", path.time_period)), move |q| q.block_rewards(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -257,17 +188,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/fee-rates/:time_period",
"/api/v1/mining/blocks/fee-rates/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-feerates-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_fee_rates(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("feerates-{:?}", path.time_period)), move |q| q.block_fee_rates(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -280,17 +204,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/blocks/sizes-weights/:time_period",
"/api/v1/mining/blocks/sizes-weights/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-sizes-{}-{:?}", state.get_height().await, path.time_period);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_block_sizes_weights(path.time_period)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("sizes-{:?}", path.time_period)), move |q| q.block_sizes_weights(path.time_period)).await
},
|op| {
op.mining_tag()
@@ -303,17 +220,10 @@ impl MiningRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/v1/mining/reward-stats/:block_count",
"/api/v1/mining/reward-stats/{block_count}",
get_with(
async |headers: HeaderMap, Path(path): Path<BlockCountPath>, State(state): State<AppState>| {
let etag = format!("{VERSION}-reward-stats-{}-{}", state.get_height().await, path.block_count);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state
.get_reward_stats(path.block_count)
.await
.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::height_with(format!("reward-stats-{}", path.block_count)), move |q| q.reward_stats(path.block_count)).await
},
|op| {
op.mining_tag()

View File

@@ -6,6 +6,7 @@ use aide::{
};
use axum::{
Extension, Json,
extract::State,
http::HeaderMap,
response::{Html, Redirect, Response},
routing::get,
@@ -13,7 +14,7 @@ use axum::{
use brk_types::Health;
use crate::{
VERSION,
CacheStrategy, VERSION,
api::{
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, transactions::TxRoutes,
@@ -49,12 +50,15 @@ impl ApiRoutes for ApiRouter<AppState> {
.api_route(
"/version",
get_with(
async || -> Json<&'static str> { Json(VERSION) },
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, |_| Ok(env!("CARGO_PKG_VERSION"))).await
},
|op| {
op.server_tag()
.summary("API version")
.description("Returns the current version of the API server")
.ok_response::<String>()
.not_modified()
},
),
)

View File

@@ -29,7 +29,8 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Addresses".to_string(),
description: Some(
"Explore Bitcoin addresses."
"Query Bitcoin address data including balances, transaction history, and UTXOs. \
Supports all address types: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR."
.to_string()
),
..Default::default()
@@ -37,7 +38,17 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Blocks".to_string(),
description: Some(
"Explore Bitcoin blocks."
"Retrieve block data by hash or height. Access block headers, transaction lists, \
and raw block bytes."
.to_string()
),
..Default::default()
},
Tag {
name: "Mempool".to_string(),
description: Some(
"Monitor unconfirmed transactions and fee estimates. Get mempool statistics, \
transaction IDs, and recommended fee rates for different confirmation targets."
.to_string()
),
..Default::default()
@@ -45,8 +56,8 @@ pub fn create_openapi() -> OpenApi {
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."
"Access Bitcoin network metrics and time-series data. Query historical statistics \
across various indexes (date, week, month, year, halving epoch) with JSON or CSV output."
.to_string()
),
..Default::default()
@@ -54,7 +65,8 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Mining".to_string(),
description: Some(
"Explore mining related endpoints."
"Mining statistics including pool distribution, hashrate, difficulty adjustments, \
block rewards, and fee rates across configurable time periods."
.to_string()
),
..Default::default()
@@ -62,15 +74,16 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Server".to_string(),
description: Some(
"Metadata and utility endpoints for API status, health checks, and system information."
"API metadata and health monitoring. Version information and service status."
.to_string()
),
..Default::default()
..Default::default()
},
Tag {
name: "Transactions".to_string(),
description: Some(
"Explore Bitcoin transactions."
"Retrieve transaction data by txid. Access full transaction details, confirmation \
status, raw hex, and output spend information."
.to_string()
),
..Default::default()

View File

@@ -2,15 +2,12 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::{Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{Transaction, TxOutspend, TxStatus, TxidPath, TxidVoutPath};
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
@@ -31,11 +28,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_transaction(txid).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction(txid)).await
},
|op| op
.transactions_tag()
@@ -58,11 +51,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_transaction_status(txid).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction_status(txid)).await
},
|op| op
.transactions_tag()
@@ -85,11 +74,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_transaction_hex(txid).await.to_text_response(&etag)
state.cached_text(&headers, CacheStrategy::Height, move |q| q.transaction_hex(txid)).await
},
|op| op
.transactions_tag()
@@ -112,12 +97,8 @@ impl TxRoutes for ApiRouter<AppState> {
Path(path): Path<TxidVoutPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
let txid = TxidPath { txid: path.txid };
state.get_tx_outspend(txid, path.vout).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspend(txid, path.vout)).await
},
|op| op
.transactions_tag()
@@ -140,11 +121,7 @@ impl TxRoutes for ApiRouter<AppState> {
Path(txid): Path<TxidPath>,
State(state): State<AppState>
| {
let etag = format!("{VERSION}-{}", state.get_height().await);
if headers.has_etag(&etag) {
return Response::new_not_modified();
}
state.get_tx_outspends(txid).await.to_json_response(&etag)
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspends(txid)).await
},
|op| op
.transactions_tag()