mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-11 07:23:32 -07:00
server: openapi fixes
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<Output> {
|
||||
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<Output> {
|
||||
// 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<Vec<&'static dyn AnyExportableVec>> {
|
||||
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<Output> {
|
||||
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<Output> {
|
||||
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"
|
||||
|
||||
@@ -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<OutputLegacy> {
|
||||
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<OutputLegacy> {
|
||||
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"
|
||||
|
||||
@@ -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<Vec<BlockFeeRatesEntry>> {
|
||||
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<Vec<BlockFeeRatesEntry>> {
|
||||
// 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(),
|
||||
// ),
|
||||
// })
|
||||
// }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<AppState> {
|
||||
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<AppState>| {
|
||||
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::<Vec<BlockInfo>>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}",
|
||||
@@ -93,18 +104,18 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/blocks/{start_height}",
|
||||
"/api/blocks/{height}",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
Path(path): Path<StartHeightParam>,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
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::<Vec<BlockInfo>>()
|
||||
.not_modified()
|
||||
|
||||
@@ -41,11 +41,11 @@ async fn req_to_response_res(
|
||||
) -> brk_error::Result<Response> {
|
||||
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();
|
||||
|
||||
@@ -43,11 +43,11 @@ async fn req_to_response_res(
|
||||
) -> brk_error::Result<Response> {
|
||||
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();
|
||||
|
||||
@@ -43,11 +43,11 @@ async fn req_to_response_res(
|
||||
) -> brk_error::Result<Response> {
|
||||
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();
|
||||
|
||||
@@ -193,27 +193,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
// !!!
|
||||
// DEPRECATED: Do not use
|
||||
// !!!
|
||||
.route(
|
||||
"/api/vecs/query",
|
||||
get(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<MetricSelectionLegacy>,
|
||||
state: State<AppState>|
|
||||
-> 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<String>,
|
||||
@@ -236,6 +218,41 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
));
|
||||
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::<serde_json::Value>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/vecs/query",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<MetricSelectionLegacy>,
|
||||
state: State<AppState>|
|
||||
-> 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::<serde_json::Value>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/fee-rates/{time_period}",
|
||||
get_with(
|
||||
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
|
||||
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::<Vec<BlockFeeRatesEntry>>()
|
||||
.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<TimePeriodParam>, State(state): State<AppState>| {
|
||||
// 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::<Vec<BlockFeeRatesEntry>>()
|
||||
// .not_modified()
|
||||
// .server_error()
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/sizes-weights/{time_period}",
|
||||
get_with(
|
||||
|
||||
@@ -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<R>(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<R, F>(self, f: F) -> Self
|
||||
where
|
||||
R: JsonSchema,
|
||||
|
||||
@@ -13,7 +13,8 @@ use vecdb::{Bytes, Formattable};
|
||||
#[schemars(
|
||||
transparent,
|
||||
with = "String",
|
||||
example = &"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
||||
example = &"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
|
||||
example = &"0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5"
|
||||
)]
|
||||
pub struct BlockHash([u8; 32]);
|
||||
|
||||
|
||||
@@ -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<i64>,
|
||||
|
||||
/// 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<i64>,
|
||||
|
||||
/// 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<usize>,
|
||||
@@ -38,23 +38,34 @@ impl DataRange {
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the raw `from` value
|
||||
pub fn from(&self) -> Option<i64> {
|
||||
self.from
|
||||
}
|
||||
|
||||
pub fn to(&self) -> Option<i64> {
|
||||
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<i64> {
|
||||
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<i64>, 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<usize>,
|
||||
to: Option<usize>,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> 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'}');
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Height>,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@ use super::TimePeriod;
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct TimePeriodParam {
|
||||
#[schemars(example = &"24h")]
|
||||
pub time_period: TimePeriod,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user