mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-02 11:13:39 -07:00
global: metrics -> series rename
This commit is contained in:
+165
-139
@@ -10,55 +10,52 @@ use axum::{
|
||||
use brk_traversable::TreeNode;
|
||||
use brk_types::{
|
||||
CostBasisCohortParam, CostBasisFormatted, CostBasisParams, CostBasisQuery, DataRangeFormat,
|
||||
Date, Index, IndexInfo, Metric, MetricCount, MetricData, MetricInfo, MetricParam,
|
||||
MetricSelection, MetricSelectionLegacy, MetricWithIndex, Metrics, PaginatedMetrics, Pagination,
|
||||
SearchQuery,
|
||||
Date, DetailedSeriesCount, Index, IndexInfo, PaginatedSeries, Pagination, SearchQuery, Series,
|
||||
SeriesData, SeriesInfo, SeriesList, SeriesSelection, SeriesSelectionLegacy,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{CacheStrategy, Error, extended::TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
use super::series::legacy;
|
||||
|
||||
mod bulk;
|
||||
mod data;
|
||||
mod legacy;
|
||||
|
||||
/// Maximum allowed request weight in bytes (650KB)
|
||||
const MAX_WEIGHT: usize = 65 * 10_000;
|
||||
/// Maximum allowed request weight for localhost (50MB)
|
||||
const MAX_WEIGHT_LOCALHOST: usize = 50 * 1_000_000;
|
||||
/// Cache control header for metric data responses
|
||||
const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate";
|
||||
|
||||
/// Returns the max weight for a request based on the client address.
|
||||
/// Localhost requests get a generous limit, external requests get a stricter one.
|
||||
fn max_weight(addr: &SocketAddr) -> usize {
|
||||
if addr.ip().is_loopback() {
|
||||
MAX_WEIGHT_LOCALHOST
|
||||
} else {
|
||||
MAX_WEIGHT
|
||||
}
|
||||
/// Legacy path parameter for `/api/metric/{metric}`
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct LegacySeriesParam {
|
||||
metric: Series,
|
||||
}
|
||||
|
||||
pub trait ApiMetricsRoutes {
|
||||
fn add_metrics_routes(self) -> Self;
|
||||
/// Legacy path parameters for `/api/metric/{metric}/{index}`
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
|
||||
struct LegacySeriesWithIndex {
|
||||
metric: Series,
|
||||
index: Index,
|
||||
}
|
||||
|
||||
impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
fn add_metrics_routes(self) -> Self {
|
||||
self.api_route(
|
||||
pub trait ApiMetricsLegacyRoutes {
|
||||
fn add_metrics_legacy_routes(self) -> Self;
|
||||
}
|
||||
|
||||
impl ApiMetricsLegacyRoutes for ApiRouter<AppState> {
|
||||
fn add_metrics_legacy_routes(self) -> Self {
|
||||
self
|
||||
// --- Deprecated /api/metrics routes ---
|
||||
.api_route(
|
||||
"/api/metrics",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metrics_catalog().clone())).await
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_catalog().clone())).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_metrics_tree")
|
||||
.id("get_metrics_tree_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Metrics catalog")
|
||||
.deprecated()
|
||||
.summary("Metrics catalog (deprecated)")
|
||||
.description(
|
||||
"Returns the complete hierarchical catalog of available metrics organized as a tree structure. \
|
||||
Metrics are grouped by categories and subcategories."
|
||||
"**DEPRECATED** - Use `/api/series` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<TreeNode>()
|
||||
.not_modified(),
|
||||
@@ -72,14 +69,18 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metric_count())).await
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_count())).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_metrics_count")
|
||||
.id("get_metrics_count_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Metric count")
|
||||
.description("Returns the number of metrics available per index type.")
|
||||
.ok_response::<Vec<MetricCount>>()
|
||||
.deprecated()
|
||||
.summary("Metric count (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/count` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<DetailedSeriesCount>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
@@ -94,11 +95,13 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.indexes().to_vec())).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_indexes")
|
||||
.id("get_indexes_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("List available indexes")
|
||||
.deprecated()
|
||||
.summary("List available indexes (deprecated)")
|
||||
.description(
|
||||
"Returns all available indexes with their accepted query aliases. Use any alias when querying metrics."
|
||||
"**DEPRECATED** - Use `/api/series/indexes` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<Vec<IndexInfo>>()
|
||||
.not_modified(),
|
||||
@@ -113,14 +116,18 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
State(state): State<AppState>,
|
||||
Query(pagination): Query<Pagination>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.metrics(pagination))).await
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.series_list(pagination))).await
|
||||
},
|
||||
|op| op
|
||||
.id("list_metrics")
|
||||
.id("list_metrics_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Metrics list")
|
||||
.description("Paginated flat list of all available metric names. Use `page` query param for pagination.")
|
||||
.ok_response::<PaginatedMetrics>()
|
||||
.deprecated()
|
||||
.summary("Metrics list (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/list` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<PaginatedSeries>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
@@ -133,18 +140,45 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<SearchQuery>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.search_metrics(&query))).await
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.search_series(&query))).await
|
||||
},
|
||||
|op| op
|
||||
.id("search_metrics")
|
||||
.id("search_metrics_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Search metrics")
|
||||
.description("Fuzzy search for metrics by name. Supports partial matches and typos.")
|
||||
.ok_response::<Vec<Metric>>()
|
||||
.deprecated()
|
||||
.summary("Search metrics (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/search` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<Vec<&str>>()
|
||||
.not_modified()
|
||||
.server_error(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/bulk",
|
||||
get_with(
|
||||
|uri: Uri, headers: HeaderMap, addr: Extension<SocketAddr>, query: Query<SeriesSelection>, state: State<AppState>| async move {
|
||||
legacy::handler(uri, headers, addr, query, state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metrics_bulk_deprecated")
|
||||
.metrics_tag()
|
||||
.deprecated()
|
||||
.summary("Bulk metric data (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/bulk` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<Vec<SeriesData>>()
|
||||
.csv_response()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
// --- Deprecated /api/metric/{metric} routes ---
|
||||
.api_route(
|
||||
"/api/metric/{metric}",
|
||||
get_with(
|
||||
@@ -152,20 +186,22 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<MetricParam>
|
||||
Path(path): Path<LegacySeriesParam>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
|
||||
q.metric_info(&path.metric).ok_or_else(|| q.metric_not_found_error(&path.metric))
|
||||
q.series_info(&path.metric).ok_or_else(|| q.series_not_found_error(&path.metric))
|
||||
}).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric_info")
|
||||
.id("get_metric_info_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Get metric info")
|
||||
.deprecated()
|
||||
.summary("Get metric info (deprecated)")
|
||||
.description(
|
||||
"Returns the supported indexes and value type for the specified metric."
|
||||
"**DEPRECATED** - Use `/api/series/{series}` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<MetricInfo>()
|
||||
.ok_response::<SeriesInfo>()
|
||||
.not_modified()
|
||||
.not_found()
|
||||
.server_error(),
|
||||
@@ -178,28 +214,24 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>,
|
||||
Path(path): Path<LegacySeriesWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(MetricSelection::from((path.index, path.metric, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
let params = SeriesSelection::from((path.index, path.metric, range));
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric")
|
||||
.id("get_metric_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Get metric data")
|
||||
.deprecated()
|
||||
.summary("Get metric data (deprecated)")
|
||||
.description(
|
||||
"Fetch data for a specific metric at the given index. \
|
||||
Use query parameters to filter by date range and format (json/csv)."
|
||||
"**DEPRECATED** - Use `/api/series/{series}/{index}` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<MetricData>()
|
||||
.ok_response::<SeriesData>()
|
||||
.csv_response()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
@@ -212,26 +244,22 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>,
|
||||
Path(path): Path<LegacySeriesWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::raw_handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(MetricSelection::from((path.index, path.metric, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
let params = SeriesSelection::from((path.index, path.metric, range));
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric_data")
|
||||
.id("get_metric_data_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Get raw metric data")
|
||||
.deprecated()
|
||||
.summary("Get raw metric data (deprecated)")
|
||||
.description(
|
||||
"Returns just the data array without the MetricData wrapper. \
|
||||
Supports the same range and format parameters as the standard endpoint."
|
||||
"**DEPRECATED** - Use `/api/series/{series}/{index}/data` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<Vec<serde_json::Value>>()
|
||||
.csv_response()
|
||||
@@ -245,7 +273,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>| {
|
||||
Path(path): Path<LegacySeriesWithIndex>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
|
||||
q.latest(&path.metric, path.index)
|
||||
@@ -253,11 +281,13 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.await
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric_latest")
|
||||
.id("get_metric_latest_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Get latest metric value")
|
||||
.deprecated()
|
||||
.summary("Get latest metric value (deprecated)")
|
||||
.description(
|
||||
"Returns the single most recent value for a metric, unwrapped (not inside a MetricData object)."
|
||||
"**DEPRECATED** - Use `/api/series/{series}/{index}/latest` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<serde_json::Value>()
|
||||
.not_found(),
|
||||
@@ -269,7 +299,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>| {
|
||||
Path(path): Path<LegacySeriesWithIndex>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
|
||||
q.len(&path.metric, path.index)
|
||||
@@ -277,10 +307,14 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.await
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric_len")
|
||||
.id("get_metric_len_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Get metric data length")
|
||||
.description("Returns the total number of data points for a metric at the given index.")
|
||||
.deprecated()
|
||||
.summary("Get metric data length (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/{series}/{index}/len` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<usize>()
|
||||
.not_found(),
|
||||
),
|
||||
@@ -291,7 +325,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>| {
|
||||
Path(path): Path<LegacySeriesWithIndex>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
|
||||
q.version(&path.metric, path.index)
|
||||
@@ -299,34 +333,19 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.await
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric_version")
|
||||
.id("get_metric_version_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Get metric version")
|
||||
.description("Returns the current version of a metric. Changes when the metric data is updated.")
|
||||
.deprecated()
|
||||
.summary("Get metric version (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/{series}/{index}/version` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<brk_types::Version>()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/bulk",
|
||||
get_with(
|
||||
|uri, headers, addr, query, state| async move {
|
||||
bulk::handler(uri, headers, addr, query, state).await.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metrics")
|
||||
.metrics_tag()
|
||||
.summary("Bulk metric data")
|
||||
.description(
|
||||
"Fetch multiple metrics in a single request. Supports filtering by index and date range. \
|
||||
Returns an array of MetricData objects. For a single metric, use `get_metric` instead."
|
||||
)
|
||||
.ok_response::<Vec<MetricData>>()
|
||||
.csv_response()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
// Cost basis distribution endpoints
|
||||
// --- Deprecated cost basis routes ---
|
||||
.api_route(
|
||||
"/api/metrics/cost-basis",
|
||||
get_with(
|
||||
@@ -336,10 +355,14 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_cost_basis_cohorts")
|
||||
op.id("get_cost_basis_cohorts_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Available cost basis cohorts")
|
||||
.description("List available cohorts for cost basis distribution.")
|
||||
.deprecated()
|
||||
.summary("Available cost basis cohorts (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/cost-basis` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<Vec<String>>()
|
||||
.server_error()
|
||||
},
|
||||
@@ -359,10 +382,14 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_cost_basis_dates")
|
||||
op.id("get_cost_basis_dates_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Available cost basis dates")
|
||||
.description("List available dates for a cohort's cost basis distribution.")
|
||||
.deprecated()
|
||||
.summary("Available cost basis dates (deprecated)")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/series/cost-basis/{cohort}/dates` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<Vec<Date>>()
|
||||
.not_found()
|
||||
.server_error()
|
||||
@@ -389,14 +416,13 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_cost_basis")
|
||||
op.id("get_cost_basis_deprecated")
|
||||
.metrics_tag()
|
||||
.summary("Cost basis distribution")
|
||||
.deprecated()
|
||||
.summary("Cost basis distribution (deprecated)")
|
||||
.description(
|
||||
"Get the cost basis distribution for a cohort on a specific date.\n\n\
|
||||
Query params:\n\
|
||||
- `bucket`: raw (default), lin200, lin500, lin1000, log10, log50, log100\n\
|
||||
- `value`: supply (default, in BTC), realized (USD), unrealized (USD)",
|
||||
"**DEPRECATED** - Use `/api/series/cost-basis/{cohort}/{date}` instead.\n\n\
|
||||
Sunset date: 2027-01-01."
|
||||
)
|
||||
.ok_response::<CostBasisFormatted>()
|
||||
.not_found()
|
||||
@@ -404,7 +430,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
// Deprecated endpoints
|
||||
// --- Deprecated /api/vecs/ routes (moved from series module) ---
|
||||
.api_route(
|
||||
"/api/vecs/{variant}",
|
||||
get_with(
|
||||
@@ -426,9 +452,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
).into_response();
|
||||
};
|
||||
|
||||
let params = MetricSelection::from((
|
||||
let params = SeriesSelection::from((
|
||||
index,
|
||||
Metrics::from(split.collect::<Vec<_>>().join(separator)),
|
||||
SeriesList::from(split.collect::<Vec<_>>().join(separator)),
|
||||
range,
|
||||
));
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
@@ -439,10 +465,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.metrics_tag()
|
||||
.summary("Legacy variant endpoint")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` instead.\n\n\
|
||||
"**DEPRECATED** - Use `/api/series/{series}/{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., `day1_to_price`). \
|
||||
Returns raw data without the MetricData wrapper."
|
||||
Legacy endpoint for querying series by variant path (e.g., `day1_to_price`). \
|
||||
Returns raw data without the SeriesData wrapper."
|
||||
)
|
||||
.deprecated()
|
||||
.ok_response::<serde_json::Value>()
|
||||
@@ -455,10 +481,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
Query(params): Query<MetricSelectionLegacy>,
|
||||
Query(params): Query<SeriesSelectionLegacy>,
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
let params: MetricSelection = params.into();
|
||||
let params: SeriesSelection = params.into();
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
@@ -467,9 +493,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.metrics_tag()
|
||||
.summary("Legacy query endpoint")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` or `/api/metrics/bulk` instead.\n\n\
|
||||
"**DEPRECATED** - Use `/api/series/{series}/{index}` or `/api/series/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."
|
||||
Legacy endpoint for querying series. Returns raw data without the SeriesData wrapper."
|
||||
)
|
||||
.deprecated()
|
||||
.ok_response::<serde_json::Value>()
|
||||
@@ -15,8 +15,8 @@ use crate::{
|
||||
Error,
|
||||
api::{
|
||||
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
|
||||
metrics::ApiMetricsRoutes, mining::MiningRoutes, server::ServerRoutes,
|
||||
transactions::TxRoutes,
|
||||
metrics_legacy::ApiMetricsLegacyRoutes, mining::MiningRoutes,
|
||||
series::ApiSeriesRoutes, server::ServerRoutes, transactions::TxRoutes,
|
||||
},
|
||||
extended::{ResponseExtended, TransformResponseExtended},
|
||||
};
|
||||
@@ -26,7 +26,8 @@ use super::AppState;
|
||||
mod addresses;
|
||||
mod blocks;
|
||||
mod mempool;
|
||||
mod metrics;
|
||||
mod metrics_legacy;
|
||||
mod series;
|
||||
mod mining;
|
||||
mod openapi;
|
||||
mod server;
|
||||
@@ -41,7 +42,8 @@ pub trait ApiRoutes {
|
||||
impl ApiRoutes for ApiRouter<AppState> {
|
||||
fn add_api_routes(self) -> Self {
|
||||
self.add_server_routes()
|
||||
.add_metrics_routes()
|
||||
.add_series_routes()
|
||||
.add_metrics_legacy_routes()
|
||||
.add_block_routes()
|
||||
.add_tx_routes()
|
||||
.add_addresses_routes()
|
||||
|
||||
@@ -22,12 +22,12 @@ pub fn create_openapi() -> OpenApi {
|
||||
let info = Info {
|
||||
title: "Bitcoin Research Kit".to_string(),
|
||||
description: Some(
|
||||
r#"API for querying Bitcoin blockchain data and on-chain metrics.
|
||||
r#"API for querying Bitcoin blockchain data and on-chain series.
|
||||
|
||||
### Features
|
||||
|
||||
- **Metrics**: Thousands of time-series metrics across multiple indexes (date, block height, etc.)
|
||||
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-metrics endpoints follow the mempool.space API format
|
||||
- **Series**: Thousands of time-series across multiple indexes (date, block height, etc.)
|
||||
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-series endpoints follow the mempool.space API format
|
||||
- **Multiple formats**: JSON and CSV output
|
||||
- **LLM-optimized**: [`/llms.txt`](/llms.txt) for discovery, [`/api.json`](/api.json) compact OpenAPI spec for tool use (full spec at [`/openapi.json`](/openapi.json))
|
||||
|
||||
@@ -35,9 +35,9 @@ pub fn create_openapi() -> OpenApi {
|
||||
|
||||
```bash
|
||||
curl -s https://bitview.space/api/block-height/0
|
||||
curl -s https://bitview.space/api/metrics/search?q=price
|
||||
curl -s https://bitview.space/api/metric/price/day
|
||||
curl -s https://bitview.space/api/metric/price/day/latest
|
||||
curl -s https://bitview.space/api/series/search?q=price
|
||||
curl -s https://bitview.space/api/series/price/day
|
||||
curl -s https://bitview.space/api/series/price/day/latest
|
||||
```
|
||||
|
||||
### Errors
|
||||
@@ -48,7 +48,7 @@ All errors return structured JSON with a consistent format:
|
||||
{
|
||||
"error": {
|
||||
"type": "not_found",
|
||||
"code": "metric_not_found",
|
||||
"code": "series_not_found",
|
||||
"message": "'foo' not found, did you mean 'bar'?",
|
||||
"doc_url": "https://bitcoinresearchkit.org/api"
|
||||
}
|
||||
@@ -56,7 +56,7 @@ All errors return structured JSON with a consistent format:
|
||||
```
|
||||
|
||||
- **`type`**: Error category — `invalid_request` (400), `forbidden` (403), `not_found` (404), `unavailable` (503), or `internal` (500)
|
||||
- **`code`**: Machine-readable error code (e.g. `invalid_address`, `metric_not_found`, `weight_exceeded`)
|
||||
- **`code`**: Machine-readable error code (e.g. `invalid_address`, `series_not_found`, `weight_exceeded`)
|
||||
- **`message`**: Human-readable description
|
||||
- **`doc_url`**: Link to API documentation
|
||||
|
||||
@@ -97,13 +97,20 @@ All errors return structured JSON with a consistent format:
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Series".to_string(),
|
||||
description: Some(
|
||||
"Access thousands of Bitcoin network time-series data. Query historical statistics \
|
||||
across various indexes (date, week, month, block height) with JSON or CSV output.\n\n\
|
||||
**Note:** Series names are subject to change while the project is in active development."
|
||||
.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Metrics".to_string(),
|
||||
description: Some(
|
||||
"Access thousands of Bitcoin network metrics and time-series data. Query historical statistics \
|
||||
across various indexes (date, week, month, block height) with JSON or CSV output.\n\n\
|
||||
**Note:** Metric names are subject to change while the project is in active development."
|
||||
.to_string(),
|
||||
"Deprecated — use Series".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
+3
-3
@@ -7,11 +7,11 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_types::{Format, MetricSelection, Output};
|
||||
use brk_types::{Format, Output, SeriesSelection};
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::metrics::{CACHE_CONTROL, max_weight},
|
||||
api::series::{CACHE_CONTROL, max_weight},
|
||||
extended::{ContentEncoding, HeaderMapExtended},
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Extension(addr): Extension<SocketAddr>,
|
||||
Query(params): Query<MetricSelection>,
|
||||
Query(params): Query<SeriesSelection>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Response> {
|
||||
// Phase 1: Search and resolve metadata (cheap)
|
||||
+6
-6
@@ -9,11 +9,11 @@ use axum::{
|
||||
};
|
||||
use brk_error::Result as BrkResult;
|
||||
use brk_query::{Query as BrkQuery, ResolvedQuery};
|
||||
use brk_types::{Format, MetricOutput, MetricSelection, Output};
|
||||
use brk_types::{Format, Output, SeriesOutput, SeriesSelection};
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::metrics::{CACHE_CONTROL, max_weight},
|
||||
api::series::{CACHE_CONTROL, max_weight},
|
||||
extended::{ContentEncoding, HeaderMapExtended},
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
Query(params): Query<MetricSelection>,
|
||||
Query(params): Query<SeriesSelection>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Response> {
|
||||
format_and_respond(uri, headers, addr, params, state, |q, r| q.format(r)).await
|
||||
@@ -33,7 +33,7 @@ pub async fn raw_handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
Query(params): Query<MetricSelection>,
|
||||
Query(params): Query<SeriesSelection>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Response> {
|
||||
format_and_respond(uri, headers, addr, params, state, |q, r| q.format_raw(r)).await
|
||||
@@ -43,9 +43,9 @@ async fn format_and_respond(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Extension(addr): Extension<SocketAddr>,
|
||||
params: MetricSelection,
|
||||
params: SeriesSelection,
|
||||
state: State<AppState>,
|
||||
formatter: fn(&BrkQuery, ResolvedQuery) -> BrkResult<MetricOutput>,
|
||||
formatter: fn(&BrkQuery, ResolvedQuery) -> BrkResult<SeriesOutput>,
|
||||
) -> Result<Response> {
|
||||
// Phase 1: Search and resolve metadata (cheap)
|
||||
let resolved = state
|
||||
+4
-4
@@ -7,15 +7,15 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_types::{Format, MetricSelection, OutputLegacy};
|
||||
use brk_types::{Format, OutputLegacy, SeriesSelection};
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::metrics::{CACHE_CONTROL, max_weight},
|
||||
api::series::{CACHE_CONTROL, max_weight},
|
||||
extended::{ContentEncoding, HeaderMapExtended},
|
||||
};
|
||||
|
||||
const SUNSET: &str = "2027-01-01T00:00:00Z";
|
||||
pub const SUNSET: &str = "2027-01-01T00:00:00Z";
|
||||
|
||||
use super::AppState;
|
||||
|
||||
@@ -23,7 +23,7 @@ pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Extension(addr): Extension<SocketAddr>,
|
||||
Query(params): Query<MetricSelection>,
|
||||
Query(params): Query<SeriesSelection>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Response> {
|
||||
// Phase 1: Search and resolve metadata (cheap)
|
||||
@@ -0,0 +1,407 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_traversable::TreeNode;
|
||||
use brk_types::{
|
||||
CostBasisCohortParam, CostBasisFormatted, CostBasisParams, CostBasisQuery, DataRangeFormat,
|
||||
Date, IndexInfo, PaginatedSeries, Pagination, SearchQuery, SeriesCount, SeriesData,
|
||||
SeriesInfo, SeriesParam, SeriesSelection, SeriesWithIndex,
|
||||
};
|
||||
|
||||
use crate::{CacheStrategy, extended::TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
mod bulk;
|
||||
mod data;
|
||||
pub mod legacy;
|
||||
|
||||
/// Maximum allowed request weight in bytes (650KB)
|
||||
const MAX_WEIGHT: usize = 65 * 10_000;
|
||||
/// Maximum allowed request weight for localhost (50MB)
|
||||
const MAX_WEIGHT_LOCALHOST: usize = 50 * 1_000_000;
|
||||
/// Cache control header for series data responses
|
||||
const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate";
|
||||
|
||||
/// Returns the max weight for a request based on the client address.
|
||||
/// Localhost requests get a generous limit, external requests get a stricter one.
|
||||
fn max_weight(addr: &SocketAddr) -> usize {
|
||||
if addr.ip().is_loopback() {
|
||||
MAX_WEIGHT_LOCALHOST
|
||||
} else {
|
||||
MAX_WEIGHT
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ApiSeriesRoutes {
|
||||
fn add_series_routes(self) -> Self;
|
||||
}
|
||||
|
||||
impl ApiSeriesRoutes for ApiRouter<AppState> {
|
||||
fn add_series_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/series",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_catalog().clone())).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_tree")
|
||||
.series_tag()
|
||||
.summary("Series catalog")
|
||||
.description(
|
||||
"Returns the complete hierarchical catalog of available series organized as a tree structure. \
|
||||
Series are grouped by categories and subcategories."
|
||||
)
|
||||
.ok_response::<TreeNode>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/count",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_count())).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_count")
|
||||
.series_tag()
|
||||
.summary("Series count")
|
||||
.description("Returns the number of series available per index type.")
|
||||
.ok_response::<Vec<SeriesCount>>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/indexes",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.indexes().to_vec())).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_indexes")
|
||||
.series_tag()
|
||||
.summary("List available indexes")
|
||||
.description(
|
||||
"Returns all available indexes with their accepted query aliases. Use any alias when querying series."
|
||||
)
|
||||
.ok_response::<Vec<IndexInfo>>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/list",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Query(pagination): Query<Pagination>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.series_list(pagination))).await
|
||||
},
|
||||
|op| op
|
||||
.id("list_series")
|
||||
.series_tag()
|
||||
.summary("Series list")
|
||||
.description("Paginated flat list of all available series names. Use `page` query param for pagination.")
|
||||
.ok_response::<PaginatedSeries>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/search",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<SearchQuery>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.search_series(&query))).await
|
||||
},
|
||||
|op| op
|
||||
.id("search_series")
|
||||
.series_tag()
|
||||
.summary("Search series")
|
||||
.description("Fuzzy search for series by name. Supports partial matches and typos.")
|
||||
.ok_response::<Vec<&str>>()
|
||||
.not_modified()
|
||||
.server_error(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/{series}",
|
||||
get_with(
|
||||
async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<SeriesParam>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
|
||||
q.series_info(&path.series).ok_or_else(|| q.series_not_found_error(&path.series))
|
||||
}).await
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_info")
|
||||
.series_tag()
|
||||
.summary("Get series info")
|
||||
.description(
|
||||
"Returns the supported indexes and value type for the specified series."
|
||||
)
|
||||
.ok_response::<SeriesInfo>()
|
||||
.not_modified()
|
||||
.not_found()
|
||||
.server_error(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/{series}/{index}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<SeriesWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(SeriesSelection::from((path.index, path.series, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_series")
|
||||
.series_tag()
|
||||
.summary("Get series data")
|
||||
.description(
|
||||
"Fetch data for a specific series at the given index. \
|
||||
Use query parameters to filter by date range and format (json/csv)."
|
||||
)
|
||||
.ok_response::<SeriesData>()
|
||||
.csv_response()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/{series}/{index}/data",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<SeriesWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::raw_handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(SeriesSelection::from((path.index, path.series, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_data")
|
||||
.series_tag()
|
||||
.summary("Get raw series data")
|
||||
.description(
|
||||
"Returns just the data array without the SeriesData wrapper. \
|
||||
Supports the same range and format parameters as the standard endpoint."
|
||||
)
|
||||
.ok_response::<Vec<serde_json::Value>>()
|
||||
.csv_response()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/{series}/{index}/latest",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<SeriesWithIndex>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
|
||||
q.latest(&path.series, path.index)
|
||||
})
|
||||
.await
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_latest")
|
||||
.series_tag()
|
||||
.summary("Get latest series value")
|
||||
.description(
|
||||
"Returns the single most recent value for a series, unwrapped (not inside a SeriesData object)."
|
||||
)
|
||||
.ok_response::<serde_json::Value>()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/{series}/{index}/len",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<SeriesWithIndex>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
|
||||
q.len(&path.series, path.index)
|
||||
})
|
||||
.await
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_len")
|
||||
.series_tag()
|
||||
.summary("Get series data length")
|
||||
.description("Returns the total number of data points for a series at the given index.")
|
||||
.ok_response::<usize>()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/{series}/{index}/version",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<SeriesWithIndex>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
|
||||
q.version(&path.series, path.index)
|
||||
})
|
||||
.await
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_version")
|
||||
.series_tag()
|
||||
.summary("Get series version")
|
||||
.description("Returns the current version of a series. Changes when the series data is updated.")
|
||||
.ok_response::<brk_types::Version>()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/bulk",
|
||||
get_with(
|
||||
|uri, headers, addr, query, state| async move {
|
||||
bulk::handler(uri, headers, addr, query, state).await.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_series_bulk")
|
||||
.series_tag()
|
||||
.summary("Bulk series data")
|
||||
.description(
|
||||
"Fetch multiple series in a single request. Supports filtering by index and date range. \
|
||||
Returns an array of SeriesData objects. For a single series, use `get_series` instead."
|
||||
)
|
||||
.ok_response::<Vec<SeriesData>>()
|
||||
.csv_response()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
// Cost basis distribution endpoints
|
||||
.api_route(
|
||||
"/api/series/cost-basis",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Static, &uri, |q| q.cost_basis_cohorts())
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_cost_basis_cohorts")
|
||||
.series_tag()
|
||||
.summary("Available cost basis cohorts")
|
||||
.description("List available cohorts for cost basis distribution.")
|
||||
.ok_response::<Vec<String>>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/cost-basis/{cohort}/dates",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(params): Path<CostBasisCohortParam>,
|
||||
State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
|
||||
q.cost_basis_dates(¶ms.cohort)
|
||||
})
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_cost_basis_dates")
|
||||
.series_tag()
|
||||
.summary("Available cost basis dates")
|
||||
.description("List available dates for a cohort's cost basis distribution.")
|
||||
.ok_response::<Vec<Date>>()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/series/cost-basis/{cohort}/{date}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(params): Path<CostBasisParams>,
|
||||
Query(query): Query<CostBasisQuery>,
|
||||
State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
|
||||
q.cost_basis_formatted(
|
||||
¶ms.cohort,
|
||||
params.date,
|
||||
query.bucket,
|
||||
query.value,
|
||||
)
|
||||
})
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_cost_basis")
|
||||
.series_tag()
|
||||
.summary("Cost basis distribution")
|
||||
.description(
|
||||
"Get the cost basis distribution for a cohort on a specific date.\n\n\
|
||||
Query params:\n\
|
||||
- `bucket`: raw (default), lin200, lin500, lin1000, log10, log50, log100\n\
|
||||
- `value`: supply (default, in BTC), realized (USD), unrealized (USD)",
|
||||
)
|
||||
.ok_response::<CostBasisFormatted>()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ pub enum CacheStrategy {
|
||||
/// Etag = VERSION-{height}, Cache-Control: must-revalidate
|
||||
Height,
|
||||
|
||||
/// Static/immutable data (blocks by hash, validate-address, metrics catalog)
|
||||
/// Static/immutable data (blocks by hash, validate-address, series catalog)
|
||||
/// Etag = VERSION only, Cache-Control: must-revalidate
|
||||
Static,
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ struct ErrorDetail {
|
||||
/// Error category: "invalid_request", "forbidden", "not_found", "unavailable", or "internal"
|
||||
#[schemars(with = "String")]
|
||||
r#type: &'static str,
|
||||
/// Machine-readable error code (e.g. "invalid_address", "metric_not_found")
|
||||
/// Machine-readable error code (e.g. "invalid_address", "series_not_found")
|
||||
#[schemars(with = "String")]
|
||||
code: &'static str,
|
||||
/// Human-readable description
|
||||
@@ -50,8 +50,8 @@ fn error_status(e: &BrkError) -> StatusCode {
|
||||
| BrkError::InvalidAddress
|
||||
| BrkError::UnsupportedType(_)
|
||||
| BrkError::Parse(_)
|
||||
| BrkError::NoMetrics
|
||||
| BrkError::MetricUnsupportedIndex { .. }
|
||||
| BrkError::NoSeries
|
||||
| BrkError::SeriesUnsupportedIndex { .. }
|
||||
| BrkError::WeightExceeded { .. } => StatusCode::BAD_REQUEST,
|
||||
|
||||
BrkError::UnknownAddress
|
||||
@@ -59,7 +59,7 @@ fn error_status(e: &BrkError) -> StatusCode {
|
||||
| BrkError::NotFound(_)
|
||||
| BrkError::NoData
|
||||
| BrkError::OutOfRange(_)
|
||||
| BrkError::MetricNotFound(_) => StatusCode::NOT_FOUND,
|
||||
| BrkError::SeriesNotFound(_) => StatusCode::NOT_FOUND,
|
||||
|
||||
BrkError::AuthFailed => StatusCode::FORBIDDEN,
|
||||
BrkError::MempoolNotAvailable => StatusCode::SERVICE_UNAVAILABLE,
|
||||
@@ -75,15 +75,15 @@ fn error_code(e: &BrkError) -> &'static str {
|
||||
BrkError::InvalidNetwork => "invalid_network",
|
||||
BrkError::UnsupportedType(_) => "unsupported_type",
|
||||
BrkError::Parse(_) => "parse_error",
|
||||
BrkError::NoMetrics => "no_metrics",
|
||||
BrkError::MetricUnsupportedIndex { .. } => "metric_unsupported_index",
|
||||
BrkError::NoSeries => "no_series",
|
||||
BrkError::SeriesUnsupportedIndex { .. } => "series_unsupported_index",
|
||||
BrkError::WeightExceeded { .. } => "weight_exceeded",
|
||||
BrkError::UnknownAddress => "unknown_address",
|
||||
BrkError::UnknownTxid => "unknown_txid",
|
||||
BrkError::NotFound(_) => "not_found",
|
||||
BrkError::OutOfRange(_) => "out_of_range",
|
||||
BrkError::NoData => "no_data",
|
||||
BrkError::MetricNotFound(_) => "metric_not_found",
|
||||
BrkError::SeriesNotFound(_) => "series_not_found",
|
||||
BrkError::MempoolNotAvailable => "mempool_not_available",
|
||||
BrkError::AuthFailed => "auth_failed",
|
||||
_ => "internal_error",
|
||||
|
||||
@@ -11,6 +11,7 @@ pub trait TransformResponseExtended<'t> {
|
||||
fn mempool_tag(self) -> Self;
|
||||
fn metrics_tag(self) -> Self;
|
||||
fn mining_tag(self) -> Self;
|
||||
fn series_tag(self) -> Self;
|
||||
fn server_tag(self) -> Self;
|
||||
fn transactions_tag(self) -> Self;
|
||||
|
||||
@@ -55,6 +56,10 @@ impl<'t> TransformResponseExtended<'t> for TransformOperation<'t> {
|
||||
self.tag("Metrics")
|
||||
}
|
||||
|
||||
fn series_tag(self) -> Self {
|
||||
self.tag("Series")
|
||||
}
|
||||
|
||||
fn mining_tag(self) -> Self {
|
||||
self.tag("Mining")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user