Skip to main content

espresso_api/
axum.rs

1//! Axum HTTP/JSON API handlers
2
3pub mod routes;
4
5use aide::{
6    axum::{ApiRouter, routing::get_with},
7    openapi::{Info, OpenApi},
8    operation::OperationOutput,
9    redoc::Redoc,
10    scalar::Scalar,
11};
12use axum::{
13    Extension, Json, Router,
14    body::Bytes,
15    extract::{Path, Request, State, ws::WebSocketUpgrade},
16    http::{HeaderMap, StatusCode, Uri, header},
17    middleware::{self, Next},
18    response::{Html, IntoResponse, Response},
19    routing::get,
20};
21use futures::stream::BoxStream;
22use schemars::transform::Transform;
23use serde::Serialize;
24use serialization_api::v2::{
25    GetIncorrectEncodingProofRequest, GetNamespaceProofRequest, GetRewardAccountProofRequest,
26    GetRewardBalanceRequest, GetRewardBalancesRequest, GetRewardClaimInputRequest,
27    GetRewardMerkleTreeRequest, GetStakeTableRequest, GetStateCertificateRequest,
28};
29use vbs::{BinarySerializer, Serializer, version::StaticVersion};
30
31use crate::{
32    error::{ApiError, AvailabilityError},
33    handlers, v1, v2,
34};
35
36/// API error response — wire-compatible with the `Custom` variant of the per-module error enums
37/// (`node::Error::Custom`, `merklized_state::Error::Custom`, etc.) that all of tide-disco's
38/// `Error::catch_all` calls produce. Most of our migrated endpoints (catchup, submit,
39/// state-signature, light-client, node, status, config, token, database) take that path, so this
40/// envelope is byte-identical with tide's error response for them. Endpoints that use a specific
41/// variant directly (e.g. `availability::Error::FetchLeaf`) emit their own shape on tide; those
42/// bytes are not matched here.
43#[derive(Debug, Serialize)]
44struct ErrorResponse {
45    #[serde(rename = "Custom")]
46    custom: CustomError,
47}
48
49#[derive(Debug, Serialize)]
50struct CustomError {
51    // Field order matches `node::Error::Custom { message, status }` declaration so serde_json
52    // emits the same key order on the wire.
53    message: String,
54    status: u16,
55}
56
57impl IntoResponse for ApiError {
58    fn into_response(self) -> Response {
59        let status = match &self {
60            ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
61            ApiError::NotFound(_) => StatusCode::NOT_FOUND,
62            ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
63        };
64
65        let body = Json(ErrorResponse {
66            custom: CustomError {
67                message: self.to_string(),
68                status: status.as_u16(),
69            },
70        });
71
72        (status, body).into_response()
73    }
74}
75
76/// Encode a successful response body based on the request's `Accept` header, matching
77/// tide-disco's content negotiation.
78///
79/// surf-disco's default `Accept` is `application/octet-stream`, so production internal clients
80/// (peer-catchup, submit-transactions, light-client provider) expect VBS-encoded responses for
81/// the endpoints that flow large structured data. Falls back to JSON otherwise.
82fn encode_response<T: Serialize>(headers: &HeaderMap, value: T) -> Result<Response, ApiError> {
83    let accept = headers
84        .get(header::ACCEPT)
85        .and_then(|v| v.to_str().ok())
86        .unwrap_or("");
87    if accept.contains("application/octet-stream") {
88        let bytes = Serializer::<StaticVersion<0, 1>>::serialize(&value)
89            .map_err(|e| ApiError::Internal(anyhow::anyhow!("vbs serialize: {e}")))?;
90        Ok(([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response())
91    } else {
92        Ok(Json(value).into_response())
93    }
94}
95
96/// Decode a request body based on its `Content-Type`, matching tide-disco's `body_auto` behavior.
97///
98/// - `application/octet-stream`: VBS (versioned binary) — what `surf-disco::Request::body_binary`
99///   sends, and what production peer-catchup / submit-transactions clients use.
100/// - `application/json`: serde_json.
101///
102/// All v1 endpoints in this codebase use the V0_1 API version for VBS framing.
103fn decode_body<T: serde::de::DeserializeOwned>(
104    headers: &HeaderMap,
105    body: &[u8],
106) -> Result<T, ApiError> {
107    let content_type = headers
108        .get(header::CONTENT_TYPE)
109        .and_then(|v| v.to_str().ok());
110    match content_type {
111        Some(ct) if ct.starts_with("application/octet-stream") => {
112            Serializer::<StaticVersion<0, 1>>::deserialize(body)
113                .map_err(|e| ApiError::BadRequest(anyhow::anyhow!("invalid binary body: {e}")))
114        },
115        Some(ct) if ct.starts_with("application/json") => serde_json::from_slice(body)
116            .map_err(|e| ApiError::BadRequest(anyhow::anyhow!("invalid json body: {e}"))),
117        Some(other) => Err(ApiError::BadRequest(anyhow::anyhow!(
118            "unsupported Content-Type: {other}"
119        ))),
120        None => Err(ApiError::BadRequest(anyhow::anyhow!(
121            "missing Content-Type header"
122        ))),
123    }
124}
125
126/// Classify an `anyhow::Error` from an availability handler into the appropriate `ApiError`
127/// variant. Errors produced via [`AvailabilityError`] in the state implementation carry semantic
128/// meaning; everything else falls back to a 500 Internal Server Error.
129fn classify_availability_error(err: anyhow::Error) -> ApiError {
130    let is_not_found = err
131        .downcast_ref::<AvailabilityError>()
132        .map(|e| matches!(e, AvailabilityError::NotFound(_)));
133    match is_not_found {
134        Some(true) => ApiError::NotFound(err),
135        Some(false) => ApiError::BadRequest(err),
136        None => ApiError::Internal(err),
137    }
138}
139
140impl OperationOutput for ApiError {
141    type Inner = Self;
142}
143
144/// Serve the OpenAPI spec (extracted from Extension)
145async fn serve_openapi_spec(Extension(api): Extension<OpenApi>) -> Json<OpenApi> {
146    Json(api)
147}
148
149/// Serve custom Swagger UI with collapsed defaults
150async fn serve_swagger_ui() -> Html<&'static str> {
151    Html(include_str!("../templates/swagger.html"))
152}
153
154/// Middleware to rewrite root paths to /v2 paths
155///
156/// Requests to `/rewards/...` get rewritten to `/v2/rewards/...`
157/// Paths already prefixed with `/v2` are left unchanged
158///
159/// Note: This middleware is only applied to the v2 router, so v1 routes never pass through it
160async fn rewrite_root_to_v2(mut req: Request, next: Next) -> Response {
161    let uri = req.uri().clone();
162    let path = uri.path();
163
164    // Only rewrite unversioned paths (not starting with /v2)
165    if !path.starts_with("/v2") && path != "/" {
166        let new_path = format!("/v2{}", path);
167        let pq = if let Some(q) = uri.query() {
168            format!("{}?{}", new_path, q)
169        } else {
170            new_path
171        };
172        if let Ok(new_uri) = Uri::builder().path_and_query(pq).build() {
173            *req.uri_mut() = new_uri;
174        }
175    }
176
177    next.run(req).await
178}
179
180/// Redirect handler for root path
181async fn redirect_to_docs() -> axum::response::Redirect {
182    axum::response::Redirect::permanent("/v2")
183}
184
185struct SendQuery<T>(T);
186
187impl<T, S> axum::extract::FromRequestParts<S> for SendQuery<T>
188where
189    T: serde::de::DeserializeOwned + Send,
190    S: Send + Sync,
191{
192    type Rejection = axum::extract::rejection::QueryRejection;
193
194    async fn from_request_parts(
195        parts: &mut axum::http::request::Parts,
196        state: &S,
197    ) -> Result<Self, Self::Rejection> {
198        axum::extract::Query::<T>::from_request_parts(parts, state)
199            .await
200            .map(|axum::extract::Query(inner)| SendQuery(inner))
201    }
202}
203
204impl<T: schemars::JsonSchema> aide::operation::OperationInput for SendQuery<T> {
205    fn operation_input(
206        ctx: &mut aide::generate::GenContext,
207        operation: &mut aide::openapi::Operation,
208    ) {
209        let schema = ctx.schema.subschema_for::<T>();
210        let params = aide::operation::parameters_from_schema(
211            ctx,
212            schema,
213            aide::operation::ParamLocation::Query,
214        );
215        aide::operation::add_parameters(ctx, operation, params);
216    }
217}
218
219/// Wire format for a WebSocket stream — negotiated from the upgrade request's `Accept` header
220/// to match tide-disco. surf-disco clients default to `application/octet-stream`, so production
221/// stream consumers expect VBS-encoded `Message::Binary` frames.
222#[derive(Clone, Copy)]
223enum WsFormat {
224    Binary,
225    Json,
226}
227
228fn ws_format(headers: &HeaderMap) -> WsFormat {
229    let accept = headers
230        .get(header::ACCEPT)
231        .and_then(|v| v.to_str().ok())
232        .unwrap_or("");
233    if accept.contains("application/octet-stream") {
234        WsFormat::Binary
235    } else {
236        WsFormat::Json
237    }
238}
239
240async fn drive_ws_stream<T: Serialize>(
241    mut socket: axum::extract::ws::WebSocket,
242    stream: BoxStream<'static, T>,
243    format: WsFormat,
244) {
245    use axum::extract::ws::Message;
246    use futures::StreamExt as _;
247    futures::pin_mut!(stream);
248    while let Some(item) = stream.next().await {
249        let msg = match format {
250            WsFormat::Binary => match Serializer::<StaticVersion<0, 1>>::serialize(&item) {
251                Ok(bytes) => Message::Binary(bytes.into()),
252                Err(_) => break,
253            },
254            WsFormat::Json => match serde_json::to_string(&item) {
255                Ok(json) => Message::Text(json.into()),
256                Err(_) => break,
257            },
258        };
259        if socket.send(msg).await.is_err() {
260            break;
261        }
262    }
263}
264
265/// Create a combined router serving both v1 and v2 APIs
266pub fn create_combined_router<S>(state: S) -> Router
267where
268    S: v1::RewardApi
269        + v1::AvailabilityApi
270        + v1::HotShotAvailabilityApi
271        + v1::BlockStateApi
272        + v1::FeeStateApi
273        + v1::StatusApi
274        + v1::ConfigApi
275        + v1::NodeApi
276        + v1::CatchupApi
277        + v1::SubmitApi
278        + v1::StateSignatureApi
279        + v1::HotShotEventsApi
280        + v1::LightClientApi
281        + v1::ExplorerApi
282        + v1::TokenApi
283        + v1::DatabaseApi
284        + v2::RewardApi
285        + v2::DataApi
286        + v2::ConsensusApi
287        + Clone
288        + Send
289        + Sync
290        + 'static,
291{
292    let router_v1 = create_router_v1(state.clone());
293    let router_v2 = create_router_v2(state).layer(middleware::from_fn(rewrite_root_to_v2));
294
295    router_v2.merge(router_v1).route("/", get(redirect_to_docs))
296}
297
298/// Create v1 router without OpenAPI documentation (internal types)
299pub fn create_router_v1<S>(state: S) -> Router
300where
301    S: v1::RewardApi
302        + v1::AvailabilityApi
303        + v1::HotShotAvailabilityApi
304        + v1::BlockStateApi
305        + v1::FeeStateApi
306        + v1::StatusApi
307        + v1::ConfigApi
308        + v1::NodeApi
309        + v1::CatchupApi
310        + v1::SubmitApi
311        + v1::StateSignatureApi
312        + v1::HotShotEventsApi
313        + v1::LightClientApi
314        + v1::ExplorerApi
315        + v1::TokenApi
316        + v1::DatabaseApi
317        + Clone
318        + Send
319        + Sync
320        + 'static,
321{
322    // Create handler closures that capture the generic state type
323    let get_reward_claim_input =
324        |State(state): State<S>, Path((height, address)): Path<(u64, String)>| async move {
325            state
326                .get_reward_claim_input(height, address)
327                .await
328                .map(Json)
329                .map_err(ApiError::Internal)
330        };
331
332    let get_reward_balance =
333        |State(state): State<S>, Path((height, address)): Path<(u64, String)>| async move {
334            state
335                .get_reward_balance(height, address)
336                .await
337                .map(Json)
338                .map_err(ApiError::Internal)
339        };
340
341    let get_latest_reward_balance = |State(state): State<S>, Path(address): Path<String>| async move {
342        state
343            .get_latest_reward_balance(address)
344            .await
345            .map(Json)
346            .map_err(ApiError::Internal)
347    };
348
349    let get_reward_account_proof =
350        |State(state): State<S>, Path((height, address)): Path<(u64, String)>| async move {
351            state
352                .get_reward_account_proof(height, address)
353                .await
354                .map(Json)
355                .map_err(ApiError::Internal)
356        };
357
358    let get_latest_reward_account_proof = |State(state): State<S>, Path(address): Path<String>| async move {
359        state
360            .get_latest_reward_account_proof(address)
361            .await
362            .map(Json)
363            .map_err(ApiError::Internal)
364    };
365
366    let get_reward_amounts =
367        |State(state): State<S>, Path((height, offset, limit)): Path<(u64, u64, u64)>| async move {
368            state
369                .get_reward_amounts(height, offset, limit)
370                .await
371                .map(Json)
372                .map_err(ApiError::Internal)
373        };
374
375    let get_reward_merkle_tree_v2 = |State(state): State<S>, Path(height): Path<u64>| async move {
376        <S as v1::RewardApi>::get_reward_merkle_tree_v2(&state, height)
377            .await
378            .map(Json)
379            .map_err(ApiError::Internal)
380    };
381
382    // Availability API handlers
383    // Route: /v1/availability/block/{height}/namespace/{namespace}
384    let get_namespace_proof_by_height =
385        |State(state): State<S>, Path((height, namespace)): Path<(u64, u32)>| async move {
386            state
387                .get_namespace_proof(v1::availability::BlockId::Height(height), namespace)
388                .await
389                .map(Json)
390                .map_err(classify_availability_error)
391        };
392
393    // Route: /v1/availability/block/hash/{hash}/namespace/{namespace}
394    let get_namespace_proof_by_hash =
395        |State(state): State<S>, Path((hash, namespace)): Path<(String, u32)>| async move {
396            state
397                .get_namespace_proof(v1::availability::BlockId::Hash(hash), namespace)
398                .await
399                .map(Json)
400                .map_err(classify_availability_error)
401        };
402
403    // Route: /v1/availability/block/payload-hash/{payload-hash}/namespace/{namespace}
404    let get_namespace_proof_by_payload_hash =
405        |State(state): State<S>, Path((payload_hash, namespace)): Path<(String, u32)>| async move {
406            state
407                .get_namespace_proof(
408                    v1::availability::BlockId::PayloadHash(payload_hash),
409                    namespace,
410                )
411                .await
412                .map(Json)
413                .map_err(classify_availability_error)
414        };
415
416    // Route: /v1/availability/block/{from}/{until}/namespace/{namespace}
417    let get_namespace_proof_range =
418        |State(state): State<S>, Path((from, until, namespace)): Path<(u64, u64, u32)>| async move {
419            state
420                .get_namespace_proof_range(from, until, namespace)
421                .await
422                .map(Json)
423                .map_err(classify_availability_error)
424        };
425
426    let get_incorrect_encoding_proof =
427        |State(state): State<S>, Path((block_number, namespace)): Path<(u64, u32)>| async move {
428            state
429                .get_incorrect_encoding_proof(
430                    v1::availability::BlockId::Height(block_number),
431                    namespace,
432                )
433                .await
434                .map(Json)
435                .map_err(classify_availability_error)
436        };
437
438    let get_state_cert_v1 = |State(state): State<S>, Path(epoch): Path<u64>| async move {
439        <S as v1::AvailabilityApi>::get_state_cert(&state, epoch)
440            .await
441            .map(Json)
442            .map_err(classify_availability_error)
443    };
444
445    let get_state_cert_v2 = |State(state): State<S>, Path(epoch): Path<u64>| async move {
446        state
447            .get_state_cert_v2(epoch)
448            .await
449            .map(Json)
450            .map_err(classify_availability_error)
451    };
452
453    // HotShot availability API handlers
454
455    let get_leaf_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
456        state
457            .get_leaf(v1::LeafId::Height(height))
458            .await
459            .map(Json)
460            .map_err(classify_availability_error)
461    };
462    let get_leaf_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
463        state
464            .get_leaf(v1::LeafId::Hash(hash))
465            .await
466            .map(Json)
467            .map_err(classify_availability_error)
468    };
469    let get_leaf_range = |State(state): State<S>, Path((from, until)): Path<(usize, usize)>| async move {
470        state
471            .get_leaf_range(from, until)
472            .await
473            .map(Json)
474            .map_err(classify_availability_error)
475    };
476
477    let get_header_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
478        state
479            .get_header(v1::BlockId::Height(height))
480            .await
481            .map(Json)
482            .map_err(classify_availability_error)
483    };
484    let get_header_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
485        state
486            .get_header(v1::BlockId::Hash(hash))
487            .await
488            .map(Json)
489            .map_err(classify_availability_error)
490    };
491    let get_header_by_payload_hash = |State(state): State<S>, Path(payload_hash): Path<String>| async move {
492        state
493            .get_header(v1::BlockId::PayloadHash(payload_hash))
494            .await
495            .map(Json)
496            .map_err(classify_availability_error)
497    };
498    let get_header_range = |State(state): State<S>, Path((from, until)): Path<(usize, usize)>| async move {
499        state
500            .get_header_range(from, until)
501            .await
502            .map(Json)
503            .map_err(classify_availability_error)
504    };
505
506    let get_block_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
507        state
508            .get_block(v1::BlockId::Height(height))
509            .await
510            .map(Json)
511            .map_err(classify_availability_error)
512    };
513    let get_block_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
514        state
515            .get_block(v1::BlockId::Hash(hash))
516            .await
517            .map(Json)
518            .map_err(classify_availability_error)
519    };
520    let get_block_by_payload_hash = |State(state): State<S>, Path(payload_hash): Path<String>| async move {
521        state
522            .get_block(v1::BlockId::PayloadHash(payload_hash))
523            .await
524            .map(Json)
525            .map_err(classify_availability_error)
526    };
527    let get_block_range = |State(state): State<S>, Path((from, until)): Path<(usize, usize)>| async move {
528        state
529            .get_block_range(from, until)
530            .await
531            .map(Json)
532            .map_err(classify_availability_error)
533    };
534
535    let get_payload_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
536        state
537            .get_payload(v1::PayloadId::Height(height))
538            .await
539            .map(Json)
540            .map_err(classify_availability_error)
541    };
542    let get_payload_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
543        state
544            .get_payload(v1::PayloadId::Hash(hash))
545            .await
546            .map(Json)
547            .map_err(classify_availability_error)
548    };
549    let get_payload_by_block_hash = |State(state): State<S>, Path(block_hash): Path<String>| async move {
550        state
551            .get_payload(v1::PayloadId::BlockHash(block_hash))
552            .await
553            .map(Json)
554            .map_err(classify_availability_error)
555    };
556    let get_payload_range = |State(state): State<S>, Path((from, until)): Path<(usize, usize)>| async move {
557        state
558            .get_payload_range(from, until)
559            .await
560            .map(Json)
561            .map_err(classify_availability_error)
562    };
563
564    let get_vid_common_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
565        state
566            .get_vid_common(v1::BlockId::Height(height))
567            .await
568            .map(Json)
569            .map_err(classify_availability_error)
570    };
571    let get_vid_common_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
572        state
573            .get_vid_common(v1::BlockId::Hash(hash))
574            .await
575            .map(Json)
576            .map_err(classify_availability_error)
577    };
578    let get_vid_common_by_payload_hash =
579        |State(state): State<S>, Path(payload_hash): Path<String>| async move {
580            state
581                .get_vid_common(v1::BlockId::PayloadHash(payload_hash))
582                .await
583                .map(Json)
584                .map_err(classify_availability_error)
585        };
586    let get_vid_common_range =
587        |State(state): State<S>, Path((from, until)): Path<(usize, usize)>| async move {
588            state
589                .get_vid_common_range(from, until)
590                .await
591                .map(Json)
592                .map_err(classify_availability_error)
593        };
594
595    let get_transaction_by_position =
596        |State(state): State<S>, Path((height, index)): Path<(u64, u64)>| async move {
597            state
598                .get_transaction_by_position(height, index)
599                .await
600                .map(Json)
601                .map_err(classify_availability_error)
602        };
603    let get_transaction_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
604        state
605            .get_transaction_by_hash(hash)
606            .await
607            .map(Json)
608            .map_err(classify_availability_error)
609    };
610    let get_transaction_proof_by_position =
611        |State(state): State<S>, Path((height, index)): Path<(u64, u64)>| async move {
612            state
613                .get_transaction_proof_by_position(height, index)
614                .await
615                .map(Json)
616                .map_err(classify_availability_error)
617        };
618    let get_transaction_proof_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
619        state
620            .get_transaction_proof_by_hash(hash)
621            .await
622            .map(Json)
623            .map_err(classify_availability_error)
624    };
625
626    let get_block_summary_by_height = |State(state): State<S>, Path(height): Path<usize>| async move {
627        state
628            .get_block_summary(height)
629            .await
630            .map(Json)
631            .map_err(classify_availability_error)
632    };
633    let get_block_summary_range =
634        |State(state): State<S>, Path((from, until)): Path<(usize, usize)>| async move {
635            state
636                .get_block_summary_range(from, until)
637                .await
638                .map(Json)
639                .map_err(classify_availability_error)
640        };
641
642    let get_limits = |State(state): State<S>| async move {
643        state
644            .get_limits()
645            .await
646            .map(Json)
647            .map_err(ApiError::Internal)
648    };
649
650    let get_cert2 = |State(state): State<S>, Path(height): Path<u64>| async move {
651        <S as v1::HotShotAvailabilityApi>::get_cert2(&state, height)
652            .await
653            .map(Json)
654            .map_err(ApiError::Internal)
655    };
656
657    // WebSocket streaming handlers
658    let stream_leaves = |ws: WebSocketUpgrade,
659                         State(state): State<S>,
660                         headers: HeaderMap,
661                         Path(height): Path<usize>| async move {
662        let format = ws_format(&headers);
663        ws.on_upgrade(move |socket| async move {
664            match state.stream_leaves(height).await {
665                Ok(stream) => drive_ws_stream(socket, stream, format).await,
666                Err(e) => tracing::warn!("stream_leaves: {e}"),
667            }
668        })
669    };
670    let stream_headers = |ws: WebSocketUpgrade,
671                          State(state): State<S>,
672                          headers: HeaderMap,
673                          Path(height): Path<usize>| async move {
674        let format = ws_format(&headers);
675        ws.on_upgrade(move |socket| async move {
676            match state.stream_headers(height).await {
677                Ok(stream) => drive_ws_stream(socket, stream, format).await,
678                Err(e) => tracing::warn!("stream_headers: {e}"),
679            }
680        })
681    };
682    let stream_blocks = |ws: WebSocketUpgrade,
683                         State(state): State<S>,
684                         headers: HeaderMap,
685                         Path(height): Path<usize>| async move {
686        let format = ws_format(&headers);
687        ws.on_upgrade(move |socket| async move {
688            match state.stream_blocks(height).await {
689                Ok(stream) => drive_ws_stream(socket, stream, format).await,
690                Err(e) => tracing::warn!("stream_blocks: {e}"),
691            }
692        })
693    };
694    let stream_payloads = |ws: WebSocketUpgrade,
695                           State(state): State<S>,
696                           headers: HeaderMap,
697                           Path(height): Path<usize>| async move {
698        let format = ws_format(&headers);
699        ws.on_upgrade(move |socket| async move {
700            match state.stream_payloads(height).await {
701                Ok(stream) => drive_ws_stream(socket, stream, format).await,
702                Err(e) => tracing::warn!("stream_payloads: {e}"),
703            }
704        })
705    };
706    let stream_vid_common = |ws: WebSocketUpgrade,
707                             State(state): State<S>,
708                             headers: HeaderMap,
709                             Path(height): Path<usize>| async move {
710        let format = ws_format(&headers);
711        ws.on_upgrade(move |socket| async move {
712            match state.stream_vid_common(height).await {
713                Ok(stream) => drive_ws_stream(socket, stream, format).await,
714                Err(e) => tracing::warn!("stream_vid_common: {e}"),
715            }
716        })
717    };
718    let stream_transactions = |ws: WebSocketUpgrade,
719                               State(state): State<S>,
720                               headers: HeaderMap,
721                               Path(height): Path<usize>| async move {
722        let format = ws_format(&headers);
723        ws.on_upgrade(move |socket| async move {
724            match state.stream_transactions(height, None).await {
725                Ok(stream) => drive_ws_stream(socket, stream, format).await,
726                Err(e) => tracing::warn!("stream_transactions: {e}"),
727            }
728        })
729    };
730    let stream_transactions_ns =
731        |ws: WebSocketUpgrade,
732         State(state): State<S>,
733         headers: HeaderMap,
734         Path((height, namespace)): Path<(usize, u32)>| async move {
735            let format = ws_format(&headers);
736            ws.on_upgrade(move |socket| async move {
737                match state.stream_transactions(height, Some(namespace)).await {
738                    Ok(stream) => drive_ws_stream(socket, stream, format).await,
739                    Err(e) => tracing::warn!("stream_transactions_ns: {e}"),
740                }
741            })
742        };
743    let stream_namespace_proofs =
744        |ws: WebSocketUpgrade,
745         State(state): State<S>,
746         headers: HeaderMap,
747         Path((height, namespace)): Path<(usize, u32)>| async move {
748            let format = ws_format(&headers);
749            ws.on_upgrade(move |socket| async move {
750                match state.stream_namespace_proofs(height, namespace).await {
751                    Ok(stream) => drive_ws_stream(socket, stream, format).await,
752                    Err(e) => tracing::warn!("stream_namespace_proofs: {e}"),
753                }
754            })
755        };
756
757    // Merklized state handlers: block-state
758    let get_block_state_path_by_height =
759        |State(state): State<S>, Path((height, key)): Path<(u64, String)>| async move {
760            <S as v1::BlockStateApi>::get_block_state_path(
761                &state,
762                v1::Snapshot::Height(height),
763                key,
764            )
765            .await
766            .map(Json)
767            .map_err(classify_availability_error)
768        };
769    let get_block_state_path_by_commit =
770        |State(state): State<S>, Path((commit, key)): Path<(String, String)>| async move {
771            <S as v1::BlockStateApi>::get_block_state_path(
772                &state,
773                v1::Snapshot::Commit(commit),
774                key,
775            )
776            .await
777            .map(Json)
778            .map_err(classify_availability_error)
779        };
780    let get_block_state_height = |State(state): State<S>| async move {
781        <S as v1::BlockStateApi>::get_block_state_height(&state)
782            .await
783            .map(Json)
784            .map_err(classify_availability_error)
785    };
786
787    // Merklized state handlers: fee-state
788    let get_fee_state_path_by_height =
789        |State(state): State<S>, Path((height, key)): Path<(u64, String)>| async move {
790            <S as v1::FeeStateApi>::get_fee_state_path(&state, v1::Snapshot::Height(height), key)
791                .await
792                .map(Json)
793                .map_err(classify_availability_error)
794        };
795    let get_fee_state_path_by_commit =
796        |State(state): State<S>, Path((commit, key)): Path<(String, String)>| async move {
797            <S as v1::FeeStateApi>::get_fee_state_path(&state, v1::Snapshot::Commit(commit), key)
798                .await
799                .map(Json)
800                .map_err(classify_availability_error)
801        };
802    let get_fee_state_height = |State(state): State<S>| async move {
803        <S as v1::FeeStateApi>::get_fee_state_height(&state)
804            .await
805            .map(Json)
806            .map_err(classify_availability_error)
807    };
808    let get_fee_balance_latest = |State(state): State<S>, Path(address): Path<String>| async move {
809        state
810            .get_fee_balance_latest(address)
811            .await
812            .map(Json)
813            .map_err(classify_availability_error)
814    };
815
816    let status_block_height = |State(state): State<S>| async move {
817        <S as v1::StatusApi>::block_height(&state)
818            .await
819            .map(Json)
820            .map_err(ApiError::Internal)
821    };
822    let status_success_rate = |State(state): State<S>| async move {
823        <S as v1::StatusApi>::success_rate(&state)
824            .await
825            .map(Json)
826            .map_err(ApiError::Internal)
827    };
828    let status_time_since_last_decide = |State(state): State<S>| async move {
829        <S as v1::StatusApi>::time_since_last_decide(&state)
830            .await
831            .map(Json)
832            .map_err(ApiError::Internal)
833    };
834    let status_metrics = |State(state): State<S>| async move {
835        match <S as v1::StatusApi>::metrics(&state).await {
836            Ok(text) => Ok((
837                [(
838                    axum::http::header::CONTENT_TYPE,
839                    "text/plain; charset=utf-8",
840                )],
841                text,
842            )),
843            Err(e) => Err(ApiError::Internal(e)),
844        }
845    };
846
847    let config_hotshot = |State(state): State<S>| async move {
848        <S as v1::ConfigApi>::hotshot_config(&state)
849            .await
850            .map(Json)
851            .map_err(ApiError::Internal)
852    };
853    let config_env = |State(state): State<S>| async move {
854        <S as v1::ConfigApi>::env(&state)
855            .await
856            .map(Json)
857            .map_err(ApiError::Internal)
858    };
859    let config_runtime = |State(state): State<S>| async move {
860        <S as v1::ConfigApi>::runtime_config(&state)
861            .await
862            .map(Json)
863            .map_err(classify_availability_error)
864    };
865
866    let node_block_height = |State(state): State<S>| async move {
867        <S as v1::NodeApi>::block_height(&state)
868            .await
869            .map(Json)
870            .map_err(classify_availability_error)
871    };
872
873    let node_count_txs = |State(state): State<S>| async move {
874        state
875            .count_transactions(None, None, None)
876            .await
877            .map(Json)
878            .map_err(classify_availability_error)
879    };
880    let node_count_txs_to = |State(state): State<S>, Path(to): Path<u64>| async move {
881        state
882            .count_transactions(None, Some(to), None)
883            .await
884            .map(Json)
885            .map_err(classify_availability_error)
886    };
887    let node_count_txs_from_to = |State(state): State<S>, Path((from, to)): Path<(u64, u64)>| async move {
888        state
889            .count_transactions(Some(from), Some(to), None)
890            .await
891            .map(Json)
892            .map_err(classify_availability_error)
893    };
894    let node_count_txs_ns = |State(state): State<S>, Path(namespace): Path<u64>| async move {
895        state
896            .count_transactions(None, None, Some(namespace))
897            .await
898            .map(Json)
899            .map_err(classify_availability_error)
900    };
901    let node_count_txs_ns_to = |State(state): State<S>, Path((namespace, to)): Path<(u64, u64)>| async move {
902        state
903            .count_transactions(None, Some(to), Some(namespace))
904            .await
905            .map(Json)
906            .map_err(classify_availability_error)
907    };
908    let node_count_txs_ns_from_to =
909        |State(state): State<S>, Path((namespace, from, to)): Path<(u64, u64, u64)>| async move {
910            state
911                .count_transactions(Some(from), Some(to), Some(namespace))
912                .await
913                .map(Json)
914                .map_err(classify_availability_error)
915        };
916
917    let node_payload_size = |State(state): State<S>| async move {
918        state
919            .payload_size(None, None, None)
920            .await
921            .map(Json)
922            .map_err(classify_availability_error)
923    };
924    let node_payload_size_to = |State(state): State<S>, Path(to): Path<u64>| async move {
925        state
926            .payload_size(None, Some(to), None)
927            .await
928            .map(Json)
929            .map_err(classify_availability_error)
930    };
931    let node_payload_size_from_to = |State(state): State<S>, Path((from, to)): Path<(u64, u64)>| async move {
932        state
933            .payload_size(Some(from), Some(to), None)
934            .await
935            .map(Json)
936            .map_err(classify_availability_error)
937    };
938    let node_payload_size_ns = |State(state): State<S>, Path(namespace): Path<u64>| async move {
939        state
940            .payload_size(None, None, Some(namespace))
941            .await
942            .map(Json)
943            .map_err(classify_availability_error)
944    };
945    let node_payload_size_ns_to =
946        |State(state): State<S>, Path((namespace, to)): Path<(u64, u64)>| async move {
947            state
948                .payload_size(None, Some(to), Some(namespace))
949                .await
950                .map(Json)
951                .map_err(classify_availability_error)
952        };
953    let node_payload_size_ns_from_to =
954        |State(state): State<S>, Path((namespace, from, to)): Path<(u64, u64, u64)>| async move {
955            state
956                .payload_size(Some(from), Some(to), Some(namespace))
957                .await
958                .map(Json)
959                .map_err(classify_availability_error)
960        };
961
962    let node_vid_share_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
963        state
964            .get_vid_share(v1::VidShareId::Height(height))
965            .await
966            .map(Json)
967            .map_err(classify_availability_error)
968    };
969    let node_vid_share_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
970        state
971            .get_vid_share(v1::VidShareId::Hash(hash))
972            .await
973            .map(Json)
974            .map_err(classify_availability_error)
975    };
976    let node_vid_share_by_payload_hash =
977        |State(state): State<S>, Path(payload_hash): Path<String>| async move {
978            state
979                .get_vid_share(v1::VidShareId::PayloadHash(payload_hash))
980                .await
981                .map(Json)
982                .map_err(classify_availability_error)
983        };
984
985    let node_sync_status = |State(state): State<S>| async move {
986        state
987            .sync_status()
988            .await
989            .map(Json)
990            .map_err(classify_availability_error)
991    };
992
993    let node_header_window_time = |State(state): State<S>, Path((start, end)): Path<(u64, u64)>| async move {
994        state
995            .get_header_window(v1::HeaderWindowStart::Time(start), end)
996            .await
997            .map(Json)
998            .map_err(classify_availability_error)
999    };
1000    let node_header_window_height =
1001        |State(state): State<S>, Path((height, end)): Path<(u64, u64)>| async move {
1002            state
1003                .get_header_window(v1::HeaderWindowStart::Height(height), end)
1004                .await
1005                .map(Json)
1006                .map_err(classify_availability_error)
1007        };
1008    let node_header_window_hash =
1009        |State(state): State<S>, Path((hash, end)): Path<(String, u64)>| async move {
1010            state
1011                .get_header_window(v1::HeaderWindowStart::Hash(hash), end)
1012                .await
1013                .map(Json)
1014                .map_err(classify_availability_error)
1015        };
1016
1017    let node_limits = |State(state): State<S>| async move {
1018        <S as v1::NodeApi>::limits(&state)
1019            .await
1020            .map(Json)
1021            .map_err(ApiError::Internal)
1022    };
1023
1024    let node_stake_table_current = |State(state): State<S>| async move {
1025        state
1026            .stake_table_current()
1027            .await
1028            .map(Json)
1029            .map_err(ApiError::Internal)
1030    };
1031    let node_stake_table = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1032        state
1033            .stake_table(epoch)
1034            .await
1035            .map(Json)
1036            .map_err(ApiError::Internal)
1037    };
1038    let node_da_stake_table_current = |State(state): State<S>| async move {
1039        state
1040            .da_stake_table_current()
1041            .await
1042            .map(Json)
1043            .map_err(ApiError::Internal)
1044    };
1045    let node_da_stake_table = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1046        state
1047            .da_stake_table(epoch)
1048            .await
1049            .map(Json)
1050            .map_err(ApiError::Internal)
1051    };
1052
1053    let node_validators = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1054        state
1055            .get_validators(epoch)
1056            .await
1057            .map(Json)
1058            .map_err(ApiError::Internal)
1059    };
1060    let node_all_validators =
1061        |State(state): State<S>, Path((epoch, offset, limit)): Path<(u64, u64, u64)>| async move {
1062            state
1063                .get_all_validators(epoch, offset, limit)
1064                .await
1065                .map(Json)
1066                .map_err(ApiError::BadRequest)
1067        };
1068
1069    let node_proposal_participation_current = |State(state): State<S>| async move {
1070        state
1071            .current_proposal_participation()
1072            .await
1073            .map(Json)
1074            .map_err(ApiError::Internal)
1075    };
1076    let node_proposal_participation = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1077        state
1078            .proposal_participation(epoch)
1079            .await
1080            .map(Json)
1081            .map_err(ApiError::Internal)
1082    };
1083    let node_vote_participation_current = |State(state): State<S>| async move {
1084        state
1085            .current_vote_participation()
1086            .await
1087            .map(Json)
1088            .map_err(ApiError::Internal)
1089    };
1090    let node_vote_participation = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1091        state
1092            .vote_participation(epoch)
1093            .await
1094            .map(Json)
1095            .map_err(ApiError::Internal)
1096    };
1097
1098    let node_block_reward = |State(state): State<S>| async move {
1099        state
1100            .get_block_reward(None)
1101            .await
1102            .map(Json)
1103            .map_err(ApiError::Internal)
1104    };
1105    let node_block_reward_epoch = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1106        state
1107            .get_block_reward(Some(epoch))
1108            .await
1109            .map(Json)
1110            .map_err(ApiError::Internal)
1111    };
1112
1113    let node_oldest_block = |State(state): State<S>| async move {
1114        state
1115            .get_oldest_block()
1116            .await
1117            .map(Json)
1118            .map_err(ApiError::Internal)
1119    };
1120    let node_oldest_leaf = |State(state): State<S>| async move {
1121        state
1122            .get_oldest_leaf()
1123            .await
1124            .map(Json)
1125            .map_err(ApiError::Internal)
1126    };
1127
1128    // Catchup handlers
1129    let catchup_account =
1130        |State(state): State<S>, Path((height, view, address)): Path<(u64, u64, String)>| async move {
1131            state
1132                .get_account(height, view, address)
1133                .await
1134                .map(Json)
1135                .map_err(classify_availability_error)
1136        };
1137    let catchup_accounts = |State(state): State<S>,
1138                            Path((height, view)): Path<(u64, u64)>,
1139                            headers: HeaderMap,
1140                            body: Bytes| async move {
1141        let accounts: Vec<<S as v1::CatchupApi>::FeeAccount> = decode_body(&headers, &body)?;
1142        let tree = state
1143            .get_accounts(height, view, accounts)
1144            .await
1145            .map_err(classify_availability_error)?;
1146        encode_response(&headers, tree)
1147    };
1148    let catchup_blocks = |State(state): State<S>, Path((height, view)): Path<(u64, u64)>| async move {
1149        state
1150            .get_blocks_frontier(height, view)
1151            .await
1152            .map(Json)
1153            .map_err(classify_availability_error)
1154    };
1155    let catchup_chainconfig = |State(state): State<S>, Path(commitment): Path<String>| async move {
1156        <S as v1::CatchupApi>::get_chain_config(&state, commitment)
1157            .await
1158            .map(Json)
1159            .map_err(classify_availability_error)
1160    };
1161    let catchup_leafchain = |State(state): State<S>, Path(height): Path<u64>| async move {
1162        state
1163            .get_leaf_chain(height)
1164            .await
1165            .map(Json)
1166            .map_err(classify_availability_error)
1167    };
1168    let catchup_cert2 = |State(state): State<S>, Path(height): Path<u64>| async move {
1169        <S as v1::CatchupApi>::get_cert2(&state, height)
1170            .await
1171            .map(Json)
1172            .map_err(classify_availability_error)
1173    };
1174    let catchup_reward_account =
1175        |State(state): State<S>, Path((height, view, address)): Path<(u64, u64, String)>| async move {
1176            state
1177                .get_reward_account_v1(height, view, address)
1178                .await
1179                .map(Json)
1180                .map_err(classify_availability_error)
1181        };
1182    let catchup_reward_accounts = |State(state): State<S>,
1183                                   Path((height, view)): Path<(u64, u64)>,
1184                                   headers: HeaderMap,
1185                                   body: Bytes| async move {
1186        let accounts: Vec<<S as v1::CatchupApi>::RewardAccountV1> = decode_body(&headers, &body)?;
1187        let tree = state
1188            .get_reward_accounts_v1(height, view, accounts)
1189            .await
1190            .map_err(classify_availability_error)?;
1191        encode_response(&headers, tree)
1192    };
1193    let catchup_reward_account_v2 =
1194        |State(state): State<S>, Path((height, view, address)): Path<(u64, u64, String)>| async move {
1195            state
1196                .get_reward_account_v2(height, view, address)
1197                .await
1198                .map(Json)
1199                .map_err(classify_availability_error)
1200        };
1201    let catchup_reward_accounts_v2 =
1202        |State(_): State<S>, Path((_height, _view)): Path<(u64, u64)>| async move {
1203            Err::<Json<()>, ApiError>(ApiError::NotFound(anyhow::anyhow!(
1204                "catchup/reward-accounts-v2 is deprecated"
1205            )))
1206        };
1207    let catchup_reward_amounts =
1208        |State(_): State<S>, Path((_height, _limit, _offset)): Path<(u64, u64, u64)>| async move {
1209            Err::<Json<()>, ApiError>(ApiError::NotFound(anyhow::anyhow!(
1210                "catchup/reward-amounts is deprecated"
1211            )))
1212        };
1213    let catchup_reward_merkle_tree_v2 =
1214        |State(state): State<S>, Path((height, view)): Path<(u64, u64)>| async move {
1215            <S as v1::CatchupApi>::get_reward_merkle_tree_v2(&state, height, view)
1216                .await
1217                .map(Json)
1218                .map_err(classify_availability_error)
1219        };
1220    let catchup_state_cert = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1221        <S as v1::CatchupApi>::get_state_cert(&state, epoch)
1222            .await
1223            .map(Json)
1224            .map_err(classify_availability_error)
1225    };
1226
1227    // Submit handler — body is decoded as VBS (binary) or JSON based on Content-Type, matching
1228    // tide-disco's `body_auto`.
1229    let submit_submit = |State(state): State<S>, headers: HeaderMap, body: Bytes| async move {
1230        let tx: <S as v1::SubmitApi>::Transaction = decode_body(&headers, &body)?;
1231        let hash = state.submit(tx).await.map_err(ApiError::Internal)?;
1232        encode_response(&headers, hash)
1233    };
1234
1235    // State signature handler
1236    let state_signature_block = |State(state): State<S>, Path(height): Path<u64>| async move {
1237        state
1238            .get_state_signature(height)
1239            .await
1240            .map(Json)
1241            .map_err(classify_availability_error)
1242    };
1243
1244    // HotShot events handlers
1245    let hotshot_events_startup = |State(state): State<S>| async move {
1246        state
1247            .startup_info()
1248            .await
1249            .map(Json)
1250            .map_err(ApiError::Internal)
1251    };
1252    let hotshot_events_stream =
1253        |State(state): State<S>, headers: HeaderMap, ws: WebSocketUpgrade| async move {
1254            let format = ws_format(&headers);
1255            match <S as v1::HotShotEventsApi>::events(&state).await {
1256                Ok(stream) => ws.on_upgrade(move |socket| drive_ws_stream(socket, stream, format)),
1257                Err(err) => ApiError::Internal(err).into_response(),
1258            }
1259        };
1260
1261    // Light-client handlers
1262    let lc_leaf_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
1263        state
1264            .get_leaf_proof(v1::LeafQuery::Height(height), None)
1265            .await
1266            .map(Json)
1267            .map_err(classify_availability_error)
1268    };
1269    let lc_leaf_by_height_finalized =
1270        |State(state): State<S>, Path((height, finalized)): Path<(u64, u64)>| async move {
1271            state
1272                .get_leaf_proof(v1::LeafQuery::Height(height), Some(finalized))
1273                .await
1274                .map(Json)
1275                .map_err(classify_availability_error)
1276        };
1277    let lc_leaf_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
1278        state
1279            .get_leaf_proof(v1::LeafQuery::Hash(hash), None)
1280            .await
1281            .map(Json)
1282            .map_err(classify_availability_error)
1283    };
1284    let lc_leaf_by_hash_finalized =
1285        |State(state): State<S>, Path((hash, finalized)): Path<(String, u64)>| async move {
1286            state
1287                .get_leaf_proof(v1::LeafQuery::Hash(hash), Some(finalized))
1288                .await
1289                .map(Json)
1290                .map_err(classify_availability_error)
1291        };
1292    let lc_leaf_by_block_hash = |State(state): State<S>, Path(block_hash): Path<String>| async move {
1293        state
1294            .get_leaf_proof(v1::LeafQuery::BlockHash(block_hash), None)
1295            .await
1296            .map(Json)
1297            .map_err(classify_availability_error)
1298    };
1299    let lc_leaf_by_block_hash_finalized =
1300        |State(state): State<S>, Path((block_hash, finalized)): Path<(String, u64)>| async move {
1301            state
1302                .get_leaf_proof(v1::LeafQuery::BlockHash(block_hash), Some(finalized))
1303                .await
1304                .map(Json)
1305                .map_err(classify_availability_error)
1306        };
1307    let lc_leaf_by_payload_hash = |State(state): State<S>, Path(payload_hash): Path<String>| async move {
1308        state
1309            .get_leaf_proof(v1::LeafQuery::PayloadHash(payload_hash), None)
1310            .await
1311            .map(Json)
1312            .map_err(classify_availability_error)
1313    };
1314    let lc_leaf_by_payload_hash_finalized =
1315        |State(state): State<S>, Path((payload_hash, finalized)): Path<(String, u64)>| async move {
1316            state
1317                .get_leaf_proof(v1::LeafQuery::PayloadHash(payload_hash), Some(finalized))
1318                .await
1319                .map(Json)
1320                .map_err(classify_availability_error)
1321        };
1322
1323    let lc_header_by_height = |State(state): State<S>, Path((root, height)): Path<(u64, u64)>| async move {
1324        state
1325            .get_header_proof(root, v1::HeaderQuery::Height(height))
1326            .await
1327            .map(Json)
1328            .map_err(classify_availability_error)
1329    };
1330    let lc_header_by_hash = |State(state): State<S>, Path((root, hash)): Path<(u64, String)>| async move {
1331        state
1332            .get_header_proof(root, v1::HeaderQuery::Hash(hash))
1333            .await
1334            .map(Json)
1335            .map_err(classify_availability_error)
1336    };
1337    let lc_header_by_payload_hash =
1338        |State(state): State<S>, Path((root, payload_hash)): Path<(u64, String)>| async move {
1339            state
1340                .get_header_proof(root, v1::HeaderQuery::PayloadHash(payload_hash))
1341                .await
1342                .map(Json)
1343                .map_err(classify_availability_error)
1344        };
1345    let lc_stake_table = |State(state): State<S>, Path(epoch): Path<u64>| async move {
1346        state
1347            .get_light_client_stake_table(epoch)
1348            .await
1349            .map(Json)
1350            .map_err(classify_availability_error)
1351    };
1352    let lc_payload = |State(state): State<S>, Path(height): Path<u64>| async move {
1353        state
1354            .get_payload_proof(height)
1355            .await
1356            .map(Json)
1357            .map_err(classify_availability_error)
1358    };
1359    let lc_payload_range = |State(state): State<S>, Path((start, end)): Path<(u64, u64)>| async move {
1360        state
1361            .get_payload_proof_range(start, end)
1362            .await
1363            .map(Json)
1364            .map_err(classify_availability_error)
1365    };
1366    let lc_namespace = |State(state): State<S>, Path((height, namespace)): Path<(u64, u64)>| async move {
1367        state
1368            .get_lc_namespace_proof(height, namespace)
1369            .await
1370            .map(Json)
1371            .map_err(classify_availability_error)
1372    };
1373    let lc_namespace_range =
1374        |State(state): State<S>, Path((start, end, namespace)): Path<(u64, u64, u64)>| async move {
1375            state
1376                .get_lc_namespace_proof_range(start, end, namespace)
1377                .await
1378                .map(Json)
1379                .map_err(classify_availability_error)
1380        };
1381
1382    // Explorer handlers
1383    let explorer_block_detail_by_height = |State(state): State<S>, Path(height): Path<u64>| async move {
1384        state
1385            .get_block_detail(v1::BlockIdent::Height(height))
1386            .await
1387            .map(Json)
1388            .map_err(classify_availability_error)
1389    };
1390    let explorer_block_detail_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
1391        state
1392            .get_block_detail(v1::BlockIdent::Hash(hash))
1393            .await
1394            .map(Json)
1395            .map_err(classify_availability_error)
1396    };
1397    let explorer_block_summaries_latest = |State(state): State<S>, Path(limit): Path<u64>| async move {
1398        state
1399            .get_block_summaries(v1::BlockIdent::Latest, limit)
1400            .await
1401            .map(Json)
1402            .map_err(classify_availability_error)
1403    };
1404    let explorer_block_summaries_from =
1405        |State(state): State<S>, Path((from, limit)): Path<(u64, u64)>| async move {
1406            state
1407                .get_block_summaries(v1::BlockIdent::Height(from), limit)
1408                .await
1409                .map(Json)
1410                .map_err(classify_availability_error)
1411        };
1412    let explorer_tx_detail_by_position =
1413        |State(state): State<S>, Path((height, offset)): Path<(u64, u64)>| async move {
1414            state
1415                .get_transaction_detail(v1::TxIdent::HeightAndOffset(height, offset))
1416                .await
1417                .map(Json)
1418                .map_err(classify_availability_error)
1419        };
1420    let explorer_tx_detail_by_hash = |State(state): State<S>, Path(hash): Path<String>| async move {
1421        state
1422            .get_transaction_detail(v1::TxIdent::Hash(hash))
1423            .await
1424            .map(Json)
1425            .map_err(classify_availability_error)
1426    };
1427    let explorer_tx_summaries_latest = |State(state): State<S>, Path(limit): Path<u64>| async move {
1428        state
1429            .get_transaction_summaries(v1::TxIdent::Latest, limit, v1::TxSummaryFilter::None)
1430            .await
1431            .map(Json)
1432            .map_err(classify_availability_error)
1433    };
1434    let explorer_tx_summaries_from =
1435        |State(state): State<S>, Path((height, offset, limit)): Path<(u64, u64, u64)>| async move {
1436            state
1437                .get_transaction_summaries(
1438                    v1::TxIdent::HeightAndOffset(height, offset),
1439                    limit,
1440                    v1::TxSummaryFilter::None,
1441                )
1442                .await
1443                .map(Json)
1444                .map_err(classify_availability_error)
1445        };
1446    let explorer_tx_summaries_by_hash =
1447        |State(state): State<S>, Path((hash, limit)): Path<(String, u64)>| async move {
1448            state
1449                .get_transaction_summaries(
1450                    v1::TxIdent::Hash(hash),
1451                    limit,
1452                    v1::TxSummaryFilter::None,
1453                )
1454                .await
1455                .map(Json)
1456                .map_err(classify_availability_error)
1457        };
1458    let explorer_tx_summaries_latest_block =
1459        |State(state): State<S>, Path((limit, block)): Path<(u64, u64)>| async move {
1460            state
1461                .get_transaction_summaries(
1462                    v1::TxIdent::Latest,
1463                    limit,
1464                    v1::TxSummaryFilter::Block(block),
1465                )
1466                .await
1467                .map(Json)
1468                .map_err(classify_availability_error)
1469        };
1470    let explorer_tx_summaries_from_block =
1471        |State(state): State<S>,
1472         Path((height, offset, limit, block)): Path<(u64, u64, u64, u64)>| async move {
1473            state
1474                .get_transaction_summaries(
1475                    v1::TxIdent::HeightAndOffset(height, offset),
1476                    limit,
1477                    v1::TxSummaryFilter::Block(block),
1478                )
1479                .await
1480                .map(Json)
1481                .map_err(classify_availability_error)
1482        };
1483    let explorer_tx_summaries_by_hash_block =
1484        |State(state): State<S>, Path((hash, limit, block)): Path<(String, u64, u64)>| async move {
1485            state
1486                .get_transaction_summaries(
1487                    v1::TxIdent::Hash(hash),
1488                    limit,
1489                    v1::TxSummaryFilter::Block(block),
1490                )
1491                .await
1492                .map(Json)
1493                .map_err(classify_availability_error)
1494        };
1495    let explorer_tx_summaries_latest_ns =
1496        |State(state): State<S>, Path((limit, namespace)): Path<(u64, i64)>| async move {
1497            state
1498                .get_transaction_summaries(
1499                    v1::TxIdent::Latest,
1500                    limit,
1501                    v1::TxSummaryFilter::Namespace(namespace),
1502                )
1503                .await
1504                .map(Json)
1505                .map_err(classify_availability_error)
1506        };
1507    let explorer_tx_summaries_from_ns =
1508        |State(state): State<S>,
1509         Path((height, offset, limit, namespace)): Path<(u64, u64, u64, i64)>| async move {
1510            state
1511                .get_transaction_summaries(
1512                    v1::TxIdent::HeightAndOffset(height, offset),
1513                    limit,
1514                    v1::TxSummaryFilter::Namespace(namespace),
1515                )
1516                .await
1517                .map(Json)
1518                .map_err(classify_availability_error)
1519        };
1520    let explorer_tx_summaries_by_hash_ns = |State(state): State<S>,
1521                                            Path((hash, limit, namespace)): Path<(
1522        String,
1523        u64,
1524        i64,
1525    )>| async move {
1526        state
1527            .get_transaction_summaries(
1528                v1::TxIdent::Hash(hash),
1529                limit,
1530                v1::TxSummaryFilter::Namespace(namespace),
1531            )
1532            .await
1533            .map(Json)
1534            .map_err(classify_availability_error)
1535    };
1536    let explorer_summary = |State(state): State<S>| async move {
1537        state
1538            .get_explorer_summary()
1539            .await
1540            .map(Json)
1541            .map_err(classify_availability_error)
1542    };
1543    let explorer_search = |State(state): State<S>, Path(query): Path<String>| async move {
1544        state
1545            .get_search_result(query)
1546            .await
1547            .map(Json)
1548            .map_err(classify_availability_error)
1549    };
1550
1551    // Token handlers
1552    let token_total_minted = |State(state): State<S>| async move {
1553        state
1554            .total_minted_supply()
1555            .await
1556            .map(Json)
1557            .map_err(classify_availability_error)
1558    };
1559    let token_circulating = |State(state): State<S>| async move {
1560        state
1561            .circulating_supply()
1562            .await
1563            .map(Json)
1564            .map_err(classify_availability_error)
1565    };
1566    let token_circulating_eth = |State(state): State<S>| async move {
1567        state
1568            .circulating_supply_ethereum()
1569            .await
1570            .map(Json)
1571            .map_err(classify_availability_error)
1572    };
1573    let token_total_issued = |State(state): State<S>| async move {
1574        state
1575            .total_issued_supply()
1576            .await
1577            .map(Json)
1578            .map_err(classify_availability_error)
1579    };
1580    let token_total_reward_distributed = |State(state): State<S>| async move {
1581        state
1582            .total_reward_distributed()
1583            .await
1584            .map(Json)
1585            .map_err(classify_availability_error)
1586    };
1587
1588    // Database handlers
1589    let database_table_sizes = |State(state): State<S>| async move {
1590        <S as v1::DatabaseApi>::get_table_sizes(&state)
1591            .await
1592            .map(Json)
1593            .map_err(ApiError::Internal)
1594    };
1595
1596    // Build plain Axum router without OpenAPI (for v1 - internal types)
1597    Router::new()
1598        .route(
1599            routes::v1::REWARD_CLAIM_INPUT_ROUTE,
1600            get(get_reward_claim_input),
1601        )
1602        .route(routes::v1::REWARD_BALANCE_ROUTE, get(get_reward_balance))
1603        .route(
1604            routes::v1::LATEST_REWARD_BALANCE_ROUTE,
1605            get(get_latest_reward_balance),
1606        )
1607        .route(
1608            routes::v1::REWARD_ACCOUNT_PROOF_ROUTE,
1609            get(get_reward_account_proof),
1610        )
1611        .route(
1612            routes::v1::LATEST_REWARD_ACCOUNT_PROOF_ROUTE,
1613            get(get_latest_reward_account_proof),
1614        )
1615        .route(routes::v1::REWARD_AMOUNTS_ROUTE, get(get_reward_amounts))
1616        .route(
1617            routes::v1::REWARD_MERKLE_TREE_V2_ROUTE,
1618            get(get_reward_merkle_tree_v2),
1619        )
1620        // Availability API routes
1621        .route(
1622            routes::v1::NAMESPACE_PROOF_BY_HEIGHT_ROUTE,
1623            get(get_namespace_proof_by_height),
1624        )
1625        .route(
1626            routes::v1::NAMESPACE_PROOF_BY_HASH_ROUTE,
1627            get(get_namespace_proof_by_hash),
1628        )
1629        .route(
1630            routes::v1::NAMESPACE_PROOF_BY_PAYLOAD_HASH_ROUTE,
1631            get(get_namespace_proof_by_payload_hash),
1632        )
1633        .route(
1634            routes::v1::NAMESPACE_PROOF_RANGE_ROUTE,
1635            get(get_namespace_proof_range),
1636        )
1637        .route(
1638            routes::v1::INCORRECT_ENCODING_PROOF_ROUTE,
1639            get(get_incorrect_encoding_proof),
1640        )
1641        .route(routes::v1::STATE_CERT_V1_ROUTE, get(get_state_cert_v1))
1642        .route(routes::v1::STATE_CERT_V2_ROUTE, get(get_state_cert_v2))
1643        // HotShot availability API routes
1644        .route(routes::v1::LEAF_BY_HEIGHT_ROUTE, get(get_leaf_by_height))
1645        .route(routes::v1::LEAF_BY_HASH_ROUTE, get(get_leaf_by_hash))
1646        .route(routes::v1::LEAF_RANGE_ROUTE, get(get_leaf_range))
1647        .route(
1648            routes::v1::HEADER_BY_HEIGHT_ROUTE,
1649            get(get_header_by_height),
1650        )
1651        .route(routes::v1::HEADER_BY_HASH_ROUTE, get(get_header_by_hash))
1652        .route(
1653            routes::v1::HEADER_BY_PAYLOAD_HASH_ROUTE,
1654            get(get_header_by_payload_hash),
1655        )
1656        .route(routes::v1::HEADER_RANGE_ROUTE, get(get_header_range))
1657        .route(routes::v1::BLOCK_BY_HEIGHT_ROUTE, get(get_block_by_height))
1658        .route(routes::v1::BLOCK_BY_HASH_ROUTE, get(get_block_by_hash))
1659        .route(
1660            routes::v1::BLOCK_BY_PAYLOAD_HASH_ROUTE,
1661            get(get_block_by_payload_hash),
1662        )
1663        .route(routes::v1::BLOCK_RANGE_ROUTE, get(get_block_range))
1664        .route(
1665            routes::v1::PAYLOAD_BY_HEIGHT_ROUTE,
1666            get(get_payload_by_height),
1667        )
1668        .route(
1669            routes::v1::PAYLOAD_BY_HASH_ROUTE,
1670            get(get_payload_by_hash),
1671        )
1672        .route(
1673            routes::v1::PAYLOAD_BY_BLOCK_HASH_ROUTE,
1674            get(get_payload_by_block_hash),
1675        )
1676        .route(routes::v1::PAYLOAD_RANGE_ROUTE, get(get_payload_range))
1677        .route(
1678            routes::v1::VID_COMMON_BY_HEIGHT_ROUTE,
1679            get(get_vid_common_by_height),
1680        )
1681        .route(
1682            routes::v1::VID_COMMON_BY_HASH_ROUTE,
1683            get(get_vid_common_by_hash),
1684        )
1685        .route(
1686            routes::v1::VID_COMMON_BY_PAYLOAD_HASH_ROUTE,
1687            get(get_vid_common_by_payload_hash),
1688        )
1689        .route(
1690            routes::v1::VID_COMMON_RANGE_ROUTE,
1691            get(get_vid_common_range),
1692        )
1693        .route(
1694            routes::v1::TRANSACTION_BY_POSITION_NOPROOF_ROUTE,
1695            get(get_transaction_by_position),
1696        )
1697        .route(
1698            routes::v1::TRANSACTION_BY_HASH_NOPROOF_ROUTE,
1699            get(get_transaction_by_hash),
1700        )
1701        .route(
1702            routes::v1::TRANSACTION_PROOF_BY_POSITION_ROUTE,
1703            get(get_transaction_proof_by_position),
1704        )
1705        .route(
1706            routes::v1::TRANSACTION_PROOF_BY_HASH_ROUTE,
1707            get(get_transaction_proof_by_hash),
1708        )
1709        .route(
1710            routes::v1::TRANSACTION_BY_POSITION_ROUTE,
1711            get(get_transaction_proof_by_position),
1712        )
1713        .route(
1714            routes::v1::TRANSACTION_BY_HASH_ROUTE,
1715            get(get_transaction_proof_by_hash),
1716        )
1717        .route(
1718            routes::v1::BLOCK_SUMMARY_BY_HEIGHT_ROUTE,
1719            get(get_block_summary_by_height),
1720        )
1721        .route(
1722            routes::v1::BLOCK_SUMMARY_RANGE_ROUTE,
1723            get(get_block_summary_range),
1724        )
1725        .route(routes::v1::LIMITS_ROUTE, get(get_limits))
1726        .route(routes::v1::CERT2_BY_HEIGHT_ROUTE, get(get_cert2))
1727        // WebSocket streaming routes
1728        .route(routes::v1::STREAM_LEAVES_ROUTE, get(stream_leaves))
1729        .route(routes::v1::STREAM_HEADERS_ROUTE, get(stream_headers))
1730        .route(routes::v1::STREAM_BLOCKS_ROUTE, get(stream_blocks))
1731        .route(routes::v1::STREAM_PAYLOADS_ROUTE, get(stream_payloads))
1732        .route(routes::v1::STREAM_VID_COMMON_ROUTE, get(stream_vid_common))
1733        .route(
1734            routes::v1::STREAM_TRANSACTIONS_ROUTE,
1735            get(stream_transactions),
1736        )
1737        .route(
1738            routes::v1::STREAM_TRANSACTIONS_NS_ROUTE,
1739            get(stream_transactions_ns),
1740        )
1741        .route(
1742            routes::v1::STREAM_NAMESPACE_PROOFS_ROUTE,
1743            get(stream_namespace_proofs),
1744        )
1745        // Merklized state: block-state.
1746        .route(
1747            routes::v1::BLOCK_STATE_HEIGHT_ROUTE,
1748            get(get_block_state_height),
1749        )
1750        .route(
1751            routes::v1::BLOCK_STATE_PATH_BY_COMMIT_ROUTE,
1752            get(get_block_state_path_by_commit),
1753        )
1754        .route(
1755            routes::v1::BLOCK_STATE_PATH_BY_HEIGHT_ROUTE,
1756            get(get_block_state_path_by_height),
1757        )
1758        // Merklized state: fee-state
1759        .route(
1760            routes::v1::FEE_STATE_HEIGHT_ROUTE,
1761            get(get_fee_state_height),
1762        )
1763        .route(
1764            routes::v1::FEE_STATE_BALANCE_LATEST_ROUTE,
1765            get(get_fee_balance_latest),
1766        )
1767        .route(
1768            routes::v1::FEE_STATE_PATH_BY_COMMIT_ROUTE,
1769            get(get_fee_state_path_by_commit),
1770        )
1771        .route(
1772            routes::v1::FEE_STATE_PATH_BY_HEIGHT_ROUTE,
1773            get(get_fee_state_path_by_height),
1774        )
1775        // Status routes
1776        .route(routes::v1::STATUS_BLOCK_HEIGHT_ROUTE, get(status_block_height))
1777        .route(routes::v1::STATUS_SUCCESS_RATE_ROUTE, get(status_success_rate))
1778        .route(
1779            routes::v1::STATUS_TIME_SINCE_LAST_DECIDE_ROUTE,
1780            get(status_time_since_last_decide),
1781        )
1782        .route(routes::v1::STATUS_METRICS_ROUTE, get(status_metrics))
1783        // Config routes
1784        .route(routes::v1::CONFIG_HOTSHOT_ROUTE, get(config_hotshot))
1785        .route(routes::v1::CONFIG_ENV_ROUTE, get(config_env))
1786        .route(routes::v1::CONFIG_RUNTIME_ROUTE, get(config_runtime))
1787        // Node routes
1788        .route(routes::v1::NODE_BLOCK_HEIGHT_ROUTE, get(node_block_height))
1789        .route(routes::v1::NODE_TRANSACTIONS_COUNT_ROUTE, get(node_count_txs))
1790        .route(
1791            routes::v1::NODE_TRANSACTIONS_COUNT_NS_ROUTE,
1792            get(node_count_txs_ns),
1793        )
1794        .route(
1795            routes::v1::NODE_TRANSACTIONS_COUNT_NS_TO_ROUTE,
1796            get(node_count_txs_ns_to),
1797        )
1798        .route(
1799            routes::v1::NODE_TRANSACTIONS_COUNT_NS_FROM_TO_ROUTE,
1800            get(node_count_txs_ns_from_to),
1801        )
1802        .route(
1803            routes::v1::NODE_TRANSACTIONS_COUNT_TO_ROUTE,
1804            get(node_count_txs_to),
1805        )
1806        .route(
1807            routes::v1::NODE_TRANSACTIONS_COUNT_FROM_TO_ROUTE,
1808            get(node_count_txs_from_to),
1809        )
1810        .route(routes::v1::NODE_PAYLOADS_SIZE_ROUTE, get(node_payload_size))
1811        .route(
1812            routes::v1::NODE_PAYLOADS_TOTAL_SIZE_ROUTE,
1813            get(node_payload_size),
1814        )
1815        .route(
1816            routes::v1::NODE_PAYLOADS_SIZE_NS_ROUTE,
1817            get(node_payload_size_ns),
1818        )
1819        .route(
1820            routes::v1::NODE_PAYLOADS_SIZE_NS_TO_ROUTE,
1821            get(node_payload_size_ns_to),
1822        )
1823        .route(
1824            routes::v1::NODE_PAYLOADS_SIZE_NS_FROM_TO_ROUTE,
1825            get(node_payload_size_ns_from_to),
1826        )
1827        .route(
1828            routes::v1::NODE_PAYLOADS_SIZE_TO_ROUTE,
1829            get(node_payload_size_to),
1830        )
1831        .route(
1832            routes::v1::NODE_PAYLOADS_SIZE_FROM_TO_ROUTE,
1833            get(node_payload_size_from_to),
1834        )
1835        .route(
1836            routes::v1::NODE_VID_SHARE_BY_HASH_ROUTE,
1837            get(node_vid_share_by_hash),
1838        )
1839        .route(
1840            routes::v1::NODE_VID_SHARE_BY_PAYLOAD_HASH_ROUTE,
1841            get(node_vid_share_by_payload_hash),
1842        )
1843        .route(
1844            routes::v1::NODE_VID_SHARE_BY_HEIGHT_ROUTE,
1845            get(node_vid_share_by_height),
1846        )
1847        .route(routes::v1::NODE_SYNC_STATUS_ROUTE, get(node_sync_status))
1848        .route(
1849            routes::v1::NODE_HEADER_WINDOW_HASH_ROUTE,
1850            get(node_header_window_hash),
1851        )
1852        .route(
1853            routes::v1::NODE_HEADER_WINDOW_HEIGHT_ROUTE,
1854            get(node_header_window_height),
1855        )
1856        .route(
1857            routes::v1::NODE_HEADER_WINDOW_TIME_ROUTE,
1858            get(node_header_window_time),
1859        )
1860        .route(routes::v1::NODE_LIMITS_ROUTE, get(node_limits))
1861        .route(
1862            routes::v1::NODE_STAKE_TABLE_CURRENT_ROUTE,
1863            get(node_stake_table_current),
1864        )
1865        .route(routes::v1::NODE_STAKE_TABLE_ROUTE, get(node_stake_table))
1866        .route(
1867            routes::v1::NODE_DA_STAKE_TABLE_CURRENT_ROUTE,
1868            get(node_da_stake_table_current),
1869        )
1870        .route(routes::v1::NODE_DA_STAKE_TABLE_ROUTE, get(node_da_stake_table))
1871        .route(routes::v1::NODE_VALIDATORS_ROUTE, get(node_validators))
1872        .route(routes::v1::NODE_ALL_VALIDATORS_ROUTE, get(node_all_validators))
1873        .route(
1874            routes::v1::NODE_PROPOSAL_PARTICIPATION_CURRENT_ROUTE,
1875            get(node_proposal_participation_current),
1876        )
1877        .route(
1878            routes::v1::NODE_PROPOSAL_PARTICIPATION_ROUTE,
1879            get(node_proposal_participation),
1880        )
1881        .route(
1882            routes::v1::NODE_VOTE_PARTICIPATION_CURRENT_ROUTE,
1883            get(node_vote_participation_current),
1884        )
1885        .route(
1886            routes::v1::NODE_VOTE_PARTICIPATION_ROUTE,
1887            get(node_vote_participation),
1888        )
1889        .route(routes::v1::NODE_BLOCK_REWARD_ROUTE, get(node_block_reward))
1890        .route(
1891            routes::v1::NODE_BLOCK_REWARD_EPOCH_ROUTE,
1892            get(node_block_reward_epoch),
1893        )
1894        .route(routes::v1::NODE_OLDEST_BLOCK_ROUTE, get(node_oldest_block))
1895        .route(routes::v1::NODE_OLDEST_LEAF_ROUTE, get(node_oldest_leaf))
1896        // Catchup routes
1897        .route(routes::v1::CATCHUP_ACCOUNT_ROUTE, get(catchup_account))
1898        .route(routes::v1::CATCHUP_ACCOUNTS_ROUTE, ::axum::routing::post(catchup_accounts))
1899        .route(routes::v1::CATCHUP_BLOCKS_ROUTE, get(catchup_blocks))
1900        .route(routes::v1::CATCHUP_CHAINCONFIG_ROUTE, get(catchup_chainconfig))
1901        .route(routes::v1::CATCHUP_LEAFCHAIN_ROUTE, get(catchup_leafchain))
1902        .route(routes::v1::CATCHUP_CERT2_ROUTE, get(catchup_cert2))
1903        .route(
1904            routes::v1::CATCHUP_REWARD_ACCOUNT_ROUTE,
1905            get(catchup_reward_account),
1906        )
1907        .route(
1908            routes::v1::CATCHUP_REWARD_ACCOUNTS_ROUTE,
1909            ::axum::routing::post(catchup_reward_accounts),
1910        )
1911        .route(
1912            routes::v1::CATCHUP_REWARD_ACCOUNT_V2_ROUTE,
1913            get(catchup_reward_account_v2),
1914        )
1915        .route(
1916            routes::v1::CATCHUP_REWARD_ACCOUNTS_V2_ROUTE,
1917            ::axum::routing::post(catchup_reward_accounts_v2),
1918        )
1919        .route(
1920            routes::v1::CATCHUP_REWARD_AMOUNTS_ROUTE,
1921            get(catchup_reward_amounts),
1922        )
1923        .route(
1924            routes::v1::CATCHUP_REWARD_MERKLE_TREE_V2_ROUTE,
1925            get(catchup_reward_merkle_tree_v2),
1926        )
1927        .route(routes::v1::CATCHUP_STATE_CERT_ROUTE, get(catchup_state_cert))
1928        // Submit
1929        .route(routes::v1::SUBMIT_ROUTE, ::axum::routing::post(submit_submit))
1930        // State signature
1931        .route(
1932            routes::v1::STATE_SIGNATURE_BLOCK_ROUTE,
1933            get(state_signature_block),
1934        )
1935        // HotShot events
1936        .route(
1937            routes::v1::HOTSHOT_EVENTS_STARTUP_ROUTE,
1938            get(hotshot_events_startup),
1939        )
1940        .route(
1941            routes::v1::HOTSHOT_EVENTS_STREAM_ROUTE,
1942            get(hotshot_events_stream),
1943        )
1944        // Light client
1945        .route(routes::v1::LC_LEAF_BY_HEIGHT_ROUTE, get(lc_leaf_by_height))
1946        .route(
1947            routes::v1::LC_LEAF_BY_HEIGHT_FINALIZED_ROUTE,
1948            get(lc_leaf_by_height_finalized),
1949        )
1950        .route(routes::v1::LC_LEAF_BY_HASH_ROUTE, get(lc_leaf_by_hash))
1951        .route(
1952            routes::v1::LC_LEAF_BY_HASH_FINALIZED_ROUTE,
1953            get(lc_leaf_by_hash_finalized),
1954        )
1955        .route(
1956            routes::v1::LC_LEAF_BY_BLOCK_HASH_ROUTE,
1957            get(lc_leaf_by_block_hash),
1958        )
1959        .route(
1960            routes::v1::LC_LEAF_BY_BLOCK_HASH_FINALIZED_ROUTE,
1961            get(lc_leaf_by_block_hash_finalized),
1962        )
1963        .route(
1964            routes::v1::LC_LEAF_BY_PAYLOAD_HASH_ROUTE,
1965            get(lc_leaf_by_payload_hash),
1966        )
1967        .route(
1968            routes::v1::LC_LEAF_BY_PAYLOAD_HASH_FINALIZED_ROUTE,
1969            get(lc_leaf_by_payload_hash_finalized),
1970        )
1971        .route(routes::v1::LC_HEADER_BY_HEIGHT_ROUTE, get(lc_header_by_height))
1972        .route(routes::v1::LC_HEADER_BY_HASH_ROUTE, get(lc_header_by_hash))
1973        .route(
1974            routes::v1::LC_HEADER_BY_PAYLOAD_HASH_ROUTE,
1975            get(lc_header_by_payload_hash),
1976        )
1977        .route(routes::v1::LC_STAKE_TABLE_ROUTE, get(lc_stake_table))
1978        .route(routes::v1::LC_PAYLOAD_ROUTE, get(lc_payload))
1979        .route(routes::v1::LC_PAYLOAD_RANGE_ROUTE, get(lc_payload_range))
1980        .route(routes::v1::LC_NAMESPACE_ROUTE, get(lc_namespace))
1981        .route(
1982            routes::v1::LC_NAMESPACE_RANGE_ROUTE,
1983            get(lc_namespace_range),
1984        )
1985        // Explorer
1986        .route(
1987            routes::v1::EXPLORER_BLOCK_DETAIL_BY_HEIGHT_ROUTE,
1988            get(explorer_block_detail_by_height),
1989        )
1990        .route(
1991            routes::v1::EXPLORER_BLOCK_DETAIL_BY_HASH_ROUTE,
1992            get(explorer_block_detail_by_hash),
1993        )
1994        .route(
1995            routes::v1::EXPLORER_BLOCK_SUMMARIES_LATEST_ROUTE,
1996            get(explorer_block_summaries_latest),
1997        )
1998        .route(
1999            routes::v1::EXPLORER_BLOCK_SUMMARIES_FROM_ROUTE,
2000            get(explorer_block_summaries_from),
2001        )
2002        .route(
2003            routes::v1::EXPLORER_TX_DETAIL_BY_POSITION_ROUTE,
2004            get(explorer_tx_detail_by_position),
2005        )
2006        .route(
2007            routes::v1::EXPLORER_TX_DETAIL_BY_HASH_ROUTE,
2008            get(explorer_tx_detail_by_hash),
2009        )
2010        .route(
2011            routes::v1::EXPLORER_TX_SUMMARIES_LATEST_BLOCK_ROUTE,
2012            get(explorer_tx_summaries_latest_block),
2013        )
2014        .route(
2015            routes::v1::EXPLORER_TX_SUMMARIES_FROM_BLOCK_ROUTE,
2016            get(explorer_tx_summaries_from_block),
2017        )
2018        .route(
2019            routes::v1::EXPLORER_TX_SUMMARIES_BY_HASH_BLOCK_ROUTE,
2020            get(explorer_tx_summaries_by_hash_block),
2021        )
2022        .route(
2023            routes::v1::EXPLORER_TX_SUMMARIES_LATEST_NS_ROUTE,
2024            get(explorer_tx_summaries_latest_ns),
2025        )
2026        .route(
2027            routes::v1::EXPLORER_TX_SUMMARIES_FROM_NS_ROUTE,
2028            get(explorer_tx_summaries_from_ns),
2029        )
2030        .route(
2031            routes::v1::EXPLORER_TX_SUMMARIES_BY_HASH_NS_ROUTE,
2032            get(explorer_tx_summaries_by_hash_ns),
2033        )
2034        .route(
2035            routes::v1::EXPLORER_TX_SUMMARIES_LATEST_ROUTE,
2036            get(explorer_tx_summaries_latest),
2037        )
2038        .route(
2039            routes::v1::EXPLORER_TX_SUMMARIES_FROM_ROUTE,
2040            get(explorer_tx_summaries_from),
2041        )
2042        .route(
2043            routes::v1::EXPLORER_TX_SUMMARIES_BY_HASH_ROUTE,
2044            get(explorer_tx_summaries_by_hash),
2045        )
2046        .route(routes::v1::EXPLORER_SUMMARY_ROUTE, get(explorer_summary))
2047        .route(routes::v1::EXPLORER_SEARCH_ROUTE, get(explorer_search))
2048        // Token
2049        .route(
2050            routes::v1::TOKEN_TOTAL_MINTED_SUPPLY_ROUTE,
2051            get(token_total_minted),
2052        )
2053        .route(
2054            routes::v1::TOKEN_CIRCULATING_SUPPLY_ROUTE,
2055            get(token_circulating),
2056        )
2057        .route(
2058            routes::v1::TOKEN_CIRCULATING_SUPPLY_ETHEREUM_ROUTE,
2059            get(token_circulating_eth),
2060        )
2061        .route(
2062            routes::v1::TOKEN_TOTAL_ISSUED_SUPPLY_ROUTE,
2063            get(token_total_issued),
2064        )
2065        .route(
2066            routes::v1::TOKEN_TOTAL_REWARD_DISTRIBUTED_ROUTE,
2067            get(token_total_reward_distributed),
2068        )
2069        // Database (diagnostic)
2070        .route(
2071            routes::v1::DATABASE_TABLE_SIZES_ROUTE,
2072            get(database_table_sizes),
2073        )
2074        .with_state(state)
2075}
2076
2077/// Create v2 router with OpenAPI documentation (proto types)
2078pub fn create_router_v2<S>(state: S) -> Router
2079where
2080    S: v2::RewardApi + v2::DataApi + v2::ConsensusApi + Clone + Send + Sync + 'static,
2081{
2082    let mut api = OpenApi {
2083        info: Info {
2084            title: "Espresso Node API v2".to_string(),
2085            description: None,
2086            version: "1.0.0".to_string(),
2087            ..Default::default()
2088        },
2089        ..Default::default()
2090    };
2091
2092    let get_reward_claim_input =
2093        |State(state): State<S>, SendQuery(request): SendQuery<GetRewardClaimInputRequest>| async move {
2094            handlers::get_reward_claim_input(&state, request)
2095                .await
2096                .map(Json)
2097        };
2098
2099    let get_reward_balance =
2100        |State(state): State<S>, SendQuery(request): SendQuery<GetRewardBalanceRequest>| async move {
2101            handlers::get_reward_balance(&state, request)
2102                .await
2103                .map(Json)
2104        };
2105
2106    let get_reward_account_proof =
2107        |State(state): State<S>, SendQuery(request): SendQuery<GetRewardAccountProofRequest>| async move {
2108            handlers::get_reward_account_proof(&state, request)
2109                .await
2110                .map(Json)
2111        };
2112
2113    let get_reward_balances =
2114        |State(state): State<S>, SendQuery(request): SendQuery<GetRewardBalancesRequest>| async move {
2115            handlers::get_reward_balances(&state, request)
2116                .await
2117                .map(Json)
2118        };
2119
2120    let get_reward_merkle_tree_v2 =
2121        |State(state): State<S>, SendQuery(request): SendQuery<GetRewardMerkleTreeRequest>| async move {
2122            handlers::get_reward_merkle_tree_v2(&state, request)
2123                .await
2124                .map(Json)
2125        };
2126
2127    let get_state_certificate =
2128        |State(state): State<S>, SendQuery(request): SendQuery<GetStateCertificateRequest>| async move {
2129            handlers::get_state_certificate(&state, request)
2130                .await
2131                .map(Json)
2132        };
2133
2134    let get_stake_table =
2135        |State(state): State<S>, SendQuery(request): SendQuery<GetStakeTableRequest>| async move {
2136            handlers::get_stake_table(&state, request).await.map(Json)
2137        };
2138
2139    let get_namespace_proof =
2140        |State(state): State<S>, SendQuery(query): SendQuery<GetNamespaceProofRequest>| async move {
2141            handlers::get_namespace_proof(&state, query).await.map(Json)
2142        };
2143
2144    let get_incorrect_encoding_proof = |State(state): State<S>,
2145                                        SendQuery(query): SendQuery<
2146        GetIncorrectEncodingProofRequest,
2147    >| async move {
2148        handlers::get_incorrect_encoding_proof(&state, query)
2149            .await
2150            .map(Json)
2151    };
2152
2153    let router = ApiRouter::new()
2154        .api_route(
2155            routes::v2::REWARD_CLAIM_INPUT_ROUTE.http,
2156            get_with(get_reward_claim_input, |op| {
2157                op.description(routes::v2::REWARD_CLAIM_INPUT_ROUTE.description)
2158                    .tag(routes::v2::REWARD_CLAIM_INPUT_ROUTE.tag)
2159            }),
2160        )
2161        .api_route(
2162            routes::v2::REWARD_BALANCE_ROUTE.http,
2163            get_with(get_reward_balance, |op| {
2164                op.description(routes::v2::REWARD_BALANCE_ROUTE.description)
2165                    .tag(routes::v2::REWARD_BALANCE_ROUTE.tag)
2166            }),
2167        )
2168        .api_route(
2169            routes::v2::REWARD_ACCOUNT_PROOF_ROUTE.http,
2170            get_with(get_reward_account_proof, |op| {
2171                op.description(routes::v2::REWARD_ACCOUNT_PROOF_ROUTE.description)
2172                    .tag(routes::v2::REWARD_ACCOUNT_PROOF_ROUTE.tag)
2173            }),
2174        )
2175        .api_route(
2176            routes::v2::REWARD_BALANCES_ROUTE.http,
2177            get_with(get_reward_balances, |op| {
2178                op.description(routes::v2::REWARD_BALANCES_ROUTE.description)
2179                    .tag(routes::v2::REWARD_BALANCES_ROUTE.tag)
2180            }),
2181        )
2182        .api_route(
2183            routes::v2::REWARD_MERKLE_TREE_V2_ROUTE.http,
2184            get_with(get_reward_merkle_tree_v2, |op| {
2185                op.description(routes::v2::REWARD_MERKLE_TREE_V2_ROUTE.description)
2186                    .tag(routes::v2::REWARD_MERKLE_TREE_V2_ROUTE.tag)
2187            }),
2188        )
2189        .api_route(
2190            routes::v2::NAMESPACE_PROOF_ROUTE.http,
2191            get_with(get_namespace_proof, |op| {
2192                op.description(routes::v2::NAMESPACE_PROOF_ROUTE.description)
2193                    .tag(routes::v2::NAMESPACE_PROOF_ROUTE.tag)
2194            }),
2195        )
2196        .api_route(
2197            routes::v2::INCORRECT_ENCODING_PROOF_ROUTE.http,
2198            get_with(get_incorrect_encoding_proof, |op| {
2199                op.description(routes::v2::INCORRECT_ENCODING_PROOF_ROUTE.description)
2200                    .tag(routes::v2::INCORRECT_ENCODING_PROOF_ROUTE.tag)
2201            }),
2202        )
2203        .api_route(
2204            routes::v2::STATE_CERTIFICATE_ROUTE.http,
2205            get_with(get_state_certificate, |op| {
2206                op.description(routes::v2::STATE_CERTIFICATE_ROUTE.description)
2207                    .tag(routes::v2::STATE_CERTIFICATE_ROUTE.tag)
2208            }),
2209        )
2210        .api_route(
2211            routes::v2::STAKE_TABLE_ROUTE.http,
2212            get_with(get_stake_table, |op| {
2213                op.description(routes::v2::STAKE_TABLE_ROUTE.description)
2214                    .tag(routes::v2::STAKE_TABLE_ROUTE.tag)
2215            }),
2216        )
2217        .finish_api(&mut api);
2218
2219    // Transform examples (array) to example (singular) for OpenAPI 3.0/Swagger compatibility
2220    if let Some(ref mut components) = api.components {
2221        let mut transform = schemars::transform::SetSingleExample::default();
2222        for schema in components.schemas.values_mut() {
2223            transform.transform(&mut schema.json_schema);
2224        }
2225    }
2226
2227    // Also transform path parameter schemas
2228    if let Some(ref mut paths) = api.paths {
2229        let mut transform = schemars::transform::SetSingleExample::default();
2230        for path_item_ref in paths.paths.values_mut() {
2231            if let aide::openapi::ReferenceOr::Item(path_item) = path_item_ref {
2232                for operation in [
2233                    &mut path_item.get,
2234                    &mut path_item.post,
2235                    &mut path_item.put,
2236                    &mut path_item.delete,
2237                    &mut path_item.patch,
2238                ]
2239                .into_iter()
2240                .flatten()
2241                {
2242                    for param in &mut operation.parameters {
2243                        if let aide::openapi::ReferenceOr::Item(param_item) = param {
2244                            let parameter_data = match param_item {
2245                                aide::openapi::Parameter::Query { parameter_data, .. } => {
2246                                    parameter_data
2247                                },
2248                                aide::openapi::Parameter::Header { parameter_data, .. } => {
2249                                    parameter_data
2250                                },
2251                                aide::openapi::Parameter::Path { parameter_data, .. } => {
2252                                    parameter_data
2253                                },
2254                                aide::openapi::Parameter::Cookie { parameter_data, .. } => {
2255                                    parameter_data
2256                                },
2257                            };
2258                            if let aide::openapi::ParameterSchemaOrContent::Schema(ref mut schema) =
2259                                parameter_data.format
2260                            {
2261                                transform.transform(&mut schema.json_schema);
2262                            }
2263                        }
2264                    }
2265                }
2266            }
2267        }
2268    }
2269
2270    router
2271        .route(routes::v2::OPENAPI_SPEC_ROUTE, get(serve_openapi_spec))
2272        .route(routes::v2::SWAGGER_ROUTE, get(serve_swagger_ui))
2273        .route("/v2/", get(serve_swagger_ui))
2274        .route(
2275            routes::v2::SCALAR_ROUTE,
2276            get(Scalar::new(routes::v2::OPENAPI_SPEC_ROUTE)
2277                .with_title("Espresso Node API v2")
2278                .axum_handler()),
2279        )
2280        .route(
2281            routes::v2::REDOC_ROUTE,
2282            get(Redoc::new(routes::v2::OPENAPI_SPEC_ROUTE)
2283                .with_title("Espresso Node API v2")
2284                .axum_handler()),
2285        )
2286        .layer(Extension(api))
2287        .with_state(state)
2288}