Skip to main content

mina_node_native/http_server/routes/
scan_state.rs

1//! Scan state endpoints.
2//!
3//! - `GET /scan-state/summary` - Get scan state summary for best tip
4//! - `GET /scan-state/summary/{block}` - Get scan state summary for specific block
5
6use std::str::FromStr;
7
8use axum::{
9    extract::{Path, State},
10    Json,
11};
12use utoipa_axum::{router::OpenApiRouter, routes};
13
14use mina_node::rpc::{
15    RpcRequest, RpcScanStateSummary, RpcScanStateSummaryGetQuery, RpcScanStateSummaryGetResponse,
16};
17use mina_p2p_messages::v2::StateHash;
18
19use crate::http_server::{AppError, AppResult, AppState, JsonErrorResponse};
20
21/// Block identifier for scan state queries.
22///
23/// Can be "latest" for best tip, a block height (u32), or a block hash.
24#[derive(Debug, Clone, utoipa::ToSchema)]
25#[allow(unused, reason = "schema type for block identifier query param")]
26enum BlockIdentifier {
27    /// Use best tip block
28    Latest,
29    /// Block height (e.g., 490467)
30    Height(u32),
31    /// Block hash (e.g., 3NLrbJrSvDVEqnMMEeWvk1TiCmcDpnUiHZqdEKVEZcqieKu1TBkS)
32    Hash(StateHash),
33}
34
35impl FromStr for BlockIdentifier {
36    type Err = String;
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        if s == "latest" {
40            Ok(BlockIdentifier::Latest)
41        } else if let Ok(height) = s.parse::<u32>() {
42            Ok(BlockIdentifier::Height(height))
43        } else {
44            s.parse::<StateHash>()
45                .map(BlockIdentifier::Hash)
46                .map_err(|_| "invalid arg! Expected 'latest', block height, or block hash".into())
47        }
48    }
49}
50
51/// Scan state routes
52pub fn routes() -> OpenApiRouter<AppState> {
53    OpenApiRouter::new()
54        .routes(routes!(summary))
55        .routes(routes!(summary_for_block))
56}
57
58/// Scan state summary for best tip
59#[utoipa::path(
60    get,
61    path = "/scan-state/summary",
62    tag = "scan-state",
63    responses(
64        (status = 200, description = "Scan state summary", body = RpcScanStateSummary),
65        (status = 500, description = "Target block not found", body = JsonErrorResponse,
66            example = json!({"error": "target block not found"}))
67    )
68)]
69async fn summary(State(state): State<AppState>) -> AppResult<Json<RpcScanStateSummary>> {
70    // TODO: "target block not found" should arguably be 404, not 500
71    let result: Option<RpcScanStateSummaryGetResponse> = state
72        .rpc_sender()
73        .oneshot_request(RpcRequest::ScanStateSummaryGet(
74            RpcScanStateSummaryGetQuery::ForBestTip,
75        ))
76        .await;
77
78    match result {
79        None => Err(AppError::ChannelDropped),
80        Some(Ok(data)) => Ok(Json(data)),
81        Some(Err(err)) => Err(AppError::Internal(err)),
82    }
83}
84
85/// Scan state summary for specific block
86#[utoipa::path(
87    get,
88    path = "/scan-state/summary/{block}",
89    tag = "scan-state",
90    params(
91        ("block" = BlockIdentifier, Path, description = "\"latest\" for best tip, block height, or block hash")
92    ),
93    responses(
94        (status = 200, description = "Scan state summary", body = RpcScanStateSummary),
95        (status = 400, description = "Invalid block identifier", body = JsonErrorResponse,
96            example = json!({"error": "invalid arg! Expected block hash or height"})),
97        (status = 500, description = "Target block not found", body = JsonErrorResponse,
98            example = json!({"error": "target block not found"}))
99    )
100)]
101async fn summary_for_block(
102    State(state): State<AppState>,
103    Path(block): Path<String>,
104) -> AppResult<Json<RpcScanStateSummary>> {
105    // Try parsing as height first, then as hash
106    let query = if let Ok(height) = block.parse::<u32>() {
107        RpcScanStateSummaryGetQuery::ForBlockWithHeight(height)
108    } else {
109        match block.parse() {
110            Ok(hash) => RpcScanStateSummaryGetQuery::ForBlockWithHash(hash),
111            Err(_) => {
112                return Err(AppError::BadRequest(
113                    "invalid arg! Expected block hash or height".to_string(),
114                ))
115            }
116        }
117    };
118
119    let result: Option<RpcScanStateSummaryGetResponse> = state
120        .rpc_sender()
121        .oneshot_request(RpcRequest::ScanStateSummaryGet(query))
122        .await;
123
124    match result {
125        None => Err(AppError::ChannelDropped),
126        Some(Ok(data)) => Ok(Json(data)),
127        Some(Err(err)) => Err(AppError::Internal(err)),
128    }
129}