use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, State}, http::{HeaderMap, Uri}, }; use brk_types::{AddrStats, AddrValidation, Transaction, Utxo, Version}; use crate::{ AppState, CacheStrategy, extended::TransformResponseExtended, params::{AddrAfterTxidParam, AddrParam, Empty, ValidateAddrParam}, }; /// Esplora `/txs` and `/txs/chain` page sizes. Wire-protocol constants from /// mempool.space/esplora, not deployment policy. `/txs` returns up to /// `MEMPOOL_PAGE` mempool entries plus a chain page sized to reach /// `TXS_TOTAL_TARGET` total, floored at `CHAIN_PAGE`. const MEMPOOL_PAGE: usize = 50; const CHAIN_PAGE: usize = 25; const TXS_TOTAL_TARGET: usize = 50; pub trait AddrRoutes { fn add_addr_routes(self) -> Self; } impl AddrRoutes for ApiRouter { fn add_addr_routes(self) -> Self { self.api_route( "/api/address/{address}", get_with(async | uri: Uri, headers: HeaderMap, Path(path): Path, _: Empty, State(state): State | { let strategy = state.addr_strategy(Version::ONE, &path.addr, false, None); state.respond_json(&headers, strategy, &uri, move |q| q.addr(path.addr)).await }, |op| op .id("get_address") .addrs_tag() .summary("Address information") .description("Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address)*") .json_response::() .not_modified() .bad_request() .not_found() .server_error() ), ) .api_route( "/api/address/{address}/txs", get_with(async | uri: Uri, headers: HeaderMap, Path(path): Path, _: Empty, State(state): State | { let strategy = state.addr_strategy(Version::ONE, &path.addr, false, None); state.respond_json(&headers, strategy, &uri, move |q| { let mempool_txs = if q.mempool().is_some() { q.addr_mempool_txs(&path.addr, MEMPOOL_PAGE)? } else { Vec::new() }; let chain_limit = TXS_TOTAL_TARGET.saturating_sub(mempool_txs.len()).max(CHAIN_PAGE); let chain_txs = q.addr_txs_chain(&path.addr, None, chain_limit)?; let mut out = mempool_txs; out.extend(chain_txs); Ok(out) }).await }, |op| op .id("get_address_txs") .addrs_tag() .summary("Address transactions") .description("Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, use `/address/{address}/txs/chain/{last_seen_txid}`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*") .json_response::>() .not_modified() .bad_request() .not_found() .server_error() ), ) .api_route( "/api/address/{address}/txs/chain", get_with(async | uri: Uri, headers: HeaderMap, Path(path): Path, _: Empty, State(state): State | { let strategy = state.addr_strategy(Version::ONE, &path.addr, true, None); state.respond_json(&headers, strategy, &uri, move |q| q.addr_txs_chain(&path.addr, None, CHAIN_PAGE)).await }, |op| op .id("get_address_confirmed_txs") .addrs_tag() .summary("Address confirmed transactions") .description("Get the first 25 confirmed transactions for an address. For pagination, use the path-style form `/txs/chain/{last_seen_txid}`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*") .json_response::>() .not_modified() .bad_request() .not_found() .server_error() ), ) .api_route( "/api/address/{address}/txs/chain/{after_txid}", get_with(async | uri: Uri, headers: HeaderMap, Path(path): Path, _: Empty, State(state): State | { let strategy = state.addr_strategy(Version::ONE, &path.addr, true, Some(&path.after_txid)); state.respond_json(&headers, strategy, &uri, move |q| q.addr_txs_chain(&path.addr, Some(path.after_txid), CHAIN_PAGE)).await }, |op| op .id("get_address_confirmed_txs_after") .addrs_tag() .summary("Address confirmed transactions (paginated)") .description("Get the next 25 confirmed transactions strictly older than `after_txid` (Esplora-canonical pagination form, matches mempool.space).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*") .json_response::>() .not_modified() .bad_request() .not_found() .server_error() ), ) .api_route( "/api/address/{address}/txs/mempool", get_with(async | uri: Uri, headers: HeaderMap, Path(path): Path, _: Empty, State(state): State | { let hash = state.sync(|q| q.addr_mempool_hash(&path.addr)).unwrap_or(0); state.respond_json(&headers, CacheStrategy::MempoolHash(hash), &uri, move |q| q.addr_mempool_txs(&path.addr, MEMPOOL_PAGE)).await }, |op| op .id("get_address_mempool_txs") .addrs_tag() .summary("Address mempool transactions") .description("Get unconfirmed transactions for an address from the mempool, newest first (up to 50).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-mempool)*") .json_response::>() .not_modified() .bad_request() .not_found() .server_error() ), ) .api_route( "/api/address/{address}/utxo", get_with(async | uri: Uri, headers: HeaderMap, Path(path): Path, _: Empty, State(state): State | { let strategy = state.addr_strategy(Version::ONE, &path.addr, false, None); let max_utxos = state.max_utxos; state.respond_json(&headers, strategy, &uri, move |q| q.addr_utxos(path.addr, max_utxos)).await }, |op| op .id("get_address_utxos") .addrs_tag() .summary("Address UTXOs") .description("Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*") .json_response::>() .not_modified() .bad_request() .not_found() .server_error() ), ) .api_route( "/api/v1/validate-address/{address}", get_with(async | uri: Uri, headers: HeaderMap, Path(path): Path, _: Empty, State(state): State | { state.respond_json(&headers, CacheStrategy::Deploy, &uri, move |_q| Ok(AddrValidation::from_addr(&path.addr))).await }, |op| op .id("validate_address") .addrs_tag() .summary("Validate address") .description("Validate a Bitcoin address and get information about its type and scriptPubKey. Returns `isvalid: false` with an error message for invalid addresses.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)*") .json_response::() .not_modified() .server_error() ), ) } }