Skip to main content

mina_node_native/http_server/routes/
stats.rs

1//! Statistics endpoints.
2//!
3//! - `GET /stats/actions` - Get action statistics (optional `id` query param)
4//! - `GET /stats/sync` - Get sync statistics (optional `limit` query param)
5//! - `GET /stats/block_producer` - Get block producer statistics
6
7use axum::{
8    extract::{Query, State},
9    Json,
10};
11use serde::Deserialize;
12use utoipa_axum::{router::OpenApiRouter, routes};
13
14use mina_node::rpc::{
15    ActionStatsQuery, RpcActionStatsGetResponse, RpcBlockProducerStatsGetResponse, RpcRequest,
16    RpcSyncStatsGetResponse, SyncStatsQuery,
17};
18
19use crate::http_server::{AppError, AppResult, AppState, JsonErrorResponse};
20
21/// Stats routes
22pub fn routes() -> OpenApiRouter<AppState> {
23    OpenApiRouter::new()
24        .routes(routes!(actions))
25        .routes(routes!(sync))
26        .routes(routes!(block_producer))
27}
28
29#[derive(Deserialize, Default)]
30struct ActionQueryParams {
31    /// Optional filter: "latest" for latest block, or a block ID (u64).
32    id: Option<String>,
33}
34
35/// Action statistics
36#[utoipa::path(
37    get,
38    path = "/stats/actions",
39    tag = "stats",
40    params(
41        ("id" = Option<String>, Query, description = "\"latest\" for latest block, or numeric block ID")
42    ),
43    responses(
44        // inline: type alias for Option<T> would register as "Option"
45        (status = 200, description = "Action statistics", body = inline(RpcActionStatsGetResponse)),
46        (status = 400, description = "Invalid id parameter", body = JsonErrorResponse,
47            example = json!({"error": "'id' must be an u64 integer: invalid digit found in string, instead passed: foo"}))
48    )
49)]
50async fn actions(
51    State(state): State<AppState>,
52    Query(params): Query<ActionQueryParams>,
53) -> AppResult<Json<RpcActionStatsGetResponse>> {
54    let query = match params.id.as_deref() {
55        None => ActionStatsQuery::SinceStart,
56        Some("latest") => ActionStatsQuery::ForLatestBlock,
57        Some(id) => {
58            let id: u64 = id.parse().map_err(|err| {
59                AppError::BadRequest(format!(
60                    "'id' must be an u64 integer: {err}, instead passed: {id}"
61                ))
62            })?;
63            ActionStatsQuery::ForBlockWithId(id)
64        }
65    };
66
67    jsonify_rpc!(state, RpcRequest::ActionStatsGet(query))
68}
69
70#[derive(Deserialize, Default)]
71struct SyncQueryParams {
72    /// Optional limit on the number of sync snapshots to return.
73    limit: Option<usize>,
74}
75
76/// Sync statistics
77#[utoipa::path(
78    get,
79    path = "/stats/sync",
80    tag = "stats",
81    params(
82        ("limit" = Option<usize>, Query, description = "Max number of sync snapshots to return")
83    ),
84    responses(
85        // inline: type alias for Option<Vec<T>> would register as "Option"
86        (status = 200, description = "Sync statistics", body = inline(RpcSyncStatsGetResponse))
87    )
88)]
89async fn sync(
90    State(state): State<AppState>,
91    Query(SyncQueryParams { limit }): Query<SyncQueryParams>,
92) -> AppResult<Json<RpcSyncStatsGetResponse>> {
93    jsonify_rpc!(state, RpcRequest::SyncStatsGet(SyncStatsQuery { limit }))
94}
95
96/// Block producer statistics
97#[utoipa::path(
98    get,
99    path = "/stats/block_producer",
100    tag = "stats",
101    responses(
102        // inline: type alias for Option<T> would register as "Option"
103        (status = 200, description = "Block producer statistics", body = inline(RpcBlockProducerStatsGetResponse))
104    )
105)]
106async fn block_producer(
107    State(state): State<AppState>,
108) -> AppResult<Json<RpcBlockProducerStatsGetResponse>> {
109    jsonify_rpc!(state, RpcRequest::BlockProducerStatsGet)
110}