mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
server: cache fixes
This commit is contained in:
@@ -30,7 +30,7 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Path(path): Path<AddrParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr);
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, false);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr(path.addr)).await
|
||||
}, |op| op
|
||||
.id("get_address")
|
||||
@@ -53,7 +53,7 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Query(params): Query<AddrTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr);
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, false);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 50)).await
|
||||
}, |op| op
|
||||
.id("get_address_txs")
|
||||
@@ -76,7 +76,7 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Query(params): Query<AddrTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr);
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, true);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 25)).await
|
||||
}, |op| op
|
||||
.id("get_address_confirmed_txs")
|
||||
@@ -119,7 +119,7 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Path(path): Path<AddrParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr);
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, false);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr_utxos(path.addr)).await
|
||||
}, |op| op
|
||||
.id("get_address_utxos")
|
||||
|
||||
@@ -117,7 +117,7 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<TimestampParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_by_timestamp(path.timestamp)).await
|
||||
state.cached_json(&headers, state.timestamp_cache(Version::ONE, path.timestamp), &uri, move |q| q.block_by_timestamp(path.timestamp)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_by_timestamp")
|
||||
|
||||
@@ -3,7 +3,7 @@ use axum::{
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, Uri},
|
||||
};
|
||||
use brk_types::{DifficultyAdjustment, HistoricalPrice, Prices, Timestamp};
|
||||
use brk_types::{DifficultyAdjustment, HistoricalPrice, Prices, Timestamp, Version};
|
||||
|
||||
use crate::{
|
||||
AppState, CacheStrategy, extended::TransformResponseExtended, params::OptionalTimestampParam,
|
||||
@@ -66,8 +66,12 @@ impl GeneralRoutes for ApiRouter<AppState> {
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<OptionalTimestampParam>,
|
||||
State(state): State<AppState>| {
|
||||
let strategy = params
|
||||
.timestamp
|
||||
.map(|ts| state.timestamp_cache(Version::ONE, ts))
|
||||
.unwrap_or(CacheStrategy::Tip);
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
|
||||
.cached_json(&headers, strategy, &uri, move |q| {
|
||||
q.historical_price(params.timestamp)
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
use brk_types::{
|
||||
BlockFeeRatesEntry, BlockFeesEntry, BlockInfoV1, BlockRewardsEntry, BlockSizesWeights,
|
||||
DifficultyAdjustmentEntry, HashrateSummary, PoolDetail, PoolHashrateEntry, PoolInfo,
|
||||
PoolsSummary, RewardStats,
|
||||
PoolsSummary, RewardStats, Version,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -154,7 +154,7 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
"/api/v1/mining/pool/{slug}/blocks/{height}",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, Path(PoolSlugAndHeightParam {slug, height}): Path<PoolSlugAndHeightParam>, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_blocks(slug, Some(height))).await
|
||||
state.cached_json(&headers, state.height_cache(Version::ONE, height), &uri, move |q| q.pool_blocks(slug, Some(height))).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_pool_blocks_from")
|
||||
|
||||
@@ -10,7 +10,8 @@ use brk_types::{CpfpInfo, MerkleProof, Transaction, TxOutspend, TxStatus, Txid,
|
||||
|
||||
use crate::{
|
||||
AppState, CacheStrategy,
|
||||
extended::TransformResponseExtended,
|
||||
cache::CacheParams,
|
||||
extended::{ResponseExtended, TransformResponseExtended},
|
||||
params::{TxidParam, TxidVout, TxidsParam},
|
||||
};
|
||||
|
||||
@@ -132,7 +133,16 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
Path(path): Path<TxidVout>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.outspend(&path.txid, path.vout)).await
|
||||
let v = Version::ONE;
|
||||
let immutable = CacheParams::immutable(v);
|
||||
if immutable.matches_etag(&headers) {
|
||||
return ResponseExtended::new_not_modified_with(&immutable);
|
||||
}
|
||||
let outspend = state.run(move |q| q.outspend(&path.txid, path.vout)).await;
|
||||
let height = state.sync(|q| q.height());
|
||||
let is_deep = outspend.as_ref().is_ok_and(|o| o.is_deeply_spent(height));
|
||||
let strategy = if is_deep { CacheStrategy::Immutable(v) } else { CacheStrategy::Tip };
|
||||
state.cached_json(&headers, strategy, &uri, move |_| outspend).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_tx_outspend")
|
||||
@@ -157,7 +167,16 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
Path(param): Path<TxidParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.outspends(¶m.txid)).await
|
||||
let v = Version::ONE;
|
||||
let immutable = CacheParams::immutable(v);
|
||||
if immutable.matches_etag(&headers) {
|
||||
return ResponseExtended::new_not_modified_with(&immutable);
|
||||
}
|
||||
let outspends = state.run(move |q| q.outspends(¶m.txid)).await;
|
||||
let height = state.sync(|q| q.height());
|
||||
let all_deep = outspends.as_ref().is_ok_and(|os| os.iter().all(|o| o.is_deeply_spent(height)));
|
||||
let strategy = if all_deep { CacheStrategy::Immutable(v) } else { CacheStrategy::Tip };
|
||||
state.cached_json(&headers, strategy, &uri, move |_| outspends).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_tx_outspends")
|
||||
@@ -237,7 +256,8 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
post_with(
|
||||
async |State(state): State<AppState>, body: String| {
|
||||
let hex = body.trim().to_string();
|
||||
state.sync(|q| q.broadcast_transaction(&hex))
|
||||
state.run(move |q| q.broadcast_transaction(&hex))
|
||||
.await
|
||||
.map(|txid| txid.to_string())
|
||||
.map_err(crate::Error::from)
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ use aide::{
|
||||
};
|
||||
use axum::{
|
||||
Extension,
|
||||
http::{HeaderMap, header},
|
||||
http::HeaderMap,
|
||||
response::{Html, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
@@ -78,13 +78,12 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
)
|
||||
.route("/api", get(Html::from(include_str!("./scalar.html"))))
|
||||
// Pre-compressed with: brotli -c -q 11 scalar.js > scalar.js.br
|
||||
.route("/scalar.js", get(|| async {
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "application/javascript"),
|
||||
(header::CONTENT_ENCODING, "br"),
|
||||
],
|
||||
.route("/scalar.js", get(|headers: HeaderMap| async move {
|
||||
Response::static_bytes(
|
||||
&headers,
|
||||
include_bytes!("./scalar.js.br").as_slice(),
|
||||
"application/javascript",
|
||||
"br",
|
||||
)
|
||||
}))
|
||||
.route(
|
||||
|
||||
@@ -4,15 +4,15 @@ use axum::{
|
||||
Extension,
|
||||
body::{Body, Bytes},
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
http::{HeaderMap, Uri},
|
||||
response::Response,
|
||||
};
|
||||
use brk_types::{Format, Output, SeriesSelection};
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::series::{CACHE_CONTROL, max_weight},
|
||||
extended::{ContentEncoding, HeaderMapExtended},
|
||||
extended::{ContentEncoding, HeaderMapExtended, ResponseExtended},
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
@@ -34,7 +34,7 @@ pub async fn handler(
|
||||
let csv_filename = resolved.csv_filename();
|
||||
|
||||
if headers.has_etag(etag.as_str()) {
|
||||
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
|
||||
return Ok(Response::new_not_modified(&etag, CACHE_CONTROL));
|
||||
}
|
||||
|
||||
// Phase 2: Format (expensive, server-side cached)
|
||||
|
||||
@@ -4,8 +4,8 @@ use axum::{
|
||||
Extension,
|
||||
body::{Body, Bytes},
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
http::{HeaderMap, Uri},
|
||||
response::Response,
|
||||
};
|
||||
use brk_error::Result as BrkResult;
|
||||
use brk_query::{Query as BrkQuery, ResolvedQuery};
|
||||
@@ -14,7 +14,7 @@ use brk_types::{Format, Output, SeriesOutput, SeriesSelection};
|
||||
use crate::{
|
||||
Result,
|
||||
api::series::{CACHE_CONTROL, max_weight},
|
||||
extended::{ContentEncoding, HeaderMapExtended},
|
||||
extended::{ContentEncoding, HeaderMapExtended, ResponseExtended},
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
@@ -57,7 +57,7 @@ async fn format_and_respond(
|
||||
let csv_filename = resolved.csv_filename();
|
||||
|
||||
if headers.has_etag(etag.as_str()) {
|
||||
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
|
||||
return Ok(Response::new_not_modified(&etag, CACHE_CONTROL));
|
||||
}
|
||||
|
||||
// Phase 2: Format (expensive, server-side cached)
|
||||
|
||||
@@ -4,15 +4,15 @@ use axum::{
|
||||
Extension,
|
||||
body::{Body, Bytes},
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
http::{HeaderMap, Uri},
|
||||
response::Response,
|
||||
};
|
||||
use brk_types::{Format, OutputLegacy, SeriesSelection};
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::series::{CACHE_CONTROL, max_weight},
|
||||
extended::{ContentEncoding, HeaderMapExtended},
|
||||
extended::{ContentEncoding, HeaderMapExtended, ResponseExtended},
|
||||
};
|
||||
|
||||
pub const SUNSET: &str = "2027-01-01T00:00:00Z";
|
||||
@@ -36,7 +36,7 @@ pub async fn handler(
|
||||
let csv_filename = resolved.csv_filename();
|
||||
|
||||
if headers.has_etag(etag.as_str()) {
|
||||
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
|
||||
return Ok(Response::new_not_modified(&etag, CACHE_CONTROL));
|
||||
}
|
||||
|
||||
// Phase 2: Format (expensive, server-side cached)
|
||||
|
||||
@@ -33,6 +33,13 @@ pub struct CacheParams {
|
||||
}
|
||||
|
||||
impl CacheParams {
|
||||
pub fn immutable(version: Version) -> Self {
|
||||
Self {
|
||||
etag: Some(format!("i{version}")),
|
||||
cache_control: "public, max-age=1, must-revalidate".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache params using CARGO_PKG_VERSION as etag (for openapi.json etc.)
|
||||
pub fn static_version() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -19,6 +19,8 @@ pub trait HeaderMapExtended {
|
||||
fn insert_content_type_application_json(&mut self);
|
||||
fn insert_content_type_text_csv(&mut self);
|
||||
|
||||
fn insert_vary_accept_encoding(&mut self);
|
||||
|
||||
fn insert_deprecation(&mut self, sunset: &'static str);
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ impl HeaderMapExtended for HeaderMap {
|
||||
fn insert_content_encoding(&mut self, encoding: ContentEncoding) {
|
||||
if let Some(value) = encoding.header_value() {
|
||||
self.insert(header::CONTENT_ENCODING, value);
|
||||
self.insert_vary_accept_encoding();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +73,10 @@ impl HeaderMapExtended for HeaderMap {
|
||||
self.insert(header::CONTENT_TYPE, "text/csv".parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_vary_accept_encoding(&mut self) {
|
||||
self.insert(header::VARY, "Accept-Encoding".parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_deprecation(&mut self, sunset: &'static str) {
|
||||
self.insert("Deprecation", "true".parse().unwrap());
|
||||
self.insert("Sunset", sunset.parse().unwrap());
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{HeaderMap, Response, StatusCode},
|
||||
http::{HeaderMap, Response, StatusCode, header},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use brk_types::Etag;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::header_map::HeaderMapExtended;
|
||||
@@ -12,22 +13,36 @@ pub trait ResponseExtended
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn new_not_modified() -> Self;
|
||||
fn new_not_modified(etag: &Etag, cache_control: &str) -> Self;
|
||||
fn new_not_modified_with(params: &CacheParams) -> Self;
|
||||
fn new_json_cached<T>(value: T, params: &CacheParams) -> Self
|
||||
where
|
||||
T: Serialize;
|
||||
fn static_json<T>(headers: &HeaderMap, value: T) -> Self
|
||||
where
|
||||
T: Serialize;
|
||||
fn static_bytes(
|
||||
headers: &HeaderMap,
|
||||
bytes: &'static [u8],
|
||||
content_type: &'static str,
|
||||
content_encoding: &'static str,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
impl ResponseExtended for Response<Body> {
|
||||
fn new_not_modified() -> Response<Body> {
|
||||
fn new_not_modified(etag: &Etag, cache_control: &str) -> Response<Body> {
|
||||
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
|
||||
let _headers = response.headers_mut();
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_etag(etag.as_str());
|
||||
headers.insert_cache_control(cache_control);
|
||||
response
|
||||
}
|
||||
|
||||
fn new_not_modified_with(params: &CacheParams) -> Response<Body> {
|
||||
let etag = Etag::from(params.etag_str());
|
||||
Self::new_not_modified(&etag, ¶ms.cache_control)
|
||||
}
|
||||
|
||||
fn new_json_cached<T>(value: T, params: &CacheParams) -> Self
|
||||
where
|
||||
T: Serialize,
|
||||
@@ -49,8 +64,29 @@ impl ResponseExtended for Response<Body> {
|
||||
{
|
||||
let params = CacheParams::static_version();
|
||||
if params.matches_etag(headers) {
|
||||
return Self::new_not_modified();
|
||||
return Self::new_not_modified_with(¶ms);
|
||||
}
|
||||
Self::new_json_cached(value, ¶ms)
|
||||
}
|
||||
|
||||
fn static_bytes(
|
||||
headers: &HeaderMap,
|
||||
bytes: &'static [u8],
|
||||
content_type: &'static str,
|
||||
content_encoding: &'static str,
|
||||
) -> Self {
|
||||
let params = CacheParams::static_version();
|
||||
if params.matches_etag(headers) {
|
||||
return Self::new_not_modified_with(¶ms);
|
||||
}
|
||||
let mut response = Response::new(Body::from(bytes));
|
||||
let h = response.headers_mut();
|
||||
h.insert(header::CONTENT_TYPE, content_type.parse().unwrap());
|
||||
h.insert(header::CONTENT_ENCODING, content_encoding.parse().unwrap());
|
||||
h.insert_cache_control(¶ms.cache_control);
|
||||
if let Some(etag) = ¶ms.etag {
|
||||
h.insert_etag(etag);
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ use std::{
|
||||
any::Any,
|
||||
net::SocketAddr,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::AtomicU64,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -59,6 +62,7 @@ impl Server {
|
||||
data_path,
|
||||
website,
|
||||
cache: Arc::new(Cache::new(1_000)),
|
||||
last_tip: Arc::new(AtomicU64::new(0)),
|
||||
started_at: jiff::Timestamp::now(),
|
||||
started_instant: Instant::now(),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::{
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -10,7 +13,10 @@ use axum::{
|
||||
http::{HeaderMap, HeaderValue, Response, Uri, header},
|
||||
};
|
||||
use brk_query::AsyncQuery;
|
||||
use brk_types::{Addr, BlockHash, BlockHashPrefix, Height, Txid, Version};
|
||||
use brk_types::{
|
||||
Addr, BlockHash, BlockHashPrefix, Height, ONE_HOUR_IN_SEC, Timestamp as BrkTimestamp, Txid,
|
||||
Version,
|
||||
};
|
||||
use derive_more::Deref;
|
||||
use jiff::Timestamp;
|
||||
use quick_cache::sync::{Cache, GuardResult};
|
||||
@@ -28,6 +34,7 @@ pub struct AppState {
|
||||
pub data_path: PathBuf,
|
||||
pub website: Website,
|
||||
pub cache: Arc<Cache<String, Bytes>>,
|
||||
pub last_tip: Arc<AtomicU64>,
|
||||
pub started_at: Timestamp,
|
||||
pub started_instant: Instant,
|
||||
}
|
||||
@@ -43,15 +50,26 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Smart address caching: checks mempool activity first, then on-chain.
|
||||
/// `Immutable` if timestamp is >6 hours old (block definitely >6 deep), `Tip` otherwise.
|
||||
pub fn timestamp_cache(&self, version: Version, timestamp: BrkTimestamp) -> CacheStrategy {
|
||||
if (*BrkTimestamp::now()).saturating_sub(*timestamp) > 6 * ONE_HOUR_IN_SEC {
|
||||
CacheStrategy::Immutable(version)
|
||||
} else {
|
||||
CacheStrategy::Tip
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_cache(&self, version: Version, addr: &Addr) -> CacheStrategy {
|
||||
pub fn addr_cache(&self, version: Version, addr: &Addr, chain_only: bool) -> CacheStrategy {
|
||||
self.sync(|q| {
|
||||
let mempool_hash = q.addr_mempool_hash(addr);
|
||||
if mempool_hash != 0 {
|
||||
return CacheStrategy::MempoolHash(mempool_hash);
|
||||
if !chain_only {
|
||||
let mempool_hash = q.addr_mempool_hash(addr);
|
||||
if mempool_hash != 0 {
|
||||
return CacheStrategy::MempoolHash(mempool_hash);
|
||||
}
|
||||
}
|
||||
q.addr_last_activity_height(addr)
|
||||
.and_then(|h| {
|
||||
@@ -125,9 +143,13 @@ impl AppState {
|
||||
F: FnOnce(&brk_query::Query, ContentEncoding) -> brk_error::Result<Bytes> + Send + 'static,
|
||||
{
|
||||
let encoding = ContentEncoding::negotiate(headers);
|
||||
let params = CacheParams::resolve(&strategy, || self.sync(|q| q.tip_hash_prefix()));
|
||||
let tip = self.sync(|q| q.tip_hash_prefix());
|
||||
if self.last_tip.swap(*tip, Ordering::Relaxed) != *tip {
|
||||
self.cache.clear();
|
||||
}
|
||||
let params = CacheParams::resolve(&strategy, || tip);
|
||||
if params.matches_etag(headers) {
|
||||
return ResponseExtended::new_not_modified();
|
||||
return ResponseExtended::new_not_modified_with(¶ms);
|
||||
}
|
||||
|
||||
let full_key = format!("{}-{}-{}", uri, params.etag_str(), encoding.as_str());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{TxStatus, Txid, Vin};
|
||||
use crate::{Height, TxStatus, Txid, Vin};
|
||||
|
||||
/// Status of an output indicating whether it has been spent
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -29,4 +29,13 @@ impl TxOutspend {
|
||||
vin: None,
|
||||
status: None,
|
||||
};
|
||||
|
||||
pub fn is_deeply_spent(&self, current_height: Height) -> bool {
|
||||
self.spent
|
||||
&& self
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|s| s.block_height)
|
||||
.is_some_and(|h| (*current_height).saturating_sub(*h) > 6)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user