mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-15 09:13:36 -07:00
global: added support for oracle histograms
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user