global: added support for oracle histograms

This commit is contained in:
nym21
2026-05-25 16:44:09 +02:00
parent ee20175cbf
commit 66037c862f
18 changed files with 493 additions and 265 deletions
+4 -3
View File
@@ -184,9 +184,10 @@ All errors return structured JSON with a consistent format:
transaction outputs, with no external price feed. Payment activity is binned on a \
log scale, and a smoothed EMA over recent blocks locates the price.\n\n\
Histograms come in two flavors, each available at the live tip (mempool-blended) \
or at any confirmed height: `raw` (per-block counts) and `ema` (the smoothed \
window). The live price is also at `/api/mempool/price`. Confirmed per-height \
price history is at `/api/vecs/height-to-price`."
or at any confirmed height: `raw` bins every output by value with no filtering, \
while `ema` is the smoothed round-dollar window the price is read from. The live \
price is also at `/api/mempool/price`. Confirmed per-height price history is at \
`/api/vecs/height-to-price`."
.to_string(),
),
..Default::default()
+61 -26
View File
@@ -2,14 +2,15 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::{HeaderMap, Uri},
response::IntoResponse,
};
use brk_oracle::{HistogramEmaCompact, HistogramRaw};
use brk_types::{Dollars, Version};
use brk_types::{Day1, Dollars, Version};
use crate::{
AppState,
extended::TransformResponseExtended,
params::{Empty, HeightParam},
params::{Empty, HeightOrDate, HeightOrDateParam},
};
pub trait OracleRoutes {
@@ -67,26 +68,42 @@ impl OracleRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/oracle/histogram/ema/{height}",
"/api/oracle/histogram/ema/{point}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
Path(path): Path<HeightOrDateParam>,
_: Empty,
State(state): State<AppState>| {
let strategy = state.height_strategy(Version::new(brk_oracle::VERSION), path.height);
state
.respond_json(&headers, strategy, &uri, move |q| {
q.confirmed_histogram_ema(usize::from(path.height))
})
.await
let version = Version::new(brk_oracle::VERSION);
match path.resolve() {
Ok(HeightOrDate::Date(date)) => {
let strategy = state.date_strategy(version, date);
state
.respond_json(&headers, strategy, &uri, move |q| {
q.confirmed_histogram_ema_day(Day1::try_from(date)?)
})
.await
}
Ok(HeightOrDate::Height(height)) => {
let strategy = state.height_strategy(version, height);
state
.respond_json(&headers, strategy, &uri, move |q| {
q.confirmed_histogram_ema(usize::from(height))
})
.await
}
Err(e) => e.into_response(),
}
},
|op| {
op.id("get_oracle_histogram_ema")
.oracle_tag()
.summary("EMA histogram at height")
.summary("EMA histogram at height or day")
.description(
"Smoothed round-dollar payment histogram for a confirmed height. \
"Smoothed round-dollar payment histogram for a confirmed point: a \
block height (`840000`) gives that block's EMA, a calendar date \
(`YYYY-MM-DD`) gives the average of that day's per-block EMAs. \
A flat array of log-scale bins.",
)
.json_response::<HistogramEmaCompact>()
@@ -112,9 +129,10 @@ impl OracleRoutes for ApiRouter<AppState> {
.oracle_tag()
.summary("Live raw histogram")
.description(
"Un-smoothed per-block round-dollar counts for the forming mempool \
block. A flat array of log-scale bins, all zero when no mempool is \
configured.",
"Unfiltered output histogram for the forming mempool block: every \
live output binned by value, with none of the round-dollar payment \
filters applied. A flat array of log-scale bins, all zero when no \
mempool is configured.",
)
.json_response::<HistogramRaw>()
.not_modified()
@@ -123,27 +141,44 @@ impl OracleRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/oracle/histogram/raw/{height}",
"/api/oracle/histogram/raw/{point}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
Path(path): Path<HeightOrDateParam>,
_: Empty,
State(state): State<AppState>| {
let strategy = state.height_strategy(Version::new(brk_oracle::VERSION), path.height);
state
.respond_json(&headers, strategy, &uri, move |q| {
q.confirmed_histogram_raw(usize::from(path.height))
})
.await
let version = Version::new(brk_oracle::VERSION);
match path.resolve() {
Ok(HeightOrDate::Date(date)) => {
let strategy = state.date_strategy(version, date);
state
.respond_json(&headers, strategy, &uri, move |q| {
q.confirmed_histogram_raw_day(Day1::try_from(date)?)
})
.await
}
Ok(HeightOrDate::Height(height)) => {
let strategy = state.height_strategy(version, height);
state
.respond_json(&headers, strategy, &uri, move |q| {
q.confirmed_histogram_raw(usize::from(height))
})
.await
}
Err(e) => e.into_response(),
}
},
|op| {
op.id("get_oracle_histogram_raw")
.oracle_tag()
.summary("Raw histogram at height")
.summary("Raw histogram at height or day")
.description(
"Un-smoothed round-dollar counts for a single confirmed block. A \
flat array of log-scale bins.",
"Unfiltered output histogram for a confirmed point: a block height \
(`840000`) gives that block's outputs, coinbase included, binned by \
value with no payment filtering; a calendar date (`YYYY-MM-DD`) sums \
every block that day. A flat array of log-scale bins.",
)
.json_response::<HistogramRaw>()
.not_modified()
+4 -3
View File
@@ -16,7 +16,8 @@ use brk_query::{Query as BrkQuery, ResolvedQuery};
use brk_traversable::TreeNode;
use brk_types::{
DataRangeFormat, Format, IndexInfo, Output, PaginatedSeries, Pagination, SearchQuery,
SeriesCount, SeriesData, SeriesInfo, SeriesNameWithIndex, SeriesSelection,
SeriesCount, SeriesData, SeriesInfo, SeriesNameWithIndex, SeriesOutput, SeriesSelection,
Version,
};
use crate::{
@@ -67,7 +68,7 @@ pub(super) async fn serve(
.await)
}
fn output_to_bytes(out: brk_types::SeriesOutput) -> BrkResult<Bytes> {
fn output_to_bytes(out: SeriesOutput) -> BrkResult<Bytes> {
Ok(match out.output {
Output::CSV(s) => Bytes::from(s),
Output::Json(v) => Bytes::from(v),
@@ -365,7 +366,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
.series_tag()
.summary("Get series version")
.description("Returns the current version of a series. Changes when the series data is updated.")
.json_response::<brk_types::Version>()
.json_response::<Version>()
.not_modified()
.not_found(),
),
@@ -0,0 +1,38 @@
use brk_types::{Date, Height};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Error;
/// Path parameter accepting either a block height (`840000`) or a calendar date
/// (`YYYY-MM-DD`). The handler resolves it and dispatches to the per-height or
/// per-day variant, choosing the matching cache strategy.
#[derive(Deserialize, JsonSchema)]
pub struct HeightOrDateParam {
#[schemars(example = &"840000")]
pub point: String,
}
/// A resolved [`HeightOrDateParam`]: a confirmed block height or a calendar day.
pub enum HeightOrDate {
Height(Height),
Date(Date),
}
impl HeightOrDateParam {
/// Parses the raw `point`: a `YYYY-MM-DD` string is a [`Date`], an all-digit
/// string is a [`Height`], anything else is a 400. Dates are tried first
/// because their dashes keep them from parsing as a height.
pub fn resolve(&self) -> Result<HeightOrDate, Error> {
if let Ok(date) = self.point.parse::<Date>() {
Ok(HeightOrDate::Date(date))
} else if let Ok(height) = self.point.parse::<usize>() {
Ok(HeightOrDate::Height(Height::from(height)))
} else {
Err(Error::bad_request(format!(
"expected a block height or YYYY-MM-DD date, got `{}`",
self.point
)))
}
}
}
+2
View File
@@ -5,6 +5,7 @@ mod blockhash_param;
mod blockhash_start_index;
mod blockhash_tx_index;
mod empty;
mod height_or_date_param;
mod height_param;
mod next_block_hash_param;
mod pool_slug_param;
@@ -25,6 +26,7 @@ pub use blockhash_param::*;
pub use blockhash_start_index::*;
pub use blockhash_tx_index::*;
pub use empty::*;
pub use height_or_date_param::*;
pub use height_param::*;
pub use next_block_hash_param::*;
pub use pool_slug_param::*;