diff --git a/crates/brk_mempool/src/projected_blocks/fees.rs b/crates/brk_mempool/src/projected_blocks/fees.rs index 8f0d00f10..1ea722002 100644 --- a/crates/brk_mempool/src/projected_blocks/fees.rs +++ b/crates/brk_mempool/src/projected_blocks/fees.rs @@ -2,9 +2,6 @@ use brk_types::{FeeRate, RecommendedFees}; use super::stats::BlockStats; -/// Minimum fee rate for estimation (sat/vB). -const MIN_FEE_RATE: f64 = 1.0; - /// Compute recommended fees from block stats (mempool.space style). pub fn compute_recommended_fees(stats: &[BlockStats]) -> RecommendedFees { RecommendedFees { @@ -12,7 +9,7 @@ pub fn compute_recommended_fees(stats: &[BlockStats]) -> RecommendedFees { half_hour_fee: median_fee_for_block(stats, 2), hour_fee: median_fee_for_block(stats, 5), economy_fee: median_fee_for_block(stats, 7), - minimum_fee: FeeRate::from(MIN_FEE_RATE), + minimum_fee: FeeRate::MIN, } } @@ -21,5 +18,5 @@ fn median_fee_for_block(stats: &[BlockStats], block_index: usize) -> FeeRate { stats .get(block_index) .map(|s| s.median_fee_rate()) - .unwrap_or_else(|| FeeRate::from(MIN_FEE_RATE)) + .unwrap_or_else(|| FeeRate::MIN) } diff --git a/crates/brk_query/src/impl/metrics.rs b/crates/brk_query/src/impl/metrics.rs index 06ed3db66..ceb3054ab 100644 --- a/crates/brk_query/src/impl/metrics.rs +++ b/crates/brk_query/src/impl/metrics.rs @@ -22,6 +22,16 @@ impl Query { } pub fn metric_not_found_error(&self, metric: &Metric) -> Error { + // Check if metric exists but with different indexes + if let Some(indexes) = self.vecs().metric_to_indexes(metric.clone()) { + let index_list: Vec<_> = indexes.iter().map(|i| i.to_string()).collect(); + return Error::String(format!( + "'{metric}' doesn't support the requested index. Supported indexes: {}", + index_list.join(", ") + )); + } + + // Metric doesn't exist, suggest alternatives if let Some(first) = self.match_metric(metric, Limit::MIN).first() { Error::String(format!("Could not find '{metric}', did you mean '{first}'?")) } else { @@ -77,8 +87,9 @@ impl Query { metric: &dyn AnyExportableVec, params: &DataRangeFormat, ) -> Result { + let len = metric.len(); let from = params.from().map(|from| metric.i64_to_usize(from)); - let to = params.to().map(|to| metric.i64_to_usize(to)); + let to = params.to_for_len(len).map(|to| metric.i64_to_usize(to)); Ok(match params.format() { Format::CSV => Output::CSV(Self::columns_to_csv( @@ -100,6 +111,9 @@ impl Query { metrics: &[&dyn AnyExportableVec], params: &DataRangeFormat, ) -> Result { + // Use min length across metrics for consistent count resolution + let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0); + let from = params.from().map(|from| { metrics .iter() @@ -108,7 +122,7 @@ impl Query { .unwrap_or_default() }); - let to = params.to().map(|to| { + let to = params.to_for_len(min_len).map(|to| { metrics .iter() .map(|v| v.i64_to_usize(to)) @@ -143,13 +157,20 @@ impl Query { }) } - /// Search for vecs matching the given metrics and index - pub fn search(&self, params: &MetricSelection) -> Vec<&'static dyn AnyExportableVec> { - params - .metrics - .iter() - .filter_map(|metric| self.vecs().get(metric, params.index)) - .collect() + /// Search for vecs matching the given metrics and index. + /// Returns error if no metrics requested or any requested metric is not found. + pub fn search(&self, params: &MetricSelection) -> Result> { + if params.metrics.is_empty() { + return Err(Error::String("No metrics specified".to_string())); + } + let mut vecs = Vec::with_capacity(params.metrics.len()); + for metric in params.metrics.iter() { + match self.vecs().get(metric, params.index) { + Some(vec) => vecs.push(vec), + None => return Err(self.metric_not_found_error(metric)), + } + } + Ok(vecs) } /// Calculate total weight of the vecs for the given range @@ -168,14 +189,11 @@ impl Query { params: MetricSelection, max_weight: usize, ) -> Result { - let vecs = self.search(¶ms); + let vecs = self.search(¶ms)?; - let Some(metric) = vecs.first() else { - let metric = params.metrics.first().cloned().unwrap_or_else(|| Metric::from("")); - return Err(self.metric_not_found_error(&metric)); - }; + let metric = vecs.first().expect("search guarantees non-empty on success"); - let weight = Self::weight(&vecs, params.from(), params.to()); + let weight = Self::weight(&vecs, params.from(), params.to_for_len(metric.len())); if weight > max_weight { return Err(Error::String(format!( "Request too heavy: {weight} bytes exceeds limit of {max_weight} bytes" @@ -196,13 +214,10 @@ impl Query { params: MetricSelection, max_weight: usize, ) -> Result { - let vecs = self.search(¶ms); + let vecs = self.search(¶ms)?; - if vecs.is_empty() { - return Ok(Output::default(params.range.format())); - } - - let weight = Self::weight(&vecs, params.from(), params.to()); + let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty"); + let weight = Self::weight(&vecs, params.from(), params.to_for_len(min_len)); if weight > max_weight { return Err(Error::String(format!( "Request too heavy: {weight} bytes exceeds limit of {max_weight} bytes" diff --git a/crates/brk_query/src/impl/metrics_legacy.rs b/crates/brk_query/src/impl/metrics_legacy.rs index e26ec6369..7037291ff 100644 --- a/crates/brk_query/src/impl/metrics_legacy.rs +++ b/crates/brk_query/src/impl/metrics_legacy.rs @@ -9,12 +9,14 @@ use crate::{DataRangeFormat, LegacyValue, MetricSelection, OutputLegacy, Query}; impl Query { /// Deprecated - raw data without MetricData wrapper pub fn format_legacy(&self, metrics: &[&dyn AnyExportableVec], params: &DataRangeFormat) -> Result { + let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0); + let from = params .from() .map(|from| metrics.iter().map(|v| v.i64_to_usize(from)).min().unwrap_or_default()); let to = params - .to() + .to_for_len(min_len) .map(|to| metrics.iter().map(|v| v.i64_to_usize(to)).min().unwrap_or_default()); let format = params.format(); @@ -57,13 +59,10 @@ impl Query { /// Deprecated - use search_and_format_checked instead pub fn search_and_format_legacy_checked(&self, params: MetricSelection, max_weight: usize) -> Result { - let vecs = self.search(¶ms); + let vecs = self.search(¶ms)?; - if vecs.is_empty() { - return Ok(OutputLegacy::default(params.range.format())); - } - - let weight = Self::weight(&vecs, params.from(), params.to()); + let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty"); + let weight = Self::weight(&vecs, params.from(), params.to_for_len(min_len)); if weight > max_weight { return Err(Error::String(format!( "Request too heavy: {weight} bytes exceeds limit of {max_weight} bytes" diff --git a/crates/brk_query/src/impl/mining/block_fee_rates.rs b/crates/brk_query/src/impl/mining/block_fee_rates.rs index 80799fb0b..95de7959c 100644 --- a/crates/brk_query/src/impl/mining/block_fee_rates.rs +++ b/crates/brk_query/src/impl/mining/block_fee_rates.rs @@ -1,3 +1,12 @@ +// TODO: INCOMPLETE - indexes_to_fee_rate.dateindex doesn't have percentile fields +// because from_txindex.rs calls remove_percentiles() before creating dateindex. +// Need to either: +// 1. Use .height instead and convert height to dateindex for iteration +// 2. Fix from_txindex.rs to preserve percentiles for dateindex +// 3. Create a separate dateindex computation path with percentiles + +#![allow(dead_code)] + use brk_error::Result; use brk_types::{BlockFeeRatesEntry, FeeRatePercentiles, TimePeriod}; use vecdb::{IterableVec, VecIndex}; @@ -6,38 +15,42 @@ use super::dateindex_iter::DateIndexIter; use crate::Query; impl Query { - pub fn block_fee_rates(&self, time_period: TimePeriod) -> Result> { - let computer = self.computer(); - let current_height = self.height(); - let start = current_height - .to_usize() - .saturating_sub(time_period.block_count()); + pub fn block_fee_rates(&self, _time_period: TimePeriod) -> Result> { + // Disabled until percentile data is available at dateindex level + Ok(Vec::new()) - let iter = DateIndexIter::new(computer, start, current_height.to_usize()); - - let vecs = &computer.chain.indexes_to_fee_rate.dateindex; - let mut min = vecs.unwrap_min().iter(); - let mut pct10 = vecs.unwrap_pct10().iter(); - let mut pct25 = vecs.unwrap_pct25().iter(); - let mut median = vecs.unwrap_median().iter(); - let mut pct75 = vecs.unwrap_pct75().iter(); - let mut pct90 = vecs.unwrap_pct90().iter(); - let mut max = vecs.unwrap_max().iter(); - - Ok(iter.collect(|di, ts, h| { - Some(BlockFeeRatesEntry { - avg_height: h, - timestamp: ts, - percentiles: FeeRatePercentiles::new( - min.get(di).unwrap_or_default(), - pct10.get(di).unwrap_or_default(), - pct25.get(di).unwrap_or_default(), - median.get(di).unwrap_or_default(), - pct75.get(di).unwrap_or_default(), - pct90.get(di).unwrap_or_default(), - max.get(di).unwrap_or_default(), - ), - }) - })) + // Original implementation: + // let computer = self.computer(); + // let current_height = self.height(); + // let start = current_height + // .to_usize() + // .saturating_sub(time_period.block_count()); + // + // let iter = DateIndexIter::new(computer, start, current_height.to_usize()); + // + // let vecs = &computer.chain.indexes_to_fee_rate.dateindex; + // let mut min = vecs.unwrap_min().iter(); + // let mut pct10 = vecs.unwrap_pct10().iter(); + // let mut pct25 = vecs.unwrap_pct25().iter(); + // let mut median = vecs.unwrap_median().iter(); + // let mut pct75 = vecs.unwrap_pct75().iter(); + // let mut pct90 = vecs.unwrap_pct90().iter(); + // let mut max = vecs.unwrap_max().iter(); + // + // Ok(iter.collect(|di, ts, h| { + // Some(BlockFeeRatesEntry { + // avg_height: h, + // timestamp: ts, + // percentiles: FeeRatePercentiles::new( + // min.get(di).unwrap_or_default(), + // pct10.get(di).unwrap_or_default(), + // pct25.get(di).unwrap_or_default(), + // median.get(di).unwrap_or_default(), + // pct75.get(di).unwrap_or_default(), + // pct90.get(di).unwrap_or_default(), + // max.get(di).unwrap_or_default(), + // ), + // }) + // })) } } diff --git a/crates/brk_query/src/impl/mining/dateindex_iter.rs b/crates/brk_query/src/impl/mining/dateindex_iter.rs index 18d49b28a..0a7e07c1a 100644 --- a/crates/brk_query/src/impl/mining/dateindex_iter.rs +++ b/crates/brk_query/src/impl/mining/dateindex_iter.rs @@ -48,8 +48,9 @@ impl<'a> DateIndexIter<'a> { .computer .chain .timeindexes_to_timestamp - .dateindex_extra - .unwrap_first() + .dateindex + .as_ref() + .expect("timeindexes_to_timestamp.dateindex should exist") .iter(); let mut heights = self.computer.indexes.dateindex_to_first_height.iter(); diff --git a/crates/brk_query/src/impl/mining/hashrate.rs b/crates/brk_query/src/impl/mining/hashrate.rs index 8b5c57038..edc3adfd4 100644 --- a/crates/brk_query/src/impl/mining/hashrate.rs +++ b/crates/brk_query/src/impl/mining/hashrate.rs @@ -23,6 +23,7 @@ impl Query { .indexes .height_to_dateindex .read_once(current_height)?; + let current_hashrate = *computer .chain .indexes_to_hash_rate @@ -58,11 +59,13 @@ impl Query { .dateindex .unwrap_last() .iter(); + let mut timestamp_iter = computer .chain .timeindexes_to_timestamp - .dateindex_extra - .unwrap_first() + .dateindex + .as_ref() + .expect("timeindexes_to_timestamp.dateindex should exist") .iter(); let mut hashrates = Vec::with_capacity(total_days / step + 1); diff --git a/crates/brk_server/src/api/blocks/mod.rs b/crates/brk_server/src/api/blocks/mod.rs index da0060eaf..b95fd0446 100644 --- a/crates/brk_server/src/api/blocks/mod.rs +++ b/crates/brk_server/src/api/blocks/mod.rs @@ -2,13 +2,11 @@ use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, State}, http::HeaderMap, - response::Redirect, - routing::get, }; use brk_query::BLOCK_TXS_PAGE_SIZE; use brk_types::{ BlockHashParam, BlockHashStartIndex, BlockHashTxIndex, BlockInfo, BlockStatus, BlockTimestamp, - HeightParam, StartHeightParam, TimestampParam, Transaction, Txid, + HeightParam, TimestampParam, Transaction, Txid, }; use crate::{CacheStrategy, extended::TransformResponseExtended}; @@ -21,10 +19,23 @@ pub trait BlockRoutes { impl BlockRoutes for ApiRouter { fn add_block_routes(self) -> Self { - self.route("/api/block", get(Redirect::temporary("/api/blocks"))) - .route( + self.api_route( "/api/blocks", - get(Redirect::temporary("/api#tag/blocks")), + get_with( + async |headers: HeaderMap, State(state): State| { + state + .cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(None)) + .await + }, + |op| { + op.blocks_tag() + .summary("Recent blocks") + .description("Retrieve the last 10 blocks. Returns block metadata for each block.") + .ok_response::>() + .not_modified() + .server_error() + }, + ), ) .api_route( "/api/block/{hash}", @@ -93,18 +104,18 @@ impl BlockRoutes for ApiRouter { ), ) .api_route( - "/api/blocks/{start_height}", + "/api/blocks/{height}", get_with( async |headers: HeaderMap, - Path(path): Path, + Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(path.start_height)).await + state.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(Some(path.height))).await }, |op| { op.blocks_tag() - .summary("Recent blocks") + .summary("Blocks from height") .description( - "Retrieve the last 10 blocks, optionally starting from a specific height. Returns block metadata for each block.", + "Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.", ) .ok_response::>() .not_modified() diff --git a/crates/brk_server/src/api/metrics/bulk.rs b/crates/brk_server/src/api/metrics/bulk.rs index 843d1884b..d96558397 100644 --- a/crates/brk_server/src/api/metrics/bulk.rs +++ b/crates/brk_server/src/api/metrics/bulk.rs @@ -41,11 +41,11 @@ async fn req_to_response_res( ) -> brk_error::Result { let format = params.format(); let height = query.sync(|q| q.height()); - let to = params.to(); - let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || { - height.into() - }); + let cache_params = + CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || { + height.into() + }); if cache_params.matches_etag(&headers) { let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); diff --git a/crates/brk_server/src/api/metrics/data.rs b/crates/brk_server/src/api/metrics/data.rs index dd9ef9fa8..9ca563620 100644 --- a/crates/brk_server/src/api/metrics/data.rs +++ b/crates/brk_server/src/api/metrics/data.rs @@ -43,11 +43,11 @@ async fn req_to_response_res( ) -> brk_error::Result { let format = params.format(); let height = query.sync(|q| q.height()); - let to = params.to(); - let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || { - height.into() - }); + let cache_params = + CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || { + height.into() + }); if cache_params.matches_etag(&headers) { let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); diff --git a/crates/brk_server/src/api/metrics/legacy.rs b/crates/brk_server/src/api/metrics/legacy.rs index f25818283..8cd16329c 100644 --- a/crates/brk_server/src/api/metrics/legacy.rs +++ b/crates/brk_server/src/api/metrics/legacy.rs @@ -43,11 +43,11 @@ async fn req_to_response_res( ) -> brk_error::Result { let format = params.format(); let height = query.sync(|q| q.height()); - let to = params.to(); - let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || { - height.into() - }); + let cache_params = + CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || { + height.into() + }); if cache_params.matches_etag(&headers) { let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index 27ebf6971..275d9ab03 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -193,27 +193,9 @@ impl ApiMetricsRoutes for ApiRouter { .not_modified(), ), ) - // !!! - // DEPRECATED: Do not use - // !!! - .route( - "/api/vecs/query", - get( - async |uri: Uri, - headers: HeaderMap, - Query(params): Query, - state: State| - -> Response { - legacy::handler(uri, headers, Query(params.into()), state).await - }, - ), - ) - // !!! - // DEPRECATED: Do not use - // !!! - .route( + .api_route( "/api/vecs/{variant}", - get( + get_with( async |uri: Uri, headers: HeaderMap, Path(variant): Path, @@ -236,6 +218,41 @@ impl ApiMetricsRoutes for ApiRouter { )); legacy::handler(uri, headers, Query(params), state).await }, + |op| op + .metrics_tag() + .summary("Legacy variant endpoint") + .description( + "**DEPRECATED** - Use `/api/metric/{metric}/{index}` instead.\n\n\ + Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\ + Legacy endpoint for querying metrics by variant path (e.g., `dateindex_to_price`). \ + Returns raw data without the MetricData wrapper." + ) + .deprecated() + .ok_response::() + .not_modified(), + ), + ) + .api_route( + "/api/vecs/query", + get_with( + async |uri: Uri, + headers: HeaderMap, + Query(params): Query, + state: State| + -> Response { + legacy::handler(uri, headers, Query(params.into()), state).await + }, + |op| op + .metrics_tag() + .summary("Legacy query endpoint") + .description( + "**DEPRECATED** - Use `/api/metric/{metric}/{index}` or `/api/metrics/bulk` instead.\n\n\ + Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\ + Legacy endpoint for querying metrics. Returns raw data without the MetricData wrapper." + ) + .deprecated() + .ok_response::() + .not_modified(), ), ) } diff --git a/crates/brk_server/src/api/mining/mod.rs b/crates/brk_server/src/api/mining/mod.rs index ace7b9005..6cceef6a6 100644 --- a/crates/brk_server/src/api/mining/mod.rs +++ b/crates/brk_server/src/api/mining/mod.rs @@ -6,7 +6,7 @@ use axum::{ routing::get, }; use brk_types::{ - BlockCountParam, BlockFeeRatesEntry, BlockFeesEntry, BlockRewardsEntry, BlockSizesWeights, + BlockCountParam, BlockFeesEntry, BlockRewardsEntry, BlockSizesWeights, DifficultyAdjustment, DifficultyAdjustmentEntry, HashrateSummary, PoolDetail, PoolInfo, PoolSlugParam, PoolsSummary, RewardStats, TimePeriodParam, }; @@ -187,22 +187,23 @@ impl MiningRoutes for ApiRouter { }, ), ) - .api_route( - "/api/v1/mining/blocks/fee-rates/{time_period}", - get_with( - async |headers: HeaderMap, Path(path): Path, State(state): State| { - 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() - .summary("Block fee rates") - .description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") - .ok_response::>() - .not_modified() - .server_error() - }, - ), - ) + // TODO: Disabled - dateindex doesn't have percentile fields (see block_fee_rates.rs) + // .api_route( + // "/api/v1/mining/blocks/fee-rates/{time_period}", + // get_with( + // async |headers: HeaderMap, Path(path): Path, State(state): State| { + // 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() + // .summary("Block fee rates") + // .description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y") + // .ok_response::>() + // .not_modified() + // .server_error() + // }, + // ), + // ) .api_route( "/api/v1/mining/blocks/sizes-weights/{time_period}", get_with( diff --git a/crates/brk_server/src/extended/transform_operation.rs b/crates/brk_server/src/extended/transform_operation.rs index 0f2cde519..cbf094409 100644 --- a/crates/brk_server/src/extended/transform_operation.rs +++ b/crates/brk_server/src/extended/transform_operation.rs @@ -11,6 +11,9 @@ pub trait TransformResponseExtended<'t> { fn server_tag(self) -> Self; fn transactions_tag(self) -> Self; + /// Mark operation as deprecated + fn deprecated(self) -> Self; + /// 200 fn ok_response(self) -> Self where @@ -66,6 +69,11 @@ impl<'t> TransformResponseExtended<'t> for TransformOperation<'t> { self.ok_response_with(|r: TransformResponse<'_, R>| r) } + fn deprecated(mut self) -> Self { + self.inner_mut().deprecated = true; + self + } + fn ok_response_with(self, f: F) -> Self where R: JsonSchema, diff --git a/crates/brk_types/src/blockhash.rs b/crates/brk_types/src/blockhash.rs index 4cae319b1..708b7ed6d 100644 --- a/crates/brk_types/src/blockhash.rs +++ b/crates/brk_types/src/blockhash.rs @@ -13,7 +13,8 @@ use vecdb::{Bytes, Formattable}; #[schemars( transparent, with = "String", - example = &"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + example = &"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + example = &"0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" )] pub struct BlockHash([u8; 32]); diff --git a/crates/brk_types/src/datarange.rs b/crates/brk_types/src/datarange.rs index 448ddee1f..f61bd0782 100644 --- a/crates/brk_types/src/datarange.rs +++ b/crates/brk_types/src/datarange.rs @@ -6,17 +6,17 @@ use crate::{de_unquote_i64, de_unquote_usize}; /// Range parameters for slicing data #[derive(Default, Debug, Deserialize, JsonSchema)] pub struct DataRange { - /// Inclusive starting index, if negative will be from the end + /// Inclusive starting index, if negative counts from end #[serde(default, alias = "f", deserialize_with = "de_unquote_i64")] #[schemars(example = 0, example = -1, example = -10, example = -1000)] from: Option, - /// Exclusive ending index, if negative will be from the end, overrides 'count' + /// Exclusive ending index, if negative counts from end #[serde(default, alias = "t", deserialize_with = "de_unquote_i64")] #[schemars(example = 1000)] to: Option, - /// Number of values requested + /// Number of values to return (ignored if `to` is set) #[serde(default, alias = "c", deserialize_with = "de_unquote_usize")] #[schemars(example = 1, example = 10, example = 100)] count: Option, @@ -38,23 +38,34 @@ impl DataRange { self } + /// Get the raw `from` value pub fn from(&self) -> Option { self.from } - pub fn to(&self) -> Option { - if self.to.is_none() - && let Some(c) = self.count - { - let c = c as i64; - if let Some(f) = self.from { - if f >= 0 || f.abs() > c { - return Some(f + c); - } - } else { - return Some(c); - } + /// Get `to` value, computing it from `from + count` if `to` is unset but `count` is set. + /// Requires the vec length to resolve negative `from` indices before adding count. + pub fn to_for_len(&self, len: usize) -> Option { + if self.to.is_some() { + return self.to; + } + + self.count.map(|count| { + let resolved_from = self.resolve_index(self.from, len, 0); + (resolved_from + count).min(len) as i64 + }) + } + + /// Returns a string for etag/cache key generation that captures all range parameters + pub fn etag_suffix(&self) -> String { + format!("{:?}{:?}{:?}", self.from, self.to, self.count) + } + + fn resolve_index(&self, idx: Option, len: usize, default: usize) -> usize { + match idx { + None => default, + Some(i) if i >= 0 => (i as usize).min(len), + Some(i) => len.saturating_sub((-i) as usize), } - self.to } } diff --git a/crates/brk_types/src/feerate.rs b/crates/brk_types/src/feerate.rs index ba95c24f8..740a0f019 100644 --- a/crates/brk_types/src/feerate.rs +++ b/crates/brk_types/src/feerate.rs @@ -14,6 +14,8 @@ use super::{Sats, VSize}; pub struct FeeRate(f64); impl FeeRate { + pub const MIN: Self = Self(0.1); + pub fn new(fr: f64) -> Self { Self(fr) } diff --git a/crates/brk_types/src/lib.rs b/crates/brk_types/src/lib.rs index 7176051e4..92860772a 100644 --- a/crates/brk_types/src/lib.rs +++ b/crates/brk_types/src/lib.rs @@ -69,8 +69,8 @@ mod mempoolentryinfo; mod mempoolinfo; mod metric; mod metriccount; -mod metricparam; mod metricdata; +mod metricparam; mod metrics; mod metricselection; mod metricselectionlegacy; @@ -114,7 +114,6 @@ mod recommendedfees; mod rewardstats; mod sats; mod semesterindex; -mod startheightparam; mod stored_bool; mod stored_f32; mod stored_f64; @@ -222,8 +221,8 @@ pub use mempoolentryinfo::*; pub use mempoolinfo::*; pub use metric::*; pub use metriccount::*; -pub use metricparam::*; pub use metricdata::*; +pub use metricparam::*; pub use metrics::*; pub use metricselection::*; pub use metricselectionlegacy::*; @@ -267,7 +266,6 @@ pub use recommendedfees::*; pub use rewardstats::*; pub use sats::*; pub use semesterindex::*; -pub use startheightparam::*; pub use stored_bool::*; pub use stored_f32::*; pub use stored_f64::*; diff --git a/crates/brk_types/src/metricdata.rs b/crates/brk_types/src/metricdata.rs index 239e8e90b..cfeed0205 100644 --- a/crates/brk_types/src/metricdata.rs +++ b/crates/brk_types/src/metricdata.rs @@ -10,8 +10,8 @@ use vecdb::AnySerializableVec; /// This type is not instantiated - use `MetricData::serialize()` to write JSON bytes directly. #[derive(JsonSchema)] pub struct MetricData { - /// Number of data points returned - pub len: usize, + /// Total number of data points in the metric + pub total: usize, /// Start index (inclusive) of the returned range pub from: usize, /// End index (exclusive) of the returned range @@ -21,21 +21,20 @@ pub struct MetricData { } impl MetricData { - /// Write metric data as JSON to buffer: `{"len":N,"from":N,"to":N,"data":[...]}` + /// Write metric data as JSON to buffer: `{"total":N,"from":N,"to":N,"data":[...]}` pub fn serialize( vec: &dyn AnySerializableVec, from: Option, to: Option, buf: &mut Vec, ) -> vecdb::Result<()> { - let len = vec.len(); + let total = vec.len(); let from_idx = from.unwrap_or(0); - let to_idx = to.unwrap_or(len).min(len); - let range_len = to_idx.saturating_sub(from_idx); + let to_idx = to.unwrap_or(total).min(total); write!( buf, - r#"{{"len":{range_len},"from":{from_idx},"to":{to_idx},"data":"# + r#"{{"total":{total},"from":{from_idx},"to":{to_idx},"data":"# )?; vec.write_json(from, to, buf)?; buf.push(b'}'); diff --git a/crates/brk_types/src/poolslug.rs b/crates/brk_types/src/poolslug.rs index af36905eb..54f61dff3 100644 --- a/crates/brk_types/src/poolslug.rs +++ b/crates/brk_types/src/poolslug.rs @@ -7,6 +7,7 @@ use vecdb::{Bytes, Formattable}; // Slug of a mining pool #[allow(clippy::upper_case_acronyms)] #[derive( + Default, Debug, Copy, Display, @@ -184,103 +185,201 @@ pub enum PoolSlug { InnopolisTech, BtcLab, Parasite, + #[schemars(skip)] Dummy158, + #[schemars(skip)] Dummy159, + #[schemars(skip)] Dummy160, + #[schemars(skip)] Dummy161, + #[schemars(skip)] Dummy162, + #[schemars(skip)] Dummy163, + #[schemars(skip)] Dummy164, + #[schemars(skip)] Dummy165, + #[schemars(skip)] Dummy166, + #[schemars(skip)] Dummy167, + #[schemars(skip)] Dummy168, + #[schemars(skip)] Dummy169, + #[schemars(skip)] Dummy170, + #[schemars(skip)] Dummy171, + #[schemars(skip)] Dummy172, + #[schemars(skip)] Dummy173, + #[schemars(skip)] Dummy174, + #[schemars(skip)] Dummy175, + #[schemars(skip)] Dummy176, + #[schemars(skip)] Dummy177, + #[schemars(skip)] Dummy178, + #[schemars(skip)] Dummy179, + #[schemars(skip)] Dummy180, + #[schemars(skip)] Dummy181, + #[schemars(skip)] Dummy182, + #[schemars(skip)] Dummy183, + #[schemars(skip)] Dummy184, + #[schemars(skip)] Dummy185, + #[schemars(skip)] Dummy186, + #[schemars(skip)] Dummy187, + #[schemars(skip)] Dummy188, + #[schemars(skip)] Dummy189, + #[schemars(skip)] Dummy190, + #[schemars(skip)] Dummy191, + #[schemars(skip)] Dummy192, + #[schemars(skip)] Dummy193, + #[schemars(skip)] Dummy194, + #[schemars(skip)] Dummy195, + #[schemars(skip)] Dummy196, + #[schemars(skip)] Dummy197, + #[schemars(skip)] Dummy198, + #[schemars(skip)] Dummy199, + #[schemars(skip)] Dummy200, + #[schemars(skip)] Dummy201, + #[schemars(skip)] Dummy202, + #[schemars(skip)] Dummy203, + #[schemars(skip)] Dummy204, + #[schemars(skip)] Dummy205, + #[schemars(skip)] Dummy206, + #[schemars(skip)] Dummy207, + #[schemars(skip)] Dummy208, + #[schemars(skip)] Dummy209, + #[schemars(skip)] Dummy210, + #[schemars(skip)] Dummy211, + #[schemars(skip)] Dummy212, + #[schemars(skip)] Dummy213, + #[schemars(skip)] Dummy214, + #[schemars(skip)] Dummy215, + #[schemars(skip)] Dummy216, + #[schemars(skip)] Dummy217, + #[schemars(skip)] Dummy218, + #[schemars(skip)] Dummy219, + #[schemars(skip)] Dummy220, + #[schemars(skip)] Dummy221, + #[schemars(skip)] Dummy222, + #[schemars(skip)] Dummy223, + #[schemars(skip)] Dummy224, + #[schemars(skip)] Dummy225, + #[schemars(skip)] Dummy226, + #[schemars(skip)] Dummy227, + #[schemars(skip)] Dummy228, + #[schemars(skip)] Dummy229, + #[schemars(skip)] Dummy230, + #[schemars(skip)] Dummy231, + #[schemars(skip)] Dummy232, + #[schemars(skip)] Dummy233, + #[schemars(skip)] Dummy234, + #[schemars(skip)] Dummy235, + #[schemars(skip)] Dummy236, + #[schemars(skip)] Dummy237, + #[schemars(skip)] Dummy238, + #[schemars(skip)] Dummy239, + #[schemars(skip)] Dummy240, + #[schemars(skip)] Dummy241, + #[schemars(skip)] Dummy242, + #[schemars(skip)] Dummy243, + #[schemars(skip)] Dummy244, + #[schemars(skip)] Dummy245, + #[schemars(skip)] Dummy246, + #[schemars(skip)] Dummy247, + #[schemars(skip)] Dummy248, + #[schemars(skip)] Dummy249, + #[schemars(skip)] Dummy250, + #[schemars(skip)] Dummy251, + #[schemars(skip)] Dummy252, + #[schemars(skip)] Dummy253, + #[schemars(skip)] Dummy254, + #[schemars(skip)] Dummy255, } diff --git a/crates/brk_types/src/startheightparam.rs b/crates/brk_types/src/startheightparam.rs deleted file mode 100644 index b09e222ab..000000000 --- a/crates/brk_types/src/startheightparam.rs +++ /dev/null @@ -1,11 +0,0 @@ -use schemars::JsonSchema; -use serde::Deserialize; - -use crate::Height; - -#[derive(Deserialize, JsonSchema)] -pub struct StartHeightParam { - /// Starting block height (optional, defaults to latest) - #[schemars(example = 800000)] - pub start_height: Option, -} diff --git a/crates/brk_types/src/timeperiod.rs b/crates/brk_types/src/timeperiod.rs index 852a7c477..fbf0ffee9 100644 --- a/crates/brk_types/src/timeperiod.rs +++ b/crates/brk_types/src/timeperiod.rs @@ -5,35 +5,26 @@ use serde::{Deserialize, Serialize}; /// /// Used to specify the lookback window for pool statistics, hashrate calculations, /// and other time-based mining metrics. -/// -/// - `Day` (alias: 24h) - Last 24 hours (~144 blocks) -/// - `ThreeDays` (alias: 3d) - Last 3 days (~432 blocks) -/// - `Week` (alias: 1w) - Last week (~1008 blocks) -/// - `Month` (alias: 1m) - Last month (~4320 blocks) -/// - `ThreeMonths` (alias: 3m) - Last 3 months (~12960 blocks) -/// - `SixMonths` (alias: 6m) - Last 6 months (~25920 blocks) -/// - `Year` (alias: 1y) - Last year (~52560 blocks) -/// - `TwoYears` (alias: 2y) - Last 2 years (~105120 blocks) -/// - `ThreeYears` (alias: 3y) - Last 3 years (~157680 blocks) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub enum TimePeriod { - #[serde(alias = "24h")] + #[default] + #[serde(rename = "24h")] Day, - #[serde(alias = "3d")] + #[serde(rename = "3d")] ThreeDays, - #[serde(alias = "1w")] + #[serde(rename = "1w")] Week, - #[serde(alias = "1m")] + #[serde(rename = "1m")] Month, - #[serde(alias = "3m")] + #[serde(rename = "3m")] ThreeMonths, - #[serde(alias = "6m")] + #[serde(rename = "6m")] SixMonths, - #[serde(alias = "1y")] + #[serde(rename = "1y")] Year, - #[serde(alias = "2y")] + #[serde(rename = "2y")] TwoYears, - #[serde(alias = "3y")] + #[serde(rename = "3y")] ThreeYears, } diff --git a/crates/brk_types/src/timeperiodparam.rs b/crates/brk_types/src/timeperiodparam.rs index e50113c61..08140cc4f 100644 --- a/crates/brk_types/src/timeperiodparam.rs +++ b/crates/brk_types/src/timeperiodparam.rs @@ -5,5 +5,6 @@ use super::TimePeriod; #[derive(Deserialize, JsonSchema)] pub struct TimePeriodParam { + #[schemars(example = &"24h")] pub time_period: TimePeriod, } diff --git a/crates/brk_types/src/validateaddressparam.rs b/crates/brk_types/src/validateaddressparam.rs index 490456dea..0541c04c7 100644 --- a/crates/brk_types/src/validateaddressparam.rs +++ b/crates/brk_types/src/validateaddressparam.rs @@ -4,5 +4,11 @@ use serde::Deserialize; #[derive(Deserialize, JsonSchema)] pub struct ValidateAddressParam { /// Bitcoin address to validate (can be any string) + #[schemars( + example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + example = &"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + example = &"not-a-valid-address", + example = &"bc1qinvalid" + )] pub address: String, }