mina_node_native/http_server/routes/
scan_state.rs1use 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#[derive(Debug, Clone, utoipa::ToSchema)]
25#[allow(unused, reason = "schema type for block identifier query param")]
26enum BlockIdentifier {
27 Latest,
29 Height(u32),
31 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
51pub fn routes() -> OpenApiRouter<AppState> {
53 OpenApiRouter::new()
54 .routes(routes!(summary))
55 .routes(routes!(summary_for_block))
56}
57
58#[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 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#[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 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}