mempool: snapshot 5 + query: new tools + server: endpoints

This commit is contained in:
nym21
2025-12-14 02:06:14 +01:00
parent db5d784ff7
commit b491b1f41f
79 changed files with 1588 additions and 129 deletions

View File

@@ -5,7 +5,8 @@ use axum::{
response::{Redirect, Response},
routing::get,
};
use brk_types::{Address, AddressStats, AddressTxidsParam, Txid, Utxo};
use brk_query::validate_address;
use brk_types::{Address, AddressStats, AddressTxidsParam, AddressValidation, Txid, Utxo};
use crate::{
VERSION,
@@ -93,5 +94,71 @@ impl AddressRoutes for ApiRouter<AppState> {
.server_error()
),
)
.api_route(
"/api/address/{address}/txs/mempool",
get_with(async |
headers: HeaderMap,
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)
}, |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()
),
)
.api_route(
"/api/address/{address}/txs/chain/{after_txid}",
get_with(async |
headers: HeaderMap,
Path((address, after_txid)): Path<(Address, Option<Txid>)>,
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, after_txid, 25).await.to_json_response(&etag)
}, |op| op
.addresses_tag()
.summary("Address confirmed transactions")
.description("Get confirmed transaction IDs for an address, 25 per page. Use after_txid for pagination.")
.ok_response::<Vec<Txid>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
),
)
.api_route(
"/api/v1/validate-address/{address}",
get_with(async |
headers: HeaderMap,
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)
}, |op| op
.addresses_tag()
.summary("Validate address")
.description("Validate a Bitcoin address and get information about its type and scriptPubKey.")
.ok_response::<AddressValidation>()
.not_modified()
),
)
}
}

View File

@@ -5,7 +5,11 @@ use axum::{
response::{Redirect, Response},
routing::get,
};
use brk_types::{BlockHashPath, BlockInfo, BlockStatus, Height, HeightPath, StartHeightPath, Txid};
use brk_query::BLOCK_TXS_PAGE_SIZE;
use brk_types::{
BlockHashPath, BlockHashStartIndexPath, BlockHashTxIndexPath, BlockInfo, BlockStatus,
BlockTimestamp, Height, HeightPath, StartHeightPath, TimestampPath, Transaction, Txid,
};
use crate::{
VERSION,
@@ -161,5 +165,111 @@ impl BlockRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/block/{hash}/txs/{start_index}",
get_with(
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)
},
|op| {
op.blocks_tag()
.summary("Block transactions (paginated)")
.description(&format!(
"Retrieve transactions in a block by block hash, starting from the specified index. Returns up to {} transactions at a time.",
BLOCK_TXS_PAGE_SIZE
))
.ok_response::<Vec<Transaction>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/block/{hash}/txid/{index}",
get_with(
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)
},
|op| {
op.blocks_tag()
.summary("Transaction ID at index")
.description(
"Retrieve a single transaction ID at a specific index within a block. Returns plain text txid.",
)
.ok_response::<Txid>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/v1/mining/blocks/timestamp/{timestamp}",
get_with(
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)
},
|op| {
op.blocks_tag()
.summary("Block by timestamp")
.description("Find the block closest to a given UNIX timestamp.")
.ok_response::<BlockTimestamp>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/block/{hash}/raw",
get_with(
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)
},
|op| {
op.blocks_tag()
.summary("Raw block")
.description(
"Returns the raw block data in binary format.",
)
.ok_response::<Vec<u8>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
}
}

View File

@@ -5,7 +5,7 @@ use axum::{
response::{Redirect, Response},
routing::get,
};
use brk_types::{MempoolInfo, RecommendedFees, Txid};
use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid};
use crate::{
VERSION,
@@ -82,5 +82,25 @@ impl MempoolRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/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)
},
|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

@@ -0,0 +1,51 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::State,
http::HeaderMap,
response::{Redirect, Response},
routing::get,
};
use brk_types::DifficultyAdjustment;
use crate::{
VERSION,
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
};
use super::AppState;
pub trait MiningRoutes {
fn add_mining_routes(self) -> Self;
}
impl MiningRoutes for ApiRouter<AppState> {
fn add_mining_routes(self) -> Self {
self.route(
"/api/v1/mining",
get(Redirect::temporary("/api#tag/mining")),
)
.api_route(
"/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)
},
|op| {
op.mining_tag()
.summary("Difficulty adjustment")
.description("Get current difficulty adjustment information including progress through the current epoch, estimated retarget date, and difficulty change prediction.")
.ok_response::<DifficultyAdjustment>()
.not_modified()
.server_error()
},
),
)
}
}

View File

@@ -16,7 +16,7 @@ use crate::{
VERSION,
api::{
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
metrics::ApiMetricsRoutes, transactions::TxRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, transactions::TxRoutes,
},
extended::{HeaderMapExtended, ResponseExtended, TransformResponseExtended},
};
@@ -27,6 +27,7 @@ mod addresses;
mod blocks;
mod mempool;
mod metrics;
mod mining;
mod openapi;
mod transactions;
@@ -41,6 +42,7 @@ impl ApiRoutes for ApiRouter<AppState> {
self.add_addresses_routes()
.add_block_routes()
.add_mempool_routes()
.add_mining_routes()
.add_tx_routes()
.add_metrics_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))

View File

@@ -5,7 +5,7 @@ use axum::{
response::{Redirect, Response},
routing::get,
};
use brk_types::{Transaction, TxStatus, TxidPath};
use brk_types::{Transaction, TxOutspend, TxStatus, TxidPath, TxidVoutPath};
use crate::{
VERSION,
@@ -104,5 +104,60 @@ impl TxRoutes for ApiRouter<AppState> {
.server_error(),
),
)
.api_route(
"/api/tx/{txid}/outspend/{vout}",
get_with(
async |
headers: HeaderMap,
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)
},
|op| op
.transactions_tag()
.summary("Output spend status")
.description(
"Get the spending status of a transaction output. Returns whether the output has been spent and, if so, the spending transaction details.",
)
.ok_response::<TxOutspend>()
.not_modified()
.bad_request()
.not_found()
.server_error(),
),
)
.api_route(
"/api/tx/{txid}/outspends",
get_with(
async |
headers: HeaderMap,
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)
},
|op| op
.transactions_tag()
.summary("All output spend statuses")
.description(
"Get the spending status of all outputs in a transaction. Returns an array with the spend status for each output.",
)
.ok_response::<Vec<TxOutspend>>()
.not_modified()
.bad_request()
.not_found()
.server_error(),
),
)
}
}