crates: snapshot

This commit is contained in:
nym21
2026-05-12 22:33:09 +02:00
parent 8fc2e71492
commit 5cc3fbfa6e
25 changed files with 450 additions and 362 deletions
+29 -10
View File
@@ -11,6 +11,14 @@ use crate::{
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;
}
@@ -26,7 +34,7 @@ impl AddrRoutes for ApiRouter<AppState> {
_: Empty,
State(state): State<AppState>
| {
let strategy = state.addr_strategy(Version::ONE, &path.addr, false);
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")
@@ -49,13 +57,24 @@ impl AddrRoutes for ApiRouter<AppState> {
_: Empty,
State(state): State<AppState>
| {
let strategy = state.addr_strategy(Version::ONE, &path.addr, false);
state.respond_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, 50, 50)).await
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, sorted with newest first. Returns up to 50 entries: mempool transactions first, then confirmed transactions filling the remainder. To paginate further confirmed transactions, use `/address/{address}/txs/chain/{last_seen_txid}`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-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::<Vec<Transaction>>()
.not_modified()
.bad_request()
@@ -72,8 +91,8 @@ impl AddrRoutes for ApiRouter<AppState> {
_: Empty,
State(state): State<AppState>
| {
let strategy = state.addr_strategy(Version::ONE, &path.addr, true);
state.respond_json(&headers, strategy, &uri, move |q| q.addr_txs_chain(&path.addr, None, 25)).await
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()
@@ -95,8 +114,8 @@ impl AddrRoutes for ApiRouter<AppState> {
_: Empty,
State(state): State<AppState>
| {
let strategy = state.addr_strategy(Version::ONE, &path.addr, true);
state.respond_json(&headers, strategy, &uri, move |q| q.addr_txs_chain(&path.addr, Some(path.after_txid), 25)).await
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()
@@ -119,7 +138,7 @@ impl AddrRoutes for ApiRouter<AppState> {
State(state): State<AppState>
| {
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, 50)).await
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()
@@ -141,7 +160,7 @@ impl AddrRoutes for ApiRouter<AppState> {
_: Empty,
State(state): State<AppState>
| {
let strategy = state.addr_strategy(Version::ONE, &path.addr, false);
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
+2 -1
View File
@@ -135,7 +135,8 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pool/{slug}/blocks",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<PoolSlugParam>, _: Empty, State(state): State<AppState>| {
state.respond_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_blocks(path.slug, None, POOL_BLOCKS_LIMIT)).await
let strategy = state.pool_blocks_strategy(Version::ONE, path.slug);
state.respond_json(&headers, strategy, &uri, move |q| q.pool_blocks(path.slug, None, POOL_BLOCKS_LIMIT)).await
},
|op| {
op.id("get_pool_blocks")
+2 -8
View File
@@ -29,13 +29,7 @@ impl ServerRoutes for ApiRouter<AppState> {
let uptime = state.started_instant.elapsed();
let started_at = state.started_at.to_string();
let sync = state
.run(move |q| {
let tip_height = q
.client()
.get_last_height()
.unwrap_or(q.height());
q.sync_status(tip_height)
})
.run(move |q| q.sync_status(q.height()))
.await
.expect("health sync task panicked");
let mut response = axum::Json(Health {
@@ -57,7 +51,7 @@ impl ServerRoutes for ApiRouter<AppState> {
op.id("get_health")
.server_tag()
.summary("Health check")
.description("Returns the health status of the API server, including uptime information.")
.description("Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`.")
.json_response::<Health>()
},
),
+42 -9
View File
@@ -6,13 +6,13 @@ use axum::{
};
use brk_query::AsyncQuery;
use brk_types::{
Addr, BlockHash, BlockHashPrefix, Date, Height, ONE_HOUR_IN_SEC, Timestamp as BrkTimestamp,
Txid, Version,
Addr, BlockHash, BlockHashPrefix, Date, Height, ONE_HOUR_IN_SEC, PoolSlug,
Timestamp as BrkTimestamp, Txid, Version,
};
use derive_more::Deref;
use jiff::Timestamp;
use serde::Serialize;
use vecdb::ReadableVec;
use vecdb::{ReadableVec, VecIndex};
use crate::{CacheParams, CacheStrategy, Error, Website, extended::ResponseExtended};
@@ -70,16 +70,26 @@ impl AppState {
})
}
/// Smart address caching: checks mempool activity first (unless `chain_only`), then on-chain.
/// - Address has mempool txs `MempoolHash(addr_specific_hash)`
/// - No mempool, has on-chain activity `BlockBound(last_activity_block)`
/// - Unknown address `Tip`
pub fn addr_strategy(&self, version: Version, addr: &Addr, chain_only: bool) -> CacheStrategy {
/// Smart address caching. Checks mempool activity first (unless `chain_only`), then on-chain.
/// - Address has mempool txs: `MempoolHash(addr_specific_hash)`
/// - No mempool, has on-chain activity: `BlockBound(last_activity_block)`
/// - Unknown address: `Tip`
///
/// `before_txid` narrows the on-chain branch to the newest activity strictly
/// older than the cursor, so paginated chain pages stay cacheable when newer
/// activity arrives above the cursor.
pub fn addr_strategy(
&self,
version: Version,
addr: &Addr,
chain_only: bool,
before_txid: Option<&Txid>,
) -> CacheStrategy {
self.sync(|q| {
if !chain_only && let Some(mempool_hash) = q.addr_mempool_hash(addr) {
return CacheStrategy::MempoolHash(mempool_hash);
}
q.addr_last_activity_height(addr)
q.addr_last_activity_height(addr, before_txid)
.and_then(|h| {
let block_hash = q.block_hash_by_height(h)?;
Ok(CacheStrategy::BlockBound(
@@ -135,6 +145,29 @@ impl AppState {
})
}
/// `BlockBound` on the pool's last-mined block hash, `Tip` if the pool has
/// never mined. Lets the no-cursor pool-blocks page stay cached when *other*
/// pools mine; only invalidates when this pool itself mines.
pub fn pool_blocks_strategy(&self, version: Version, slug: PoolSlug) -> CacheStrategy {
self.sync(|q| {
let tip = q.height().to_usize();
let last = q
.computer()
.pools
.pool_heights
.read()
.get(&slug)
.and_then(|heights| {
let pos = heights.partition_point(|h| h.to_usize() <= tip);
pos.checked_sub(1).map(|i| heights[i])
});
match last.and_then(|h| q.block_hash_by_height(h).ok()) {
Some(hash) => CacheStrategy::BlockBound(version, BlockHashPrefix::from(&hash)),
None => CacheStrategy::Tip,
}
})
}
pub fn mempool_strategy(&self) -> CacheStrategy {
let hash = self.sync(|q| q.mempool().map(|m| m.next_block_hash().into()).unwrap_or(0));
CacheStrategy::MempoolHash(hash)