Skip to main content

espresso_api/
axum.rs

1//! Axum HTTP/JSON API handlers
2
3pub mod routes;
4
5use std::{ops::Range, sync::Arc};
6
7use aide::{
8    axum::{
9        ApiRouter,
10        routing::{get_with, post_with},
11    },
12    openapi::{
13        Info, OpenApi, Parameter, ParameterData, ParameterSchemaOrContent, PathStyle, ReferenceOr,
14        SchemaObject,
15    },
16    operation::OperationOutput,
17    scalar::Scalar,
18};
19use axum::{
20    Extension, Json, Router,
21    body::Bytes,
22    extract::{Path, Request, State, ws::WebSocketUpgrade},
23    http::{HeaderMap, StatusCode, Uri, header},
24    response::{Html, IntoResponse, Response},
25    routing::{any, get},
26};
27use http_wire::{
28    ContentType, DecodeFailure, WireError, drive_ws_stream, healthcheck_response,
29    module_healthcheck_response,
30};
31use schemars::transform::Transform;
32use serde::{Deserialize, Serialize};
33use tokio::sync::Semaphore;
34
35use crate::{
36    error::{ApiError, classify as classify_availability_error},
37    v1,
38};
39
40// The `router_*` builders take trait objects so each is compiled once, not once per state type.
41type RewardState = Arc<dyn v1::RewardApi + Send + Sync>;
42type AvailabilityState = Arc<dyn Availability>;
43type BlockState = Arc<dyn v1::BlockStateApi + Send + Sync>;
44type FeeState = Arc<dyn v1::FeeStateApi + Send + Sync>;
45type StatusState = Arc<dyn v1::StatusApi + Send + Sync>;
46type ConfigState = Arc<dyn v1::ConfigApi + Send + Sync>;
47type NodeState = Arc<dyn v1::NodeApi + Send + Sync>;
48type CatchupState = Arc<dyn v1::CatchupApi + Send + Sync>;
49type SubmitState = Arc<dyn v1::SubmitApi + Send + Sync>;
50type StateSignatureState = Arc<dyn v1::StateSignatureApi + Send + Sync>;
51type LightClientState = Arc<dyn v1::LightClientApi + Send + Sync>;
52type ExplorerState = Arc<dyn v1::ExplorerApi + Send + Sync>;
53type DatabaseState = Arc<dyn v1::DatabaseApi + Send + Sync>;
54type TokenState = Arc<dyn v1::TokenApi + Send + Sync>;
55
56/// The availability router serves both availability traits from one state object.
57pub(crate) trait Availability:
58    v1::AvailabilityApi + v1::HotShotAvailabilityApi + Send + Sync
59{
60}
61impl<T: v1::AvailabilityApi + v1::HotShotAvailabilityApi + Send + Sync> Availability for T {}
62
63/// API error response, wire-compatible with the `Custom` variant of the per-module error enums
64/// (`node::Error::Custom`, `merklized_state::Error::Custom`, etc.) that all of tide-disco's
65/// `Error::catch_all` calls produce. Most of our migrated endpoints (catchup, submit,
66/// state-signature, light-client, node, status, config, token, database) take that path, so this
67/// envelope is byte-identical with tide's error response for them. Endpoints that use a specific
68/// variant directly (e.g. `availability::Error::FetchLeaf`) emit their own shape on tide; those
69/// bytes are not matched here.
70#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
71#[error("{custom}")]
72struct ErrorResponse {
73    #[serde(rename = "Custom")]
74    custom: CustomError,
75}
76
77#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
78#[error("error {status}: {message}")]
79struct CustomError {
80    // Field order matches `node::Error::Custom { message, status }` declaration so serde_json
81    // emits the same key order on the wire.
82    message: String,
83    status: u16,
84}
85
86impl ErrorResponse {
87    /// Scrubs credentials out of the message. `ApiError`'s `Display` already does this for handler
88    /// errors, which the tonic adapter shares; this also covers messages that never went through
89    /// it, such as a serialization failure. Error bodies go to unauthenticated callers and never
90    /// pass the OTLP exporter.
91    fn new(status: StatusCode, message: String) -> Self {
92        Self {
93            custom: CustomError {
94                message: espresso_utils::redact::scrub(&message),
95                status: status.as_u16(),
96            },
97        }
98    }
99}
100
101impl IntoResponse for ApiError {
102    fn into_response(self) -> Response {
103        let status = match &self {
104            ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
105            ApiError::NotFound(_) => StatusCode::NOT_FOUND,
106            ApiError::Gone(_) => StatusCode::GONE,
107            ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
108            ApiError::Unavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
109        };
110        (status, Json(ErrorResponse::new(status, self.to_string()))).into_response()
111    }
112}
113
114impl WireError for ErrorResponse {
115    fn status(&self) -> StatusCode {
116        StatusCode::from_u16(self.custom.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
117    }
118
119    fn catch_all(status: StatusCode, message: String) -> Self {
120        Self::new(status, message)
121    }
122}
123
124/// Encode a successful response body based on the request's `Accept` header, matching
125/// tide-disco's content negotiation.
126///
127/// surf-disco's default `Accept` is `application/octet-stream`, so production internal clients
128/// (peer-catchup, submit-transactions, light-client provider) expect VBS-encoded responses for
129/// the endpoints that flow large structured data. Falls back to JSON otherwise.
130fn encode_response<T: Serialize>(headers: &HeaderMap, value: T) -> Response {
131    http_wire::encode_ok::<ErrorResponse, _>(headers, value)
132}
133
134/// Decode a request body based on its `Content-Type`, matched by media-type essence.
135///
136/// - `application/octet-stream`: VBS (versioned binary), what `Request::body_binary`
137///   sends, and what production peer-catchup / submit-transactions clients use.
138/// - `application/json`: serde_json.
139pub(crate) fn decode_body<T: serde::de::DeserializeOwned>(
140    headers: &HeaderMap,
141    body: &[u8],
142) -> Result<T, ApiError> {
143    http_wire::decode_body(headers, body).map_err(|err| {
144        ApiError::BadRequest(match err {
145            DecodeFailure::Binary(err) => anyhow::anyhow!("invalid binary body: {err}"),
146            DecodeFailure::Json(err) => anyhow::anyhow!("invalid json body: {err}"),
147            DecodeFailure::UnsupportedContentType => {
148                match headers
149                    .get(header::CONTENT_TYPE)
150                    .and_then(|v| v.to_str().ok())
151                {
152                    Some(other) => anyhow::anyhow!("unsupported Content-Type: {other}"),
153                    None => anyhow::anyhow!("missing Content-Type header"),
154                }
155            },
156        })
157    })
158}
159
160impl OperationOutput for ApiError {
161    type Inner = Self;
162}
163
164/// Successful JSON response for v1 handlers, most of which return domain types (from
165/// `espresso-types`, `hotshot-query-service`, etc.) that don't implement `schemars::JsonSchema`;
166/// this crate doesn't add OpenAPI derives to domain types. Wire format is identical to
167/// `axum::Json<T>`; only the OpenAPI operation gets an untyped 200 response instead of a generated
168/// schema.
169struct ApiJson<T>(T);
170
171impl<T: Serialize> IntoResponse for ApiJson<T> {
172    fn into_response(self) -> Response {
173        Json(self.0).into_response()
174    }
175}
176
177impl<T> OperationOutput for ApiJson<T> {
178    type Inner = T;
179
180    fn inferred_responses(
181        _ctx: &mut aide::generate::GenContext,
182        _operation: &mut aide::openapi::Operation,
183    ) -> Vec<(Option<u16>, aide::openapi::Response)> {
184        vec![(Some(200), aide::openapi::Response::default())]
185    }
186}
187
188/// In-flight request slots for `max_connections`.
189#[derive(Clone)]
190pub(crate) struct RequestLimit(pub(crate) Arc<Semaphore>);
191
192/// Each request holds a slot while in flight, excess gets 429. A websocket's slot is released
193/// at the 101 upgrade and an SSE stream's once its headers go out: long-lived streams are
194/// deliberately unbounded here, since demo workloads (nasty-client holds hundreds of streams by
195/// design) dwarf the request budget of 25.
196pub(crate) async fn limit_requests(
197    Extension(RequestLimit(semaphore)): Extension<RequestLimit>,
198    req: Request,
199    next: axum::middleware::Next,
200) -> Response {
201    match semaphore.try_acquire_owned() {
202        Ok(_permit) => next.run(req).await,
203        Err(_) => StatusCode::TOO_MANY_REQUESTS.into_response(),
204    }
205}
206
207/// Serve custom Swagger UI with collapsed defaults, pointed at the given OpenAPI spec route.
208fn swagger_html(spec_route: &str) -> Html<String> {
209    Html(include_str!("../templates/swagger.html").replace("{{OPENAPI_SPEC_ROUTE}}", spec_route))
210}
211
212/// v2 is WIP, so `/` points at the v1 docs; 307 so browsers don't cache the redirect.
213async fn redirect_to_docs() -> axum::response::Redirect {
214    axum::response::Redirect::temporary("/v1")
215}
216
217/// Tide-disco served every v1 module at both `/<module>/...` and `/v1/<module>/...`, and legacy
218/// clients (surf-disco, the light-client, tests) still address the unversioned and `/v0` forms.
219/// Axum only declares the `/v1/...` and `/v2/...` route shapes, and `Router::layer` middleware
220/// runs after routing, so it can never redirect a request onto a route it doesn't already match.
221/// This function is instead wrapped around the whole router with `tower::util::MapRequestLayer`
222/// (see `serve_axum`), which runs before routing, to rewrite the URI so the declared routes match.
223///
224/// Excludes paths that are intentionally unversioned: `/`, `/healthcheck`, `/version`, and
225/// anything already prefixed with `/v1` or `/v2`.
226pub(crate) fn rewrite_legacy_uri(mut req: Request) -> Request {
227    let uri = req.uri().clone();
228    let path = uri.path();
229
230    let is_reserved =
231        path == "/" || path == "/healthcheck" || path == "/version" || path.is_empty();
232    let is_versioned =
233        path == "/v1" || path.starts_with("/v1/") || path == "/v2" || path.starts_with("/v2/");
234    let new_path = if is_versioned || is_reserved {
235        None
236    } else if path == "/v0" {
237        Some("/v1".to_string())
238    } else if let Some(rest) = path.strip_prefix("/v0/") {
239        Some(format!("/v1/{rest}"))
240    } else {
241        Some(format!("/v1{path}"))
242    };
243
244    if let Some(new_path) = new_path {
245        let pq = if let Some(q) = uri.query() {
246            format!("{new_path}?{q}")
247        } else {
248            new_path
249        };
250        if let Ok(new_uri) = Uri::builder().path_and_query(pq).build() {
251            *req.uri_mut() = new_uri;
252        }
253    }
254
255    req
256}
257
258/// Add the routes that every mode serves regardless of which API modules are enabled:
259/// `/`, `/healthcheck`, `/v1/healthcheck`, `/v2/healthcheck`, `/v1/{module}/healthcheck`, and
260/// `/version`. Callers apply CORS.
261pub(crate) fn with_top_level_routes(router: Router) -> Router {
262    router
263        .route("/", get(redirect_to_docs))
264        .route("/healthcheck", get(healthcheck))
265        // Clients configured with a versioned base URL resolve `healthcheck` against it, so each
266        // version prefix answers with the same app health as the unversioned route.
267        .route("/v1/healthcheck", get(healthcheck))
268        .route("/v2/healthcheck", get(healthcheck))
269        .route("/v1/{module}/healthcheck", get(module_healthcheck))
270        .route("/version", get(version))
271}
272
273async fn healthcheck(headers: HeaderMap) -> Response {
274    healthcheck_response(&headers)
275}
276
277/// `/v1/{module}/healthcheck`, reached by legacy clients via the `/{module}/healthcheck` rewrite.
278///
279/// Divergence from tide-disco: matches any `{module}` string, so unregistered module names report
280/// healthy instead of 404. Constraining it to the registered set would have to track which
281/// modules each serve mode mounts; not worth it for a liveness probe.
282async fn module_healthcheck(headers: HeaderMap) -> Response {
283    module_healthcheck_response(&headers)
284}
285
286/// Tide-disco-compatible version response. Tide emits the binary's clap version; we emit the
287/// crate version so `http_client::Client::connect` and similar polling helpers succeed.
288async fn version() -> Json<serde_json::Value> {
289    Json(serde_json::json!({
290        "version": env!("CARGO_PKG_VERSION"),
291    }))
292}
293
294pub(crate) fn router_reward(state: RewardState) -> ApiRouter {
295    let get_reward_claim_input =
296        |State(state): State<RewardState>, Path((height, address)): Path<(u64, String)>| async move {
297            state
298                .get_reward_claim_input(height, address)
299                .await
300                .map(ApiJson)
301                .map_err(classify_availability_error)
302        };
303
304    let get_reward_balance =
305        |State(state): State<RewardState>, Path((height, address)): Path<(u64, String)>| async move {
306            state
307                .get_reward_balance(height, address)
308                .await
309                .map(ApiJson)
310                .map_err(classify_availability_error)
311        };
312
313    let get_latest_reward_balance =
314        |State(state): State<RewardState>, Path(address): Path<String>| async move {
315            state
316                .get_latest_reward_balance(address)
317                .await
318                .map(ApiJson)
319                .map_err(classify_availability_error)
320        };
321
322    let get_reward_account_proof =
323        |State(state): State<RewardState>, Path((height, address)): Path<(u64, String)>| async move {
324            state
325                .get_reward_account_proof(height, address)
326                .await
327                .map(ApiJson)
328                .map_err(classify_availability_error)
329        };
330
331    let get_latest_reward_account_proof =
332        |State(state): State<RewardState>, Path(address): Path<String>| async move {
333            state
334                .get_latest_reward_account_proof(address)
335                .await
336                .map(ApiJson)
337                .map_err(classify_availability_error)
338        };
339
340    let get_reward_amounts =
341        |State(state): State<RewardState>, Path((height, offset, limit)): Path<(u64, u64, u64)>| async move {
342            state
343                .get_reward_amounts(height, offset, limit)
344                .await
345                .map(ApiJson)
346                .map_err(classify_availability_error)
347        };
348
349    let get_reward_merkle_tree_v2 = |State(state): State<RewardState>, Path(height): Path<u64>| async move {
350        state
351            .get_reward_merkle_tree_v2(height)
352            .await
353            .map(ApiJson)
354            .map_err(classify_availability_error)
355    };
356
357    let get_reward_state_height = |State(state): State<RewardState>| async move {
358        state
359            .get_reward_state_height()
360            .await
361            .map(ApiJson)
362            .map_err(classify_availability_error)
363    };
364
365    let get_reward_state_v2_height = |State(state): State<RewardState>| async move {
366        state
367            .get_reward_state_v2_height()
368            .await
369            .map(ApiJson)
370            .map_err(classify_availability_error)
371    };
372
373    // Same underlying V2-tree lookup as `reward-state-v2/reward-balance`; tide registers this
374    // route unconditionally for both merklized-state modules regardless of tree version.
375    let get_reward_balance_v1 =
376        |State(state): State<RewardState>, Path((height, address)): Path<(u64, String)>| async move {
377            state
378                .get_reward_balance(height, address)
379                .await
380                .map(ApiJson)
381                .map_err(classify_availability_error)
382        };
383
384    let get_reward_account_proof_v1 =
385        |State(state): State<RewardState>, Path((height, address)): Path<(u64, String)>| async move {
386            state
387                .get_reward_account_proof_v1(height, address)
388                .await
389                .map(ApiJson)
390                .map_err(classify_availability_error)
391        };
392
393    // Merklized-state `get_path` handlers, inherited by both reward mounts from the legacy
394    // `hotshot-query-service` merklized-state base routes (mirrors router_block_state /
395    // router_fee_state below).
396    let get_reward_state_path_v1_by_height =
397        |State(state): State<RewardState>, Path((height, key)): Path<(u64, String)>| async move {
398            state
399                .get_reward_state_path_v1(v1::Snapshot::Height(height), key)
400                .await
401                .map(ApiJson)
402                .map_err(classify_availability_error)
403        };
404
405    let get_reward_state_path_v1_by_commit =
406        |State(state): State<RewardState>, Path((commit, key)): Path<(String, String)>| async move {
407            state
408                .get_reward_state_path_v1(v1::Snapshot::Commit(commit), key)
409                .await
410                .map(ApiJson)
411                .map_err(classify_availability_error)
412        };
413
414    let get_reward_state_path_v2_by_height =
415        |State(state): State<RewardState>, Path((height, key)): Path<(u64, String)>| async move {
416            state
417                .get_reward_state_path_v2(v1::Snapshot::Height(height), key)
418                .await
419                .map(ApiJson)
420                .map_err(classify_availability_error)
421        };
422
423    let get_reward_state_path_v2_by_commit =
424        |State(state): State<RewardState>, Path((commit, key)): Path<(String, String)>| async move {
425            state
426                .get_reward_state_path_v2(v1::Snapshot::Commit(commit), key)
427                .await
428                .map(ApiJson)
429                .map_err(classify_availability_error)
430        };
431
432    ApiRouter::new()
433        .api_route(
434            routes::v1::REWARD_CLAIM_INPUT_ROUTE,
435            get_with(get_reward_claim_input, |op| {
436                op.summary("Get reward claim input").description(
437                    "Returns the RewardClaimInput needed to call claimRewards() on L1: lifetime \
438                     rewards, Merkle proof, and auth root inputs, for the account at the given \
439                     block height finalized by the light client contract.",
440                )
441            }),
442        )
443        .api_route(
444            routes::v1::REWARD_BALANCE_ROUTE,
445            get_with(get_reward_balance, |op| {
446                op.summary("Get reward balance at height").description(
447                    "Get balance in reward state at a specific height for an Ethereum address.",
448                )
449            }),
450        )
451        .api_route(
452            routes::v1::LATEST_REWARD_BALANCE_ROUTE,
453            get_with(get_latest_reward_balance, |op| {
454                op.summary("Get latest reward balance")
455                    .description("Get current balance in reward state for an Ethereum address.")
456            }),
457        )
458        .api_route(
459            routes::v1::REWARD_ACCOUNT_PROOF_ROUTE,
460            get_with(get_reward_account_proof, |op| {
461                op.summary("Get reward account proof").description(
462                    "Get the Merkle proof for a reward account at a given block height \
463                     (RewardAccountProofV1 pre-V4, RewardAccountProofV2 from V4 onward).",
464                )
465            }),
466        )
467        .api_route(
468            routes::v1::LATEST_REWARD_ACCOUNT_PROOF_ROUTE,
469            get_with(get_latest_reward_account_proof, |op| {
470                op.summary("Get latest reward account proof").description(
471                    "Get the Merkle proof (RewardAccountProofV2) for a reward account at the \
472                     latest block height finalized by the light client contract.",
473                )
474            }),
475        )
476        .api_route(
477            routes::v1::REWARD_AMOUNTS_ROUTE,
478            get_with(get_reward_amounts, |op| {
479                op.summary("List reward amounts").description(
480                    "Return all RewardMerkleTreeV2 accounts stored for the requested height, \
481                     paginated by offset and limit (limit must be <= 10000).",
482                )
483            }),
484        )
485        .api_route(
486            routes::v1::REWARD_MERKLE_TREE_V2_ROUTE,
487            get_with(get_reward_merkle_tree_v2, |op| {
488                op.summary("Get RewardMerkleTreeV2 snapshot").description(
489                    "Get the snapshot of this node's RewardMerkleTreeV2 at the given block \
490                     height, serialized as RewardMerkleTreeV2Data.",
491                )
492            }),
493        )
494        .api_route(
495            routes::v1::REWARD_STATE_HEIGHT_ROUTE,
496            get_with(get_reward_state_height, |op| {
497                op.summary("Get reward-state block height").description(
498                    "Latest block height for which the merklized reward state (V1) is available.",
499                )
500            }),
501        )
502        .api_route(
503            routes::v1::REWARD_STATE_V2_HEIGHT_ROUTE,
504            get_with(get_reward_state_v2_height, |op| {
505                op.summary("Get reward-state-v2 block height").description(
506                    "Latest block height for which the merklized reward state (V2) is available.",
507                )
508            }),
509        )
510        .api_route(
511            routes::v1::REWARD_V1_BALANCE_ROUTE,
512            get_with(get_reward_balance_v1, |op| {
513                op.summary("Get reward balance at height (v1 mount)")
514                    .description(
515                        "Same handler as reward-state-v2/reward-balance, registered on the \
516                         reward-state mount; tide-disco shared this handler across both \
517                         merklized-state mounts.",
518                    )
519            }),
520        )
521        .api_route(
522            routes::v1::REWARD_V1_ACCOUNT_PROOF_ROUTE,
523            get_with(get_reward_account_proof_v1, |op| {
524                op.summary("Get reward account proof (v1 mount)")
525                    .description(
526                        "Same handler as reward-state-v2/proof, registered on the reward-state \
527                         mount; tide-disco shared this handler across both merklized-state mounts.",
528                    )
529            }),
530        )
531        // Tide-disco twins of the reward-state-v2 routes above, registered on the same
532        // handlers (tide shared them across both merklized-state modules).
533        .api_route(
534            routes::v1::REWARD_V1_LATEST_BALANCE_ROUTE,
535            get_with(get_latest_reward_balance, |op| {
536                op.summary("Get latest reward balance (v1 mount)")
537                    .description(
538                        "Same handler as reward-state-v2/reward-balance/latest, registered on the \
539                         reward-state mount; tide-disco shared this handler across both \
540                         merklized-state mounts.",
541                    )
542            }),
543        )
544        .api_route(
545            routes::v1::REWARD_V1_LATEST_ACCOUNT_PROOF_ROUTE,
546            get_with(get_latest_reward_account_proof, |op| {
547                op.summary("Get latest reward account proof (v1 mount)")
548                    .description(
549                        "Same handler as reward-state-v2/proof/latest, registered on the \
550                         reward-state mount; tide-disco shared this handler across both \
551                         merklized-state mounts.",
552                    )
553            }),
554        )
555        .api_route(
556            routes::v1::REWARD_V1_AMOUNTS_ROUTE,
557            get_with(get_reward_amounts, |op| {
558                op.summary("List reward amounts (v1 mount)").description(
559                    "Same handler as reward-state-v2/reward-amounts, registered on the \
560                     reward-state mount; tide-disco shared this handler across both \
561                     merklized-state mounts.",
562                )
563            }),
564        )
565        .api_route(
566            routes::v1::REWARD_V1_MERKLE_TREE_V2_ROUTE,
567            get_with(get_reward_merkle_tree_v2, |op| {
568                op.summary("Get RewardMerkleTreeV2 snapshot (v1 mount)")
569                    .description(
570                        "Same handler as reward-state-v2/reward-merkle-tree-v2, registered on the \
571                         reward-state mount; tide-disco shared this handler across both \
572                         merklized-state mounts.",
573                    )
574            }),
575        )
576        .api_route(
577            routes::v1::REWARD_STATE_PATH_BY_HEIGHT_ROUTE,
578            get_with(get_reward_state_path_v1_by_height, |op| {
579                op.summary("Get reward-state Merkle path by height")
580                    .description(
581                        "Retrieve the Merkle path for the membership proof of a leaf in the \
582                         reward-state (V1) tree, by block height and key.",
583                    )
584            }),
585        )
586        .api_route(
587            routes::v1::REWARD_STATE_PATH_BY_COMMIT_ROUTE,
588            get_with(get_reward_state_path_v1_by_commit, |op| {
589                op.summary("Get reward-state Merkle path by commitment")
590                    .description(
591                        "Retrieve the Merkle path for the membership proof of a leaf in the \
592                         reward-state (V1) tree, by tree commitment and key.",
593                    )
594            }),
595        )
596        .api_route(
597            routes::v1::REWARD_STATE_V2_PATH_BY_HEIGHT_ROUTE,
598            get_with(get_reward_state_path_v2_by_height, |op| {
599                op.summary("Get reward-state-v2 Merkle path by height")
600                    .description(
601                        "Retrieve the Merkle path for the membership proof of a leaf in the \
602                         reward-state-v2 tree, by block height and key.",
603                    )
604            }),
605        )
606        .api_route(
607            routes::v1::REWARD_STATE_V2_PATH_BY_COMMIT_ROUTE,
608            get_with(get_reward_state_path_v2_by_commit, |op| {
609                op.summary("Get reward-state-v2 Merkle path by commitment")
610                    .description(
611                        "Retrieve the Merkle path for the membership proof of a leaf in the \
612                         reward-state-v2 tree, by tree commitment and key.",
613                    )
614            }),
615        )
616        .with_state(state)
617}
618
619pub(crate) fn router_availability(state: AvailabilityState) -> ApiRouter {
620    // Availability API handlers
621    // Route: /v1/availability/block/{height}/namespace/{namespace}
622    let get_namespace_proof_by_height =
623        |State(state): State<AvailabilityState>, Path((height, namespace)): Path<(u64, u32)>| async move {
624            state
625                .get_namespace_proof(v1::availability::BlockId::Height(height), namespace)
626                .await
627                .map(ApiJson)
628                .map_err(classify_availability_error)
629        };
630
631    // Route: /v1/availability/block/hash/{hash}/namespace/{namespace}
632    let get_namespace_proof_by_hash =
633        |State(state): State<AvailabilityState>, Path((hash, namespace)): Path<(String, u32)>| async move {
634            state
635                .get_namespace_proof(v1::availability::BlockId::Hash(hash), namespace)
636                .await
637                .map(ApiJson)
638                .map_err(classify_availability_error)
639        };
640
641    // Route: /v1/availability/block/payload-hash/{payload-hash}/namespace/{namespace}
642    let get_namespace_proof_by_payload_hash =
643        |State(state): State<AvailabilityState>,
644         Path((payload_hash, namespace)): Path<(String, u32)>| async move {
645            state
646                .get_namespace_proof(
647                    v1::availability::BlockId::PayloadHash(payload_hash),
648                    namespace,
649                )
650                .await
651                .map(ApiJson)
652                .map_err(classify_availability_error)
653        };
654
655    // Route: /v1/availability/block/{from}/{until}/namespace/{namespace}
656    let get_namespace_proof_range =
657        |State(state): State<AvailabilityState>,
658         Path((from, until, namespace)): Path<(u64, u64, u32)>| async move {
659            state
660                .get_namespace_proof_range(from, until, namespace)
661                .await
662                .map(ApiJson)
663                .map_err(classify_availability_error)
664        };
665
666    let get_incorrect_encoding_proof =
667        |State(state): State<AvailabilityState>,
668         Path((block_number, namespace)): Path<(u64, u32)>| async move {
669            state
670                .get_incorrect_encoding_proof(
671                    v1::availability::BlockId::Height(block_number),
672                    namespace,
673                )
674                .await
675                .map(ApiJson)
676                .map_err(classify_availability_error)
677        };
678
679    let get_state_cert_v1 = |State(state): State<AvailabilityState>, Path(epoch): Path<u64>| async move {
680        state
681            .get_state_cert(epoch)
682            .await
683            .map(ApiJson)
684            .map_err(classify_availability_error)
685    };
686
687    let get_state_cert_v2 = |State(state): State<AvailabilityState>, Path(epoch): Path<u64>| async move {
688        state
689            .get_state_cert_v2(epoch)
690            .await
691            .map(ApiJson)
692            .map_err(classify_availability_error)
693    };
694
695    // HotShot availability API handlers
696    let get_leaf_by_height = |State(state): State<AvailabilityState>, Path(height): Path<u64>| async move {
697        state
698            .get_leaf(v1::LeafId::Height(height))
699            .await
700            .map(ApiJson)
701            .map_err(classify_availability_error)
702    };
703
704    let get_leaf_by_hash = |State(state): State<AvailabilityState>, Path(hash): Path<String>| async move {
705        state
706            .get_leaf(v1::LeafId::Hash(hash))
707            .await
708            .map(ApiJson)
709            .map_err(classify_availability_error)
710    };
711
712    let get_leaf_range = |State(state): State<AvailabilityState>,
713                          Path((from, until)): Path<(usize, usize)>| async move {
714        state
715            .get_leaf_range(from, until)
716            .await
717            .map(ApiJson)
718            .map_err(classify_availability_error)
719    };
720
721    let get_header_by_height = |State(state): State<AvailabilityState>, Path(height): Path<u64>| async move {
722        state
723            .get_header(v1::BlockId::Height(height))
724            .await
725            .map(ApiJson)
726            .map_err(classify_availability_error)
727    };
728
729    let get_header_by_hash = |State(state): State<AvailabilityState>, Path(hash): Path<String>| async move {
730        state
731            .get_header(v1::BlockId::Hash(hash))
732            .await
733            .map(ApiJson)
734            .map_err(classify_availability_error)
735    };
736
737    let get_header_by_payload_hash =
738        |State(state): State<AvailabilityState>, Path(payload_hash): Path<String>| async move {
739            state
740                .get_header(v1::BlockId::PayloadHash(payload_hash))
741                .await
742                .map(ApiJson)
743                .map_err(classify_availability_error)
744        };
745
746    let get_header_range = |State(state): State<AvailabilityState>,
747                            Path((from, until)): Path<(usize, usize)>| async move {
748        state
749            .get_header_range(from, until)
750            .await
751            .map(ApiJson)
752            .map_err(classify_availability_error)
753    };
754
755    let get_block_by_height = |State(state): State<AvailabilityState>, Path(height): Path<u64>| async move {
756        state
757            .get_block(v1::BlockId::Height(height))
758            .await
759            .map(ApiJson)
760            .map_err(classify_availability_error)
761    };
762
763    let get_block_by_hash = |State(state): State<AvailabilityState>, Path(hash): Path<String>| async move {
764        state
765            .get_block(v1::BlockId::Hash(hash))
766            .await
767            .map(ApiJson)
768            .map_err(classify_availability_error)
769    };
770
771    let get_block_by_payload_hash =
772        |State(state): State<AvailabilityState>, Path(payload_hash): Path<String>| async move {
773            state
774                .get_block(v1::BlockId::PayloadHash(payload_hash))
775                .await
776                .map(ApiJson)
777                .map_err(classify_availability_error)
778        };
779
780    let get_block_range = |State(state): State<AvailabilityState>,
781                           Path((from, until)): Path<(usize, usize)>| async move {
782        state
783            .get_block_range(from, until)
784            .await
785            .map(ApiJson)
786            .map_err(classify_availability_error)
787    };
788
789    let get_payload_by_height = |State(state): State<AvailabilityState>,
790                                 Path(height): Path<u64>| async move {
791        state
792            .get_payload(v1::PayloadId::Height(height))
793            .await
794            .map(ApiJson)
795            .map_err(classify_availability_error)
796    };
797
798    let get_payload_by_hash = |State(state): State<AvailabilityState>, Path(hash): Path<String>| async move {
799        state
800            .get_payload(v1::PayloadId::Hash(hash))
801            .await
802            .map(ApiJson)
803            .map_err(classify_availability_error)
804    };
805
806    let get_payload_by_block_hash =
807        |State(state): State<AvailabilityState>, Path(block_hash): Path<String>| async move {
808            state
809                .get_payload(v1::PayloadId::BlockHash(block_hash))
810                .await
811                .map(ApiJson)
812                .map_err(classify_availability_error)
813        };
814
815    let get_payload_range =
816        |State(state): State<AvailabilityState>, Path((from, until)): Path<(usize, usize)>| async move {
817            state
818                .get_payload_range(from, until)
819                .await
820                .map(ApiJson)
821                .map_err(classify_availability_error)
822        };
823
824    let get_vid_common_by_height = |State(state): State<AvailabilityState>,
825                                    Path(height): Path<u64>| async move {
826        state
827            .get_vid_common(v1::BlockId::Height(height))
828            .await
829            .map(ApiJson)
830            .map_err(classify_availability_error)
831    };
832
833    let get_vid_common_by_hash = |State(state): State<AvailabilityState>,
834                                  Path(hash): Path<String>| async move {
835        state
836            .get_vid_common(v1::BlockId::Hash(hash))
837            .await
838            .map(ApiJson)
839            .map_err(classify_availability_error)
840    };
841
842    let get_vid_common_by_payload_hash =
843        |State(state): State<AvailabilityState>, Path(payload_hash): Path<String>| async move {
844            state
845                .get_vid_common(v1::BlockId::PayloadHash(payload_hash))
846                .await
847                .map(ApiJson)
848                .map_err(classify_availability_error)
849        };
850
851    let get_vid_common_range =
852        |State(state): State<AvailabilityState>, Path((from, until)): Path<(usize, usize)>| async move {
853            state
854                .get_vid_common_range(from, until)
855                .await
856                .map(ApiJson)
857                .map_err(classify_availability_error)
858        };
859
860    let get_leaf_ranges =
861        |State(state): State<AvailabilityState>, headers: HeaderMap, body: Bytes| async move {
862            let ranges: Vec<Range<u64>> = decode_body(&headers, &body)?;
863            let leaves = state
864                .get_leaf_ranges(ranges)
865                .await
866                .map_err(classify_availability_error)?;
867            Ok::<_, ApiError>(encode_response(&headers, leaves))
868        };
869
870    let get_block_ranges =
871        |State(state): State<AvailabilityState>, headers: HeaderMap, body: Bytes| async move {
872            let ranges: Vec<Range<u64>> = decode_body(&headers, &body)?;
873            let blocks = state
874                .get_block_ranges(ranges)
875                .await
876                .map_err(classify_availability_error)?;
877            Ok::<_, ApiError>(encode_response(&headers, blocks))
878        };
879
880    let get_vid_common_ranges =
881        |State(state): State<AvailabilityState>, headers: HeaderMap, body: Bytes| async move {
882            let ranges: Vec<Range<u64>> = decode_body(&headers, &body)?;
883            let common = state
884                .get_vid_common_ranges(ranges)
885                .await
886                .map_err(classify_availability_error)?;
887            Ok::<_, ApiError>(encode_response(&headers, common))
888        };
889
890    let get_transaction_by_position =
891        |State(state): State<AvailabilityState>, Path((height, index)): Path<(u64, u64)>| async move {
892            state
893                .get_transaction_by_position(height, index)
894                .await
895                .map(ApiJson)
896                .map_err(classify_availability_error)
897        };
898
899    let get_transaction_by_hash = |State(state): State<AvailabilityState>,
900                                   Path(hash): Path<String>| async move {
901        state
902            .get_transaction_by_hash(hash)
903            .await
904            .map(ApiJson)
905            .map_err(classify_availability_error)
906    };
907
908    let get_transaction_proof_by_position =
909        |State(state): State<AvailabilityState>, Path((height, index)): Path<(u64, u64)>| async move {
910            state
911                .get_transaction_proof_by_position(height, index)
912                .await
913                .map(ApiJson)
914                .map_err(classify_availability_error)
915        };
916
917    let get_transaction_proof_by_hash =
918        |State(state): State<AvailabilityState>, Path(hash): Path<String>| async move {
919            state
920                .get_transaction_proof_by_hash(hash)
921                .await
922                .map(ApiJson)
923                .map_err(classify_availability_error)
924        };
925
926    let get_block_summary_by_height =
927        |State(state): State<AvailabilityState>, Path(height): Path<usize>| async move {
928            state
929                .get_block_summary(height)
930                .await
931                .map(ApiJson)
932                .map_err(classify_availability_error)
933        };
934
935    let get_block_summary_range =
936        |State(state): State<AvailabilityState>, Path((from, until)): Path<(usize, usize)>| async move {
937            state
938                .get_block_summary_range(from, until)
939                .await
940                .map(ApiJson)
941                .map_err(classify_availability_error)
942        };
943
944    let get_limits = |State(state): State<AvailabilityState>| async move {
945        state
946            .get_limits()
947            .await
948            .map(ApiJson)
949            .map_err(ApiError::Internal)
950    };
951
952    let get_cert2 = |State(state): State<AvailabilityState>, Path(height): Path<u64>| async move {
953        match state.get_cert2(height).await {
954            Ok(Some(cert2)) => Ok(ApiJson(cert2)),
955            Ok(None) => Err(ApiError::NotFound(anyhow::anyhow!(
956                "no cert2 available for height {height}"
957            ))),
958            Err(err) => Err(ApiError::Internal(err)),
959        }
960    };
961
962    // WebSocket streaming handlers
963    let stream_leaves = |ws: WebSocketUpgrade,
964                         State(state): State<AvailabilityState>,
965                         headers: HeaderMap,
966                         Path(height): Path<usize>| async move {
967        let format = ContentType::negotiate(&headers);
968        ws.on_upgrade(move |socket| async move {
969            match state.stream_leaves(height).await {
970                Ok(stream) => drive_ws_stream(socket, stream, format).await,
971                Err(e) => tracing::warn!("stream_leaves: {e}"),
972            }
973        })
974    };
975
976    let stream_headers = |ws: WebSocketUpgrade,
977                          State(state): State<AvailabilityState>,
978                          headers: HeaderMap,
979                          Path(height): Path<usize>| async move {
980        let format = ContentType::negotiate(&headers);
981        ws.on_upgrade(move |socket| async move {
982            match state.stream_headers(height).await {
983                Ok(stream) => drive_ws_stream(socket, stream, format).await,
984                Err(e) => tracing::warn!("stream_headers: {e}"),
985            }
986        })
987    };
988
989    let stream_blocks = |ws: WebSocketUpgrade,
990                         State(state): State<AvailabilityState>,
991                         headers: HeaderMap,
992                         Path(height): Path<usize>| async move {
993        let format = ContentType::negotiate(&headers);
994        ws.on_upgrade(move |socket| async move {
995            match state.stream_blocks(height).await {
996                Ok(stream) => drive_ws_stream(socket, stream, format).await,
997                Err(e) => tracing::warn!("stream_blocks: {e}"),
998            }
999        })
1000    };
1001
1002    let stream_payloads = |ws: WebSocketUpgrade,
1003                           State(state): State<AvailabilityState>,
1004                           headers: HeaderMap,
1005                           Path(height): Path<usize>| async move {
1006        let format = ContentType::negotiate(&headers);
1007        ws.on_upgrade(move |socket| async move {
1008            match state.stream_payloads(height).await {
1009                Ok(stream) => drive_ws_stream(socket, stream, format).await,
1010                Err(e) => tracing::warn!("stream_payloads: {e}"),
1011            }
1012        })
1013    };
1014
1015    let stream_vid_common = |ws: WebSocketUpgrade,
1016                             State(state): State<AvailabilityState>,
1017                             headers: HeaderMap,
1018                             Path(height): Path<usize>| async move {
1019        let format = ContentType::negotiate(&headers);
1020        ws.on_upgrade(move |socket| async move {
1021            match state.stream_vid_common(height).await {
1022                Ok(stream) => drive_ws_stream(socket, stream, format).await,
1023                Err(e) => tracing::warn!("stream_vid_common: {e}"),
1024            }
1025        })
1026    };
1027
1028    let stream_transactions = |ws: WebSocketUpgrade,
1029                               State(state): State<AvailabilityState>,
1030                               headers: HeaderMap,
1031                               Path(height): Path<usize>| async move {
1032        let format = ContentType::negotiate(&headers);
1033        ws.on_upgrade(move |socket| async move {
1034            match state.stream_transactions(height, None).await {
1035                Ok(stream) => drive_ws_stream(socket, stream, format).await,
1036                Err(e) => tracing::warn!("stream_transactions: {e}"),
1037            }
1038        })
1039    };
1040
1041    let stream_transactions_ns =
1042        |ws: WebSocketUpgrade,
1043         State(state): State<AvailabilityState>,
1044         headers: HeaderMap,
1045         Path((height, namespace)): Path<(usize, u32)>| async move {
1046            let format = ContentType::negotiate(&headers);
1047            ws.on_upgrade(move |socket| async move {
1048                match state.stream_transactions(height, Some(namespace)).await {
1049                    Ok(stream) => drive_ws_stream(socket, stream, format).await,
1050                    Err(e) => tracing::warn!("stream_transactions_ns: {e}"),
1051                }
1052            })
1053        };
1054
1055    let stream_namespace_proofs =
1056        |ws: WebSocketUpgrade,
1057         State(state): State<AvailabilityState>,
1058         headers: HeaderMap,
1059         Path((height, namespace)): Path<(usize, u32)>| async move {
1060            let format = ContentType::negotiate(&headers);
1061            ws.on_upgrade(move |socket| async move {
1062                match state.stream_namespace_proofs(height, namespace).await {
1063                    Ok(stream) => drive_ws_stream(socket, stream, format).await,
1064                    Err(e) => tracing::warn!("stream_namespace_proofs: {e}"),
1065                }
1066            })
1067        };
1068
1069    ApiRouter::new()
1070        .api_route(
1071            routes::v1::NAMESPACE_PROOF_BY_HEIGHT_ROUTE,
1072            get_with(get_namespace_proof_by_height, |op| {
1073                op.summary("Get namespace proof").description(
1074                    "Get the transactions in a namespace of the given block, along with a proof \
1075                     of completeness.",
1076                )
1077            }),
1078        )
1079        .api_route(
1080            routes::v1::NAMESPACE_PROOF_BY_HASH_ROUTE,
1081            get_with(get_namespace_proof_by_hash, |op| {
1082                op.summary("Get namespace proof").description(
1083                    "Get the transactions in a namespace of the given block, along with a proof \
1084                     of completeness.",
1085                )
1086            }),
1087        )
1088        .api_route(
1089            routes::v1::NAMESPACE_PROOF_BY_PAYLOAD_HASH_ROUTE,
1090            get_with(get_namespace_proof_by_payload_hash, |op| {
1091                op.summary("Get namespace proof").description(
1092                    "Get the transactions in a namespace of the given block, along with a proof \
1093                     of completeness.",
1094                )
1095            }),
1096        )
1097        .api_route(
1098            routes::v1::NAMESPACE_PROOF_RANGE_ROUTE,
1099            get_with(get_namespace_proof_range, |op| {
1100                op.summary("Get namespace proofs for a range").description(
1101                    "Get the transactions in the specified namespace from each block in a range, \
1102                     with proofs.",
1103                )
1104            }),
1105        )
1106        .api_route(
1107            routes::v1::INCORRECT_ENCODING_PROOF_ROUTE,
1108            get_with(get_incorrect_encoding_proof, |op| {
1109                op.summary("Get incorrect-encoding proof").description(
1110                    "Generate a proof of incorrect namespace encoding for the given block number.",
1111                )
1112            }),
1113        )
1114        .api_route(
1115            routes::v1::STATE_CERT_V1_ROUTE,
1116            get_with(get_state_cert_v1, |op| {
1117                op.summary("Get state certificate (V1)").description(
1118                    "Get the light client state update certificate (V1) for the given epoch, used \
1119                     to update the light client contract's stake table.",
1120                )
1121            }),
1122        )
1123        .api_route(
1124            routes::v1::STATE_CERT_V2_ROUTE,
1125            get_with(get_state_cert_v2, |op| {
1126                op.summary("Get state certificate (V2)").description(
1127                    "Get the light client state update certificate (V2) for the given epoch; \
1128                     includes the auth_root Keccak-256 hash of the reward Merkle tree roots.",
1129                )
1130            }),
1131        )
1132        .api_route(
1133            routes::v1::LEAF_BY_HEIGHT_ROUTE,
1134            get_with(get_leaf_by_height, |op| {
1135                op.summary("Get leaf").description(
1136                    "Get a leaf by its position in the ledger (0 is genesis) or its hash.",
1137                )
1138            }),
1139        )
1140        .api_route(
1141            routes::v1::LEAF_BY_HASH_ROUTE,
1142            get_with(get_leaf_by_hash, |op| {
1143                op.summary("Get leaf").description(
1144                    "Get a leaf by its position in the ledger (0 is genesis) or its hash.",
1145                )
1146            }),
1147        )
1148        .api_route(
1149            routes::v1::LEAF_RANGE_ROUTE,
1150            get_with(get_leaf_range, |op| {
1151                op.summary("Get leaves in range").description(
1152                    "Get leaves by position in the ledger, from the given `from` up to `until`.",
1153                )
1154            }),
1155        )
1156        .api_route(
1157            routes::v1::HEADER_BY_HEIGHT_ROUTE,
1158            get_with(get_header_by_height, |op| {
1159                op.summary("Get header").description(
1160                    "Get a header by its position in the ledger (0 is genesis) or its hash.",
1161                )
1162            }),
1163        )
1164        .api_route(
1165            routes::v1::HEADER_BY_HASH_ROUTE,
1166            get_with(get_header_by_hash, |op| {
1167                op.summary("Get header").description(
1168                    "Get a header by its position in the ledger (0 is genesis) or its hash.",
1169                )
1170            }),
1171        )
1172        .api_route(
1173            routes::v1::HEADER_BY_PAYLOAD_HASH_ROUTE,
1174            get_with(get_header_by_payload_hash, |op| {
1175                op.summary("Get header").description(
1176                    "Get a header by its position in the ledger (0 is genesis) or its hash.",
1177                )
1178            }),
1179        )
1180        .api_route(
1181            routes::v1::HEADER_RANGE_ROUTE,
1182            get_with(get_header_range, |op| {
1183                op.summary("Get headers in range").description(
1184                    "Get headers by position in the ledger, from the given `from` up to `until`.",
1185                )
1186            }),
1187        )
1188        .api_route(
1189            routes::v1::BLOCK_BY_HEIGHT_ROUTE,
1190            get_with(get_block_by_height, |op| {
1191                op.summary("Get block").description(
1192                    "Get a block (header, payload, hash, size) by its position in the ledger or \
1193                     its hash.",
1194                )
1195            }),
1196        )
1197        .api_route(
1198            routes::v1::BLOCK_BY_HASH_ROUTE,
1199            get_with(get_block_by_hash, |op| {
1200                op.summary("Get block").description(
1201                    "Get a block (header, payload, hash, size) by its position in the ledger or \
1202                     its hash.",
1203                )
1204            }),
1205        )
1206        .api_route(
1207            routes::v1::BLOCK_BY_PAYLOAD_HASH_ROUTE,
1208            get_with(get_block_by_payload_hash, |op| {
1209                op.summary("Get block").description(
1210                    "Get a block (header, payload, hash, size) by its position in the ledger or \
1211                     its hash.",
1212                )
1213            }),
1214        )
1215        .api_route(
1216            routes::v1::BLOCK_RANGE_ROUTE,
1217            get_with(get_block_range, |op| {
1218                op.summary("Get blocks in range").description(
1219                    "Get blocks by position in the ledger, from the given `from` up to `until`.",
1220                )
1221            }),
1222        )
1223        .api_route(
1224            routes::v1::PAYLOAD_BY_HEIGHT_ROUTE,
1225            get_with(get_payload_by_height, |op| {
1226                op.summary("Get payload").description(
1227                    "Get the payload of a block by its position in the ledger or its hash.",
1228                )
1229            }),
1230        )
1231        .api_route(
1232            routes::v1::PAYLOAD_BY_HASH_ROUTE,
1233            get_with(get_payload_by_hash, |op| {
1234                op.summary("Get payload").description(
1235                    "Get the payload of a block by its position in the ledger or its hash.",
1236                )
1237            }),
1238        )
1239        .api_route(
1240            routes::v1::PAYLOAD_BY_BLOCK_HASH_ROUTE,
1241            get_with(get_payload_by_block_hash, |op| {
1242                op.summary("Get payload").description(
1243                    "Get the payload of a block by its position in the ledger or its hash.",
1244                )
1245            }),
1246        )
1247        .api_route(
1248            routes::v1::PAYLOAD_RANGE_ROUTE,
1249            get_with(get_payload_range, |op| {
1250                op.summary("Get payloads in range").description(
1251                    "Get payloads by block position, from the given `from` up to `until`.",
1252                )
1253            }),
1254        )
1255        .api_route(
1256            routes::v1::VID_COMMON_BY_HEIGHT_ROUTE,
1257            get_with(get_vid_common_by_height, |op| {
1258                op.summary("Get VID common data").description(
1259                    "Get common VID data for a block; data shared by all storage nodes, not a VID \
1260                     share.",
1261                )
1262            }),
1263        )
1264        .api_route(
1265            routes::v1::VID_COMMON_BY_HASH_ROUTE,
1266            get_with(get_vid_common_by_hash, |op| {
1267                op.summary("Get VID common data").description(
1268                    "Get common VID data for a block; data shared by all storage nodes, not a VID \
1269                     share.",
1270                )
1271            }),
1272        )
1273        .api_route(
1274            routes::v1::VID_COMMON_BY_PAYLOAD_HASH_ROUTE,
1275            get_with(get_vid_common_by_payload_hash, |op| {
1276                op.summary("Get VID common data").description(
1277                    "Get common VID data for a block; data shared by all storage nodes, not a VID \
1278                     share.",
1279                )
1280            }),
1281        )
1282        .api_route(
1283            routes::v1::VID_COMMON_RANGE_ROUTE,
1284            get_with(get_vid_common_range, |op| {
1285                op.summary("Get VID common data in range").description(
1286                    "Get VID common objects by block position, from the given `from` up to \
1287                     `until`.",
1288                )
1289            }),
1290        )
1291        .api_route(
1292            routes::v1::LEAF_RANGES_ROUTE,
1293            post_with(get_leaf_ranges, |op| {
1294                op.summary("Get leaves for a set of height ranges")
1295                    .description(
1296                        "Get leaves for the height ranges in the request body, which must be \
1297                         ascending and disjoint but need not be contiguous. Answers in full or \
1298                         not at all, like the range endpoints: heights this node lacks are \
1299                         fetched from peers, and a 404 means at least one was not available in \
1300                         time.",
1301                    )
1302            }),
1303        )
1304        .api_route(
1305            routes::v1::BLOCK_RANGES_ROUTE,
1306            post_with(get_block_ranges, |op| {
1307                op.summary("Get blocks for a set of height ranges")
1308                    .description(
1309                        "Get blocks for the height ranges in the request body, which must be \
1310                         ascending and disjoint but need not be contiguous. Answers in full or \
1311                         not at all, like the range endpoints: heights this node lacks are \
1312                         fetched from peers, and a 404 means at least one was not available in \
1313                         time.",
1314                    )
1315            }),
1316        )
1317        .api_route(
1318            routes::v1::VID_COMMON_RANGES_ROUTE,
1319            post_with(get_vid_common_ranges, |op| {
1320                op.summary("Get VID common data for a set of height ranges")
1321                    .description(
1322                        "Get VID common data for the height ranges in the request body, which \
1323                         must be ascending and disjoint but need not be contiguous. Answers in \
1324                         full or not at all, like the range endpoints: heights this node lacks \
1325                         are fetched from peers, and a 404 means at least one was not available \
1326                         in time.",
1327                    )
1328            }),
1329        )
1330        .api_route(
1331            routes::v1::TRANSACTION_BY_POSITION_NOPROOF_ROUTE,
1332            get_with(get_transaction_by_position, |op| {
1333                op.summary("Get transaction (no proof)").description(
1334                    "Get a transaction by its index in a block or by its hash, without an \
1335                     inclusion proof.",
1336                )
1337            }),
1338        )
1339        .api_route(
1340            routes::v1::TRANSACTION_BY_HASH_NOPROOF_ROUTE,
1341            get_with(get_transaction_by_hash, |op| {
1342                op.summary("Get transaction (no proof)").description(
1343                    "Get a transaction by its index in a block or by its hash, without an \
1344                     inclusion proof.",
1345                )
1346            }),
1347        )
1348        .api_route(
1349            routes::v1::TRANSACTION_PROOF_BY_POSITION_ROUTE,
1350            get_with(get_transaction_proof_by_position, |op| {
1351                op.summary("Get transaction with inclusion proof")
1352                    .description(
1353                        "Get a transaction by its index in a block or by its hash, along with an \
1354                         application-defined inclusion proof.",
1355                    )
1356            }),
1357        )
1358        .api_route(
1359            routes::v1::TRANSACTION_PROOF_BY_HASH_ROUTE,
1360            get_with(get_transaction_proof_by_hash, |op| {
1361                op.summary("Get transaction with inclusion proof")
1362                    .description(
1363                        "Get a transaction by its index in a block or by its hash, along with an \
1364                         application-defined inclusion proof.",
1365                    )
1366            }),
1367        )
1368        .api_route(
1369            routes::v1::TRANSACTION_BY_POSITION_ROUTE,
1370            get_with(get_transaction_proof_by_position, |op| {
1371                op.summary("Get transaction with inclusion proof")
1372                    .description(
1373                        "Get a transaction by its index in a block or by its hash, along with an \
1374                         application-defined inclusion proof.",
1375                    )
1376            }),
1377        )
1378        .api_route(
1379            routes::v1::TRANSACTION_BY_HASH_ROUTE,
1380            get_with(get_transaction_proof_by_hash, |op| {
1381                op.summary("Get transaction with inclusion proof")
1382                    .description(
1383                        "Get a transaction by its index in a block or by its hash, along with an \
1384                         application-defined inclusion proof.",
1385                    )
1386            }),
1387        )
1388        .api_route(
1389            routes::v1::BLOCK_SUMMARY_BY_HEIGHT_ROUTE,
1390            get_with(get_block_summary_by_height, |op| {
1391                op.summary("Get block summary").description(
1392                    "Get the block summary for a block based on its position in the ledger.",
1393                )
1394            }),
1395        )
1396        .api_route(
1397            routes::v1::BLOCK_SUMMARY_RANGE_ROUTE,
1398            get_with(get_block_summary_range, |op| {
1399                op.summary("Get block summaries in range").description(
1400                    "Get block summaries by position, from the given `from` up to `until`.",
1401                )
1402            }),
1403        )
1404        .api_route(
1405            routes::v1::LIMITS_ROUTE,
1406            get_with(get_limits, |op| {
1407                op.summary("Get availability limits").description(
1408                    "Get implementation-defined limits restricting availability range queries \
1409                     (small/large object range limits).",
1410                )
1411            }),
1412        )
1413        .api_route(
1414            routes::v1::CERT2_BY_HEIGHT_ROUTE,
1415            get_with(get_cert2, |op| {
1416                op.summary("Get finality certificate").description(
1417                    "Get the finality certificate (Certificate2) at the given block height.",
1418                )
1419            }),
1420        )
1421        .api_route(
1422            routes::v1::STREAM_LEAVES_ROUTE,
1423            get_with(stream_leaves, |op| {
1424                op.summary("Stream leaves (websocket)").description(
1425                    "Websocket endpoint: subscribe to a stream of leaves in sequence order, \
1426                     starting at the given height.",
1427                )
1428            }),
1429        )
1430        .api_route(
1431            routes::v1::STREAM_HEADERS_ROUTE,
1432            get_with(stream_headers, |op| {
1433                op.summary("Stream headers (websocket)").description(
1434                    "Websocket endpoint: subscribe to a stream of headers in sequence order, \
1435                     starting at the given height.",
1436                )
1437            }),
1438        )
1439        .api_route(
1440            routes::v1::STREAM_BLOCKS_ROUTE,
1441            get_with(stream_blocks, |op| {
1442                op.summary("Stream blocks (websocket)").description(
1443                    "Websocket endpoint: subscribe to a stream of blocks in sequence order, \
1444                     starting at the given height.",
1445                )
1446            }),
1447        )
1448        .api_route(
1449            routes::v1::STREAM_PAYLOADS_ROUTE,
1450            get_with(stream_payloads, |op| {
1451                op.summary("Stream payloads (websocket)").description(
1452                    "Websocket endpoint: subscribe to a stream of block payloads in sequence \
1453                     order, starting at the given height.",
1454                )
1455            }),
1456        )
1457        .api_route(
1458            routes::v1::STREAM_VID_COMMON_ROUTE,
1459            get_with(stream_vid_common, |op| {
1460                op.summary("Stream VID common data (websocket)")
1461                    .description(
1462                        "Websocket endpoint: subscribe to a stream of VID common data in sequence \
1463                         order, starting at the given height.",
1464                    )
1465            }),
1466        )
1467        .api_route(
1468            routes::v1::STREAM_TRANSACTIONS_ROUTE,
1469            get_with(stream_transactions, |op| {
1470                op.summary("Stream transactions (websocket)").description(
1471                    "Websocket endpoint: subscribe to a stream of all transactions starting at \
1472                     the given height.",
1473                )
1474            }),
1475        )
1476        .api_route(
1477            routes::v1::STREAM_TRANSACTIONS_NS_ROUTE,
1478            get_with(stream_transactions_ns, |op| {
1479                op.summary("Stream namespace transactions (websocket)")
1480                    .description(
1481                        "Websocket endpoint: subscribe to a stream of transactions in one \
1482                         namespace, starting at the given height.",
1483                    )
1484            }),
1485        )
1486        .api_route(
1487            routes::v1::STREAM_NAMESPACE_PROOFS_ROUTE,
1488            get_with(stream_namespace_proofs, |op| {
1489                op.summary("Stream namespace proofs (websocket)")
1490                    .description(
1491                        "Websocket endpoint: subscribe to namespace data and proofs for each \
1492                         block, starting at the given height.",
1493                    )
1494            }),
1495        )
1496        .with_state(state)
1497}
1498
1499pub(crate) fn router_block_state(state: BlockState) -> ApiRouter {
1500    let get_block_state_height = |State(state): State<BlockState>| async move {
1501        state
1502            .get_block_state_height()
1503            .await
1504            .map(ApiJson)
1505            .map_err(classify_availability_error)
1506    };
1507
1508    let get_block_state_path_by_commit =
1509        |State(state): State<BlockState>, Path((commit, key)): Path<(String, String)>| async move {
1510            state
1511                .get_block_state_path(v1::Snapshot::Commit(commit), key)
1512                .await
1513                .map(ApiJson)
1514                .map_err(classify_availability_error)
1515        };
1516
1517    // Merklized state handlers: block-state
1518    let get_block_state_path_by_height =
1519        |State(state): State<BlockState>, Path((height, key)): Path<(u64, String)>| async move {
1520            state
1521                .get_block_state_path(v1::Snapshot::Height(height), key)
1522                .await
1523                .map(ApiJson)
1524                .map_err(classify_availability_error)
1525        };
1526
1527    ApiRouter::new()
1528        .api_route(
1529            routes::v1::BLOCK_STATE_HEIGHT_ROUTE,
1530            get_with(get_block_state_height, |op| {
1531                op.summary("Get block-state height").description(
1532                    "Latest block height for which the merklized blocks-Merkle-tree state is \
1533                     available.",
1534                )
1535            }),
1536        )
1537        .api_route(
1538            routes::v1::BLOCK_STATE_PATH_BY_COMMIT_ROUTE,
1539            get_with(get_block_state_path_by_commit, |op| {
1540                op.summary("Get block-state Merkle path by commitment")
1541                    .description(
1542                        "Retrieve the Merkle path for a leaf in the blocks Merkle tree, by tree \
1543                         commitment and key.",
1544                    )
1545            }),
1546        )
1547        .api_route(
1548            routes::v1::BLOCK_STATE_PATH_BY_HEIGHT_ROUTE,
1549            get_with(get_block_state_path_by_height, |op| {
1550                op.summary("Get block-state Merkle path by height")
1551                    .description(
1552                        "Retrieve the Merkle path for a leaf in the blocks Merkle tree, by block \
1553                         height and key.",
1554                    )
1555            }),
1556        )
1557        .with_state(state)
1558}
1559
1560pub(crate) fn router_fee_state(state: FeeState) -> ApiRouter {
1561    let get_fee_state_height = |State(state): State<FeeState>| async move {
1562        state
1563            .get_fee_state_height()
1564            .await
1565            .map(ApiJson)
1566            .map_err(classify_availability_error)
1567    };
1568
1569    let get_fee_balance_latest = |State(state): State<FeeState>, Path(address): Path<String>| async move {
1570        state
1571            .get_fee_balance_latest(address)
1572            .await
1573            .map(ApiJson)
1574            .map_err(classify_availability_error)
1575    };
1576
1577    let get_fee_state_path_by_commit =
1578        |State(state): State<FeeState>, Path((commit, key)): Path<(String, String)>| async move {
1579            state
1580                .get_fee_state_path(v1::Snapshot::Commit(commit), key)
1581                .await
1582                .map(ApiJson)
1583                .map_err(classify_availability_error)
1584        };
1585
1586    // Merklized state handlers: fee-state
1587    let get_fee_state_path_by_height =
1588        |State(state): State<FeeState>, Path((height, key)): Path<(u64, String)>| async move {
1589            state
1590                .get_fee_state_path(v1::Snapshot::Height(height), key)
1591                .await
1592                .map(ApiJson)
1593                .map_err(classify_availability_error)
1594        };
1595
1596    ApiRouter::new()
1597        .api_route(
1598            routes::v1::FEE_STATE_HEIGHT_ROUTE,
1599            get_with(get_fee_state_height, |op| {
1600                op.summary("Get fee-state height").description(
1601                    "Latest block height for which the merklized fee state is available.",
1602                )
1603            }),
1604        )
1605        .api_route(
1606            routes::v1::FEE_STATE_BALANCE_LATEST_ROUTE,
1607            get_with(get_fee_balance_latest, |op| {
1608                op.summary("Get latest fee balance").description(
1609                    "Get the latest fee account balance for an address from the fee Merkle tree.",
1610                )
1611            }),
1612        )
1613        .api_route(
1614            routes::v1::FEE_STATE_PATH_BY_COMMIT_ROUTE,
1615            get_with(get_fee_state_path_by_commit, |op| {
1616                op.summary("Get fee-state Merkle path by commitment")
1617                    .description(
1618                        "Retrieve the Merkle path for a leaf in the fee state tree, by tree \
1619                         commitment and key.",
1620                    )
1621            }),
1622        )
1623        .api_route(
1624            routes::v1::FEE_STATE_PATH_BY_HEIGHT_ROUTE,
1625            get_with(get_fee_state_path_by_height, |op| {
1626                op.summary("Get fee-state Merkle path by height")
1627                    .description(
1628                        "Retrieve the Merkle path for a leaf in the fee state tree, by block \
1629                         height and key.",
1630                    )
1631            }),
1632        )
1633        .with_state(state)
1634}
1635
1636pub(crate) fn router_status(state: StatusState) -> ApiRouter {
1637    let status_block_height = |State(state): State<StatusState>| async move {
1638        state
1639            .block_height()
1640            .await
1641            .map(ApiJson)
1642            .map_err(ApiError::Internal)
1643    };
1644
1645    let status_success_rate = |State(state): State<StatusState>| async move {
1646        state
1647            .success_rate()
1648            .await
1649            .map(ApiJson)
1650            .map_err(ApiError::Internal)
1651    };
1652
1653    let status_time_since_last_decide = |State(state): State<StatusState>| async move {
1654        state
1655            .time_since_last_decide()
1656            .await
1657            .map(ApiJson)
1658            .map_err(ApiError::Internal)
1659    };
1660
1661    let status_metrics = |State(state): State<StatusState>| async move {
1662        match state.metrics().await {
1663            Ok(text) => (
1664                [(
1665                    axum::http::header::CONTENT_TYPE,
1666                    "text/plain; charset=utf-8",
1667                )],
1668                text,
1669            )
1670                .into_response(),
1671            Err(e) => ApiError::Internal(e).into_response(),
1672        }
1673    };
1674
1675    let status_keys = |State(state): State<StatusState>| async move {
1676        state
1677            .keys()
1678            .await
1679            .map(ApiJson)
1680            .map_err(classify_availability_error)
1681    };
1682
1683    ApiRouter::new()
1684        .api_route(
1685            routes::v1::STATUS_BLOCK_HEIGHT_ROUTE,
1686            get_with(status_block_height, |op| {
1687                op.summary("Get latest committed block height")
1688                    .description("Get the height of the latest committed block.")
1689            }),
1690        )
1691        .api_route(
1692            routes::v1::STATUS_SUCCESS_RATE_ROUTE,
1693            get_with(status_success_rate, |op| {
1694                op.summary("Get view success rate")
1695                    .description("Get the fraction of views which resulted in a committed block.")
1696            }),
1697        )
1698        .api_route(
1699            routes::v1::STATUS_TIME_SINCE_LAST_DECIDE_ROUTE,
1700            get_with(status_time_since_last_decide, |op| {
1701                op.summary("Get time since last decide")
1702                    .description("Get the time elapsed in seconds since the last decided view.")
1703            }),
1704        )
1705        .api_route(
1706            routes::v1::STATUS_METRICS_ROUTE,
1707            get_with(status_metrics, |op| {
1708                op.summary("Get Prometheus metrics")
1709                    .description("Prometheus endpoint exposing consensus-related metrics.")
1710            }),
1711        )
1712        .api_route(
1713            routes::v1::STATUS_KEYS_ROUTE,
1714            get_with(status_keys, |op| {
1715                op.summary("Get node public keys").description(
1716                    "Get this node's public keys (Ethereum account, BLS, Schnorr, x25519). The \
1717                     BLS and Schnorr keys are formatted as in stake-table responses; the x25519 \
1718                     key is tagged base64. The Ethereum account is taken from the node's \
1719                     stake-table registration and is null if the node is not registered.",
1720                )
1721            }),
1722        )
1723        .with_state(state)
1724}
1725
1726pub(crate) fn router_config(state: ConfigState) -> ApiRouter {
1727    let config_hotshot = |State(state): State<ConfigState>| async move {
1728        state
1729            .hotshot_config()
1730            .await
1731            .map(ApiJson)
1732            .map_err(ApiError::Internal)
1733    };
1734
1735    let config_env = |State(state): State<ConfigState>| async move {
1736        state.env().await.map(ApiJson).map_err(ApiError::Internal)
1737    };
1738
1739    let config_runtime = |State(state): State<ConfigState>| async move {
1740        state
1741            .runtime_config()
1742            .await
1743            .map(ApiJson)
1744            .map_err(classify_availability_error)
1745    };
1746
1747    ApiRouter::new()
1748        .api_route(
1749            routes::v1::CONFIG_HOTSHOT_ROUTE,
1750            get_with(config_hotshot, |op| {
1751                op.summary("Get HotShot config")
1752                    .description("Get the HotShot configuration for the current node.")
1753            }),
1754        )
1755        .api_route(
1756            routes::v1::CONFIG_ENV_ROUTE,
1757            get_with(config_env, |op| {
1758                op.summary("Get environment variables").description(
1759                    "Get all ESPRESSO_ environment variables set for the current node.",
1760                )
1761            }),
1762        )
1763        .api_route(
1764            routes::v1::CONFIG_RUNTIME_ROUTE,
1765            get_with(config_runtime, |op| {
1766                op.summary("Get runtime config").description(
1767                    "Get the merged runtime configuration (CLI flags + env vars + defaults); \
1768                     secrets and L1 RPC URLs are redacted.",
1769                )
1770            }),
1771        )
1772        .with_state(state)
1773}
1774
1775pub(crate) fn router_node(state: NodeState) -> ApiRouter {
1776    let node_block_height = |State(state): State<NodeState>| async move {
1777        state
1778            .block_height()
1779            .await
1780            .map(ApiJson)
1781            .map_err(classify_availability_error)
1782    };
1783
1784    let node_count_txs = |State(state): State<NodeState>| async move {
1785        state
1786            .count_transactions(None, None, None)
1787            .await
1788            .map(ApiJson)
1789            .map_err(classify_availability_error)
1790    };
1791
1792    let node_count_txs_ns = |State(state): State<NodeState>, Path(namespace): Path<u64>| async move {
1793        state
1794            .count_transactions(None, None, Some(namespace))
1795            .await
1796            .map(ApiJson)
1797            .map_err(classify_availability_error)
1798    };
1799
1800    let node_count_txs_ns_to =
1801        |State(state): State<NodeState>, Path((namespace, to)): Path<(u64, u64)>| async move {
1802            state
1803                .count_transactions(None, Some(to), Some(namespace))
1804                .await
1805                .map(ApiJson)
1806                .map_err(classify_availability_error)
1807        };
1808
1809    let node_count_txs_ns_from_to =
1810        |State(state): State<NodeState>, Path((namespace, from, to)): Path<(u64, u64, u64)>| async move {
1811            state
1812                .count_transactions(Some(from), Some(to), Some(namespace))
1813                .await
1814                .map(ApiJson)
1815                .map_err(classify_availability_error)
1816        };
1817
1818    let node_count_txs_to = |State(state): State<NodeState>, Path(to): Path<u64>| async move {
1819        state
1820            .count_transactions(None, Some(to), None)
1821            .await
1822            .map(ApiJson)
1823            .map_err(classify_availability_error)
1824    };
1825
1826    let node_count_txs_from_to =
1827        |State(state): State<NodeState>, Path((from, to)): Path<(u64, u64)>| async move {
1828            state
1829                .count_transactions(Some(from), Some(to), None)
1830                .await
1831                .map(ApiJson)
1832                .map_err(classify_availability_error)
1833        };
1834
1835    let node_payload_size = |State(state): State<NodeState>| async move {
1836        state
1837            .payload_size(None, None, None)
1838            .await
1839            .map(ApiJson)
1840            .map_err(classify_availability_error)
1841    };
1842
1843    let node_payload_size_ns = |State(state): State<NodeState>, Path(namespace): Path<u64>| async move {
1844        state
1845            .payload_size(None, None, Some(namespace))
1846            .await
1847            .map(ApiJson)
1848            .map_err(classify_availability_error)
1849    };
1850
1851    let node_payload_size_ns_to =
1852        |State(state): State<NodeState>, Path((namespace, to)): Path<(u64, u64)>| async move {
1853            state
1854                .payload_size(None, Some(to), Some(namespace))
1855                .await
1856                .map(ApiJson)
1857                .map_err(classify_availability_error)
1858        };
1859
1860    let node_payload_size_ns_from_to =
1861        |State(state): State<NodeState>, Path((namespace, from, to)): Path<(u64, u64, u64)>| async move {
1862            state
1863                .payload_size(Some(from), Some(to), Some(namespace))
1864                .await
1865                .map(ApiJson)
1866                .map_err(classify_availability_error)
1867        };
1868
1869    let node_payload_size_to = |State(state): State<NodeState>, Path(to): Path<u64>| async move {
1870        state
1871            .payload_size(None, Some(to), None)
1872            .await
1873            .map(ApiJson)
1874            .map_err(classify_availability_error)
1875    };
1876
1877    let node_payload_size_from_to =
1878        |State(state): State<NodeState>, Path((from, to)): Path<(u64, u64)>| async move {
1879            state
1880                .payload_size(Some(from), Some(to), None)
1881                .await
1882                .map(ApiJson)
1883                .map_err(classify_availability_error)
1884        };
1885
1886    let node_vid_share_by_hash = |State(state): State<NodeState>, Path(hash): Path<String>| async move {
1887        state
1888            .get_vid_share(v1::VidShareId::Hash(hash))
1889            .await
1890            .map(ApiJson)
1891            .map_err(classify_availability_error)
1892    };
1893
1894    let node_vid_share_by_payload_hash =
1895        |State(state): State<NodeState>, Path(payload_hash): Path<String>| async move {
1896            state
1897                .get_vid_share(v1::VidShareId::PayloadHash(payload_hash))
1898                .await
1899                .map(ApiJson)
1900                .map_err(classify_availability_error)
1901        };
1902
1903    let node_vid_share_by_height = |State(state): State<NodeState>, Path(height): Path<u64>| async move {
1904        state
1905            .get_vid_share(v1::VidShareId::Height(height))
1906            .await
1907            .map(ApiJson)
1908            .map_err(classify_availability_error)
1909    };
1910
1911    let node_sync_status = |State(state): State<NodeState>| async move {
1912        state
1913            .sync_status()
1914            .await
1915            .map(ApiJson)
1916            .map_err(classify_availability_error)
1917    };
1918
1919    let node_header_window_hash =
1920        |State(state): State<NodeState>, Path((hash, end)): Path<(String, u64)>| async move {
1921            state
1922                .get_header_window(v1::HeaderWindowStart::Hash(hash), end)
1923                .await
1924                .map(ApiJson)
1925                .map_err(classify_availability_error)
1926        };
1927
1928    let node_header_window_height =
1929        |State(state): State<NodeState>, Path((height, end)): Path<(u64, u64)>| async move {
1930            state
1931                .get_header_window(v1::HeaderWindowStart::Height(height), end)
1932                .await
1933                .map(ApiJson)
1934                .map_err(classify_availability_error)
1935        };
1936
1937    let node_header_window_time =
1938        |State(state): State<NodeState>, Path((start, end)): Path<(u64, u64)>| async move {
1939            state
1940                .get_header_window(v1::HeaderWindowStart::Time(start), end)
1941                .await
1942                .map(ApiJson)
1943                .map_err(classify_availability_error)
1944        };
1945
1946    let node_limits = |State(state): State<NodeState>| async move {
1947        state
1948            .limits()
1949            .await
1950            .map(ApiJson)
1951            .map_err(ApiError::Internal)
1952    };
1953
1954    let node_stake_table_current = |State(state): State<NodeState>| async move {
1955        state
1956            .stake_table_current()
1957            .await
1958            .map(ApiJson)
1959            .map_err(ApiError::Internal)
1960    };
1961
1962    let node_stake_table = |State(state): State<NodeState>, Path(epoch): Path<u64>| async move {
1963        state
1964            .stake_table(epoch)
1965            .await
1966            .map(ApiJson)
1967            .map_err(ApiError::Internal)
1968    };
1969
1970    let node_da_stake_table_current = |State(state): State<NodeState>| async move {
1971        state
1972            .da_stake_table_current()
1973            .await
1974            .map(ApiJson)
1975            .map_err(ApiError::Internal)
1976    };
1977
1978    let node_da_stake_table = |State(state): State<NodeState>, Path(epoch): Path<u64>| async move {
1979        state
1980            .da_stake_table(epoch)
1981            .await
1982            .map(ApiJson)
1983            .map_err(ApiError::Internal)
1984    };
1985
1986    let node_validators = |State(state): State<NodeState>, Path(epoch): Path<u64>| async move {
1987        state
1988            .get_validators(epoch)
1989            .await
1990            .map(ApiJson)
1991            .map_err(ApiError::Internal)
1992    };
1993
1994    let node_all_validators =
1995        |State(state): State<NodeState>, Path((epoch, offset, limit)): Path<(u64, u64, u64)>| async move {
1996            state
1997                .get_all_validators(epoch, offset, limit)
1998                .await
1999                .map(ApiJson)
2000                .map_err(ApiError::BadRequest)
2001        };
2002
2003    let node_proposal_participation_current = |State(state): State<NodeState>| async move {
2004        state
2005            .current_proposal_participation()
2006            .await
2007            .map(ApiJson)
2008            .map_err(ApiError::Internal)
2009    };
2010
2011    let node_proposal_participation = |State(state): State<NodeState>, Path(epoch): Path<u64>| async move {
2012        state
2013            .proposal_participation(epoch)
2014            .await
2015            .map(ApiJson)
2016            .map_err(ApiError::Internal)
2017    };
2018
2019    let node_vote_participation_current = |State(state): State<NodeState>| async move {
2020        state
2021            .current_vote_participation()
2022            .await
2023            .map(ApiJson)
2024            .map_err(ApiError::Internal)
2025    };
2026
2027    let node_vote_participation = |State(state): State<NodeState>, Path(epoch): Path<u64>| async move {
2028        state
2029            .vote_participation(epoch)
2030            .await
2031            .map(ApiJson)
2032            .map_err(ApiError::Internal)
2033    };
2034
2035    let node_block_reward = |State(state): State<NodeState>| async move {
2036        state
2037            .get_block_reward(None)
2038            .await
2039            .map(ApiJson)
2040            .map_err(ApiError::Internal)
2041    };
2042
2043    let node_block_reward_epoch = |State(state): State<NodeState>, Path(epoch): Path<u64>| async move {
2044        state
2045            .get_block_reward(Some(epoch))
2046            .await
2047            .map(ApiJson)
2048            .map_err(ApiError::Internal)
2049    };
2050
2051    let node_oldest_block = |State(state): State<NodeState>| async move {
2052        state
2053            .get_oldest_block()
2054            .await
2055            .map(ApiJson)
2056            .map_err(ApiError::Internal)
2057    };
2058
2059    let node_oldest_leaf = |State(state): State<NodeState>| async move {
2060        state
2061            .get_oldest_leaf()
2062            .await
2063            .map(ApiJson)
2064            .map_err(ApiError::Internal)
2065    };
2066
2067    ApiRouter::new()
2068        .api_route(
2069            routes::v1::NODE_BLOCK_HEIGHT_ROUTE,
2070            get_with(node_block_height, |op| {
2071                op.summary("Get node's block height")
2072                    .description("The current height of the chain, as observed by this node.")
2073            }),
2074        )
2075        .api_route(
2076            routes::v1::NODE_TRANSACTIONS_COUNT_ROUTE,
2077            get_with(node_count_txs, |op| {
2078                op.summary("Count transactions").description(
2079                    "Get the number of transactions in the chain, optionally restricted by block \
2080                     range and/or namespace.",
2081                )
2082            }),
2083        )
2084        .api_route(
2085            routes::v1::NODE_TRANSACTIONS_COUNT_NS_ROUTE,
2086            get_with(node_count_txs_ns, |op| {
2087                op.summary("Count transactions").description(
2088                    "Get the number of transactions in the chain, optionally restricted by block \
2089                     range and/or namespace.",
2090                )
2091            }),
2092        )
2093        .api_route(
2094            routes::v1::NODE_TRANSACTIONS_COUNT_NS_TO_ROUTE,
2095            get_with(node_count_txs_ns_to, |op| {
2096                op.summary("Count transactions").description(
2097                    "Get the number of transactions in the chain, optionally restricted by block \
2098                     range and/or namespace.",
2099                )
2100            }),
2101        )
2102        .api_route(
2103            routes::v1::NODE_TRANSACTIONS_COUNT_NS_FROM_TO_ROUTE,
2104            get_with(node_count_txs_ns_from_to, |op| {
2105                op.summary("Count transactions").description(
2106                    "Get the number of transactions in the chain, optionally restricted by block \
2107                     range and/or namespace.",
2108                )
2109            }),
2110        )
2111        .api_route(
2112            routes::v1::NODE_TRANSACTIONS_COUNT_TO_ROUTE,
2113            get_with(node_count_txs_to, |op| {
2114                op.summary("Count transactions").description(
2115                    "Get the number of transactions in the chain, optionally restricted by block \
2116                     range and/or namespace.",
2117                )
2118            }),
2119        )
2120        .api_route(
2121            routes::v1::NODE_TRANSACTIONS_COUNT_FROM_TO_ROUTE,
2122            get_with(node_count_txs_from_to, |op| {
2123                op.summary("Count transactions").description(
2124                    "Get the number of transactions in the chain, optionally restricted by block \
2125                     range and/or namespace.",
2126                )
2127            }),
2128        )
2129        .api_route(
2130            routes::v1::NODE_PAYLOADS_SIZE_ROUTE,
2131            get_with(node_payload_size, |op| {
2132                op.summary("Get payload size").description(
2133                    "Get the cumulative size (bytes) of payload data in the chain, optionally \
2134                     restricted by block range and/or namespace.",
2135                )
2136            }),
2137        )
2138        .api_route(
2139            routes::v1::NODE_PAYLOADS_TOTAL_SIZE_ROUTE,
2140            get_with(node_payload_size, |op| {
2141                op.summary("Get payload size")
2142                    .description("Deprecated alias for payloads/size.")
2143            }),
2144        )
2145        .api_route(
2146            routes::v1::NODE_PAYLOADS_SIZE_NS_ROUTE,
2147            get_with(node_payload_size_ns, |op| {
2148                op.summary("Get payload size").description(
2149                    "Get the cumulative size (bytes) of payload data in the chain, optionally \
2150                     restricted by block range and/or namespace.",
2151                )
2152            }),
2153        )
2154        .api_route(
2155            routes::v1::NODE_PAYLOADS_SIZE_NS_TO_ROUTE,
2156            get_with(node_payload_size_ns_to, |op| {
2157                op.summary("Get payload size").description(
2158                    "Get the cumulative size (bytes) of payload data in the chain, optionally \
2159                     restricted by block range and/or namespace.",
2160                )
2161            }),
2162        )
2163        .api_route(
2164            routes::v1::NODE_PAYLOADS_SIZE_NS_FROM_TO_ROUTE,
2165            get_with(node_payload_size_ns_from_to, |op| {
2166                op.summary("Get payload size").description(
2167                    "Get the cumulative size (bytes) of payload data in the chain, optionally \
2168                     restricted by block range and/or namespace.",
2169                )
2170            }),
2171        )
2172        .api_route(
2173            routes::v1::NODE_PAYLOADS_SIZE_TO_ROUTE,
2174            get_with(node_payload_size_to, |op| {
2175                op.summary("Get payload size").description(
2176                    "Get the cumulative size (bytes) of payload data in the chain, optionally \
2177                     restricted by block range and/or namespace.",
2178                )
2179            }),
2180        )
2181        .api_route(
2182            routes::v1::NODE_PAYLOADS_SIZE_FROM_TO_ROUTE,
2183            get_with(node_payload_size_from_to, |op| {
2184                op.summary("Get payload size").description(
2185                    "Get the cumulative size (bytes) of payload data in the chain, optionally \
2186                     restricted by block range and/or namespace.",
2187                )
2188            }),
2189        )
2190        .api_route(
2191            routes::v1::NODE_VID_SHARE_BY_HASH_ROUTE,
2192            get_with(node_vid_share_by_hash, |op| {
2193                op.summary("Get this node's VID share").description(
2194                    "Get information needed to run the VID reconstruction protocol for a block: \
2195                     this node's VID share, if available.",
2196                )
2197            }),
2198        )
2199        .api_route(
2200            routes::v1::NODE_VID_SHARE_BY_PAYLOAD_HASH_ROUTE,
2201            get_with(node_vid_share_by_payload_hash, |op| {
2202                op.summary("Get this node's VID share").description(
2203                    "Get information needed to run the VID reconstruction protocol for a block: \
2204                     this node's VID share, if available.",
2205                )
2206            }),
2207        )
2208        .api_route(
2209            routes::v1::NODE_VID_SHARE_BY_HEIGHT_ROUTE,
2210            get_with(node_vid_share_by_height, |op| {
2211                op.summary("Get this node's VID share").description(
2212                    "Get information needed to run the VID reconstruction protocol for a block: \
2213                     this node's VID share, if available.",
2214                )
2215            }),
2216        )
2217        .api_route(
2218            routes::v1::NODE_SYNC_STATUS_ROUTE,
2219            get_with(node_sync_status, |op| {
2220                op.summary("Get node sync status").description(
2221                    "Get the node's progress syncing with the latest chain state \
2222                     (missing/present/pruned ranges for blocks, leaves, and VID common).",
2223                )
2224            }),
2225        )
2226        .api_route(
2227            routes::v1::NODE_HEADER_WINDOW_HASH_ROUTE,
2228            get_with(node_header_window_hash, |op| {
2229                op.summary("Get header window").description(
2230                    "Get block headers whose timestamps fall in a time window, plus one header \
2231                     before and after to prove completeness.",
2232                )
2233            }),
2234        )
2235        .api_route(
2236            routes::v1::NODE_HEADER_WINDOW_HEIGHT_ROUTE,
2237            get_with(node_header_window_height, |op| {
2238                op.summary("Get header window").description(
2239                    "Get block headers whose timestamps fall in a time window, plus one header \
2240                     before and after to prove completeness.",
2241                )
2242            }),
2243        )
2244        .api_route(
2245            routes::v1::NODE_HEADER_WINDOW_TIME_ROUTE,
2246            get_with(node_header_window_time, |op| {
2247                op.summary("Get header window").description(
2248                    "Get block headers whose timestamps fall in a time window, plus one header \
2249                     before and after to prove completeness.",
2250                )
2251            }),
2252        )
2253        .api_route(
2254            routes::v1::NODE_LIMITS_ROUTE,
2255            get_with(node_limits, |op| {
2256                op.summary("Get node limits").description(
2257                    "Get implementation-defined limits restricting node API requests (e.g. \
2258                     header/window query size).",
2259                )
2260            }),
2261        )
2262        .api_route(
2263            routes::v1::NODE_STAKE_TABLE_CURRENT_ROUTE,
2264            get_with(node_stake_table_current, |op| {
2265                op.summary("Get current stake table")
2266                    .description("Get the stake table for the current epoch.")
2267            }),
2268        )
2269        .api_route(
2270            routes::v1::NODE_STAKE_TABLE_ROUTE,
2271            get_with(node_stake_table, |op| {
2272                op.summary("Get stake table for epoch")
2273                    .description("Get the stake table for the given epoch.")
2274            }),
2275        )
2276        .api_route(
2277            routes::v1::NODE_DA_STAKE_TABLE_CURRENT_ROUTE,
2278            get_with(node_da_stake_table_current, |op| {
2279                op.summary("Get current DA stake table")
2280                    .description("Get the DA stake table for the current epoch.")
2281            }),
2282        )
2283        .api_route(
2284            routes::v1::NODE_DA_STAKE_TABLE_ROUTE,
2285            get_with(node_da_stake_table, |op| {
2286                op.summary("Get DA stake table for epoch")
2287                    .description("Get the DA stake table for the given epoch.")
2288            }),
2289        )
2290        .api_route(
2291            routes::v1::NODE_VALIDATORS_ROUTE,
2292            get_with(node_validators, |op| {
2293                op.summary("Get validators for epoch")
2294                    .description("Get the validators map for the given epoch.")
2295            }),
2296        )
2297        .api_route(
2298            routes::v1::NODE_ALL_VALIDATORS_ROUTE,
2299            get_with(node_all_validators, |op| {
2300                op.summary("Get all validators for epoch").description(
2301                    "Get all validators, including inactive ones, for the given epoch, paginated \
2302                     by offset and limit.",
2303                )
2304            }),
2305        )
2306        .api_route(
2307            routes::v1::NODE_PROPOSAL_PARTICIPATION_CURRENT_ROUTE,
2308            get_with(node_proposal_participation_current, |op| {
2309                op.summary("Get current proposal participation")
2310                    .description(
2311                        "Get the mapping from leader key to the fraction of views proposed \
2312                         properly as leader.",
2313                    )
2314            }),
2315        )
2316        .api_route(
2317            routes::v1::NODE_PROPOSAL_PARTICIPATION_ROUTE,
2318            get_with(node_proposal_participation, |op| {
2319                op.summary("Get proposal participation for epoch")
2320                    .description(
2321                        "Get the mapping from leader key to proposal participation rate for the \
2322                         given epoch.",
2323                    )
2324            }),
2325        )
2326        .api_route(
2327            routes::v1::NODE_VOTE_PARTICIPATION_CURRENT_ROUTE,
2328            get_with(node_vote_participation_current, |op| {
2329                op.summary("Get current vote participation").description(
2330                    "Get the mapping from node key to the fraction of views properly voted.",
2331                )
2332            }),
2333        )
2334        .api_route(
2335            routes::v1::NODE_VOTE_PARTICIPATION_ROUTE,
2336            get_with(node_vote_participation, |op| {
2337                op.summary("Get vote participation for epoch").description(
2338                    "Get the mapping from node key to vote participation rate for the given epoch.",
2339                )
2340            }),
2341        )
2342        .api_route(
2343            routes::v1::NODE_BLOCK_REWARD_ROUTE,
2344            get_with(node_block_reward, |op| {
2345                op.summary("Get block reward")
2346                    .description("Get the block reward.")
2347            }),
2348        )
2349        .api_route(
2350            routes::v1::NODE_BLOCK_REWARD_EPOCH_ROUTE,
2351            get_with(node_block_reward_epoch, |op| {
2352                op.summary("Get block reward for epoch")
2353                    .description("Get the block reward for the given epoch.")
2354            }),
2355        )
2356        .api_route(
2357            routes::v1::NODE_OLDEST_BLOCK_ROUTE,
2358            get_with(node_oldest_block, |op| {
2359                op.summary("Get oldest block").description(
2360                    "Get the oldest (smallest height) block present in storage, or null if none \
2361                     is stored.",
2362                )
2363            }),
2364        )
2365        .api_route(
2366            routes::v1::NODE_OLDEST_LEAF_ROUTE,
2367            get_with(node_oldest_leaf, |op| {
2368                op.summary("Get oldest leaf").description(
2369                    "Get the oldest (smallest height) leaf present in storage, or null if none is \
2370                     stored.",
2371                )
2372            }),
2373        )
2374        .with_state(state)
2375}
2376
2377pub(crate) fn router_catchup(state: CatchupState) -> ApiRouter {
2378    // Catchup handlers
2379    let catchup_account =
2380        |State(state): State<CatchupState>,
2381         Path((height, view, address)): Path<(u64, u64, String)>| async move {
2382            state
2383                .get_account(height, view, address)
2384                .await
2385                .map(ApiJson)
2386                .map_err(classify_availability_error)
2387        };
2388
2389    let catchup_accounts = |State(state): State<CatchupState>,
2390                            Path((height, view)): Path<(u64, u64)>,
2391                            headers: HeaderMap,
2392                            body: Bytes| async move {
2393        let accounts = decode_body(&headers, &body)?;
2394        let tree = state
2395            .get_accounts(height, view, accounts)
2396            .await
2397            .map_err(classify_availability_error)?;
2398        Ok::<_, ApiError>(encode_response(&headers, tree))
2399    };
2400
2401    let catchup_blocks = |State(state): State<CatchupState>,
2402                          Path((height, view)): Path<(u64, u64)>| async move {
2403        state
2404            .get_blocks_frontier(height, view)
2405            .await
2406            .map(ApiJson)
2407            .map_err(classify_availability_error)
2408    };
2409
2410    let catchup_chainconfig = |State(state): State<CatchupState>,
2411                               Path(commitment): Path<String>| async move {
2412        state
2413            .get_chain_config(commitment)
2414            .await
2415            .map(ApiJson)
2416            .map_err(classify_availability_error)
2417    };
2418
2419    let catchup_leafchain = |State(state): State<CatchupState>, Path(height): Path<u64>| async move {
2420        state
2421            .get_leaf_chain(height)
2422            .await
2423            .map(ApiJson)
2424            .map_err(classify_availability_error)
2425    };
2426
2427    let catchup_cert2 = |State(state): State<CatchupState>, Path(height): Path<u64>| async move {
2428        state
2429            .get_cert2(height)
2430            .await
2431            .map(ApiJson)
2432            .map_err(classify_availability_error)
2433    };
2434
2435    let catchup_reward_account =
2436        |State(state): State<CatchupState>,
2437         Path((height, view, address)): Path<(u64, u64, String)>| async move {
2438            state
2439                .get_reward_account_v1(height, view, address)
2440                .await
2441                .map(ApiJson)
2442                .map_err(classify_availability_error)
2443        };
2444
2445    let catchup_reward_accounts = |State(state): State<CatchupState>,
2446                                   Path((height, view)): Path<(u64, u64)>,
2447                                   headers: HeaderMap,
2448                                   body: Bytes| async move {
2449        let accounts = decode_body(&headers, &body)?;
2450        let tree = state
2451            .get_reward_accounts_v1(height, view, accounts)
2452            .await
2453            .map_err(classify_availability_error)?;
2454        Ok::<_, ApiError>(encode_response(&headers, tree))
2455    };
2456
2457    let catchup_reward_account_v2 =
2458        |State(state): State<CatchupState>,
2459         Path((height, view, address)): Path<(u64, u64, String)>| async move {
2460            state
2461                .get_reward_account_v2(height, view, address)
2462                .await
2463                .map(ApiJson)
2464                .map_err(classify_availability_error)
2465        };
2466
2467    let catchup_reward_accounts_v2 =
2468        |State(_): State<CatchupState>, Path((_height, _view)): Path<(u64, u64)>| async move {
2469            Err::<Json<()>, ApiError>(ApiError::NotFound(anyhow::anyhow!(
2470                "catchup/reward-accounts-v2 is deprecated"
2471            )))
2472        };
2473
2474    let catchup_reward_amounts =
2475        |State(_): State<CatchupState>, Path((_height, _limit, _offset)): Path<(u64, u64, u64)>| async move {
2476            Err::<Json<()>, ApiError>(ApiError::NotFound(anyhow::anyhow!(
2477                "catchup/reward-amounts is deprecated"
2478            )))
2479        };
2480
2481    let catchup_reward_merkle_tree_v2 =
2482        |State(state): State<CatchupState>, Path((height, view)): Path<(u64, u64)>| async move {
2483            state
2484                .get_reward_merkle_tree_v2(height, view)
2485                .await
2486                .map(ApiJson)
2487                .map_err(classify_availability_error)
2488        };
2489
2490    let catchup_state_cert = |State(state): State<CatchupState>, Path(epoch): Path<u64>| async move {
2491        state
2492            .get_state_cert(epoch)
2493            .await
2494            .map(ApiJson)
2495            .map_err(classify_availability_error)
2496    };
2497
2498    ApiRouter::new()
2499        .api_route(
2500            routes::v1::CATCHUP_ACCOUNT_ROUTE,
2501            get_with(catchup_account, |op| {
2502                op.summary("Catch up fee account balance").description(
2503                    "Get the fee account balance and Merkle proof for an address at the given \
2504                     block height and view, for catchup.",
2505                )
2506            }),
2507        )
2508        .api_route(
2509            routes::v1::CATCHUP_ACCOUNTS_ROUTE,
2510            post_with(catchup_accounts, |op| {
2511                op.summary("Catch up fee accounts (bulk)").description(
2512                    "Bulk version of the fee account endpoint; request body is a JSON array of \
2513                     TaggedBase64 fee accounts, response is a FeeMerkleTree.",
2514                )
2515            }),
2516        )
2517        .api_route(
2518            routes::v1::CATCHUP_BLOCKS_ROUTE,
2519            get_with(catchup_blocks, |op| {
2520                op.summary("Catch up blocks Merkle frontier").description(
2521                    "Get the blocks Merkle tree frontier at the given block height and view, for \
2522                     catchup.",
2523                )
2524            }),
2525        )
2526        .api_route(
2527            routes::v1::CATCHUP_CHAINCONFIG_ROUTE,
2528            get_with(catchup_chainconfig, |op| {
2529                op.summary("Catch up chain config").description(
2530                    "Retrieve the chain config matching the given commitment from a peer; used \
2531                     when a node missed a protocol upgrade.",
2532                )
2533            }),
2534        )
2535        .api_route(
2536            routes::v1::CATCHUP_LEAFCHAIN_ROUTE,
2537            get_with(catchup_leafchain, |op| {
2538                op.summary("Catch up leaf chain").description(
2539                    "Fetch a leaf chain that decides the block at the given height, for catching \
2540                     up the stake table.",
2541                )
2542            }),
2543        )
2544        .api_route(
2545            routes::v1::CATCHUP_CERT2_ROUTE,
2546            get_with(catchup_cert2, |op| {
2547                op.summary("Catch up cert2").description(
2548                    "Fetch the cert2 stored at exactly the given height, if one exists; 404 \
2549                     otherwise.",
2550                )
2551            }),
2552        )
2553        .api_route(
2554            routes::v1::CATCHUP_REWARD_ACCOUNT_ROUTE,
2555            get_with(catchup_reward_account, |op| {
2556                op.summary("Catch up reward account (V1)").description(
2557                    "Get the reward account balance for an address at the given height and view.",
2558                )
2559            }),
2560        )
2561        .api_route(
2562            routes::v1::CATCHUP_REWARD_ACCOUNTS_ROUTE,
2563            post_with(catchup_reward_accounts, |op| {
2564                op.summary("Catch up reward accounts (bulk, V1)")
2565                    .description(
2566                        "Bulk version of the reward account endpoint; request body is a JSON \
2567                         array of TaggedBase64 reward accounts, response is a RewardMerkleTreeV1.",
2568                    )
2569            }),
2570        )
2571        .api_route(
2572            routes::v1::CATCHUP_REWARD_ACCOUNT_V2_ROUTE,
2573            get_with(catchup_reward_account_v2, |op| {
2574                op.summary("Catch up reward account (V2)").description(
2575                    "Get the reward account balance for an address at the given height and view, \
2576                     from RewardMerkleTreeV2.",
2577                )
2578            }),
2579        )
2580        .api_route(
2581            routes::v1::CATCHUP_REWARD_ACCOUNTS_V2_ROUTE,
2582            post_with(catchup_reward_accounts_v2, |op| {
2583                op.summary("Catch up reward accounts (bulk, V2) (deprecated)")
2584                    .description("Deprecated: this endpoint always returns 404 Not Found.")
2585            }),
2586        )
2587        .api_route(
2588            routes::v1::CATCHUP_REWARD_AMOUNTS_ROUTE,
2589            get_with(catchup_reward_amounts, |op| {
2590                op.summary("List reward amounts (deprecated)")
2591                    .description("Deprecated: this endpoint always returns 404 Not Found.")
2592            }),
2593        )
2594        .api_route(
2595            routes::v1::CATCHUP_REWARD_MERKLE_TREE_V2_ROUTE,
2596            get_with(catchup_reward_merkle_tree_v2, |op| {
2597                op.summary("Catch up RewardMerkleTreeV2").description(
2598                    "Get the RewardMerkleTreeV2 from consensus state at the given height and \
2599                     view, serialized as RewardMerkleTreeV2Data.",
2600                )
2601            }),
2602        )
2603        .api_route(
2604            routes::v1::CATCHUP_STATE_CERT_ROUTE,
2605            get_with(catchup_state_cert, |op| {
2606                op.summary("Catch up state certificate")
2607                    .description("Get the light client state certificate for the given epoch.")
2608            }),
2609        )
2610        .with_state(state)
2611}
2612
2613pub(crate) fn router_submit(state: SubmitState) -> ApiRouter {
2614    // Submit handler: body is decoded as VBS (binary) or JSON based on Content-Type, matching
2615    // tide-disco's `body_auto`.
2616    let submit_submit = |State(state): State<SubmitState>, headers: HeaderMap, body: Bytes| async move {
2617        let tx = decode_body(&headers, &body)?;
2618        let hash = state
2619            .submit(tx)
2620            .await
2621            .map_err(classify_availability_error)?;
2622        Ok::<_, ApiError>(encode_response(&headers, hash))
2623    };
2624
2625    ApiRouter::new()
2626        .api_route(
2627            routes::v1::SUBMIT_ROUTE,
2628            post_with(submit_submit, |op| {
2629                op.summary("Submit transaction")
2630                    .description("Submit a transaction to the HotShot handle for sequencing.")
2631            }),
2632        )
2633        .with_state(state)
2634}
2635
2636pub(crate) fn router_state_signature(state: StateSignatureState) -> ApiRouter {
2637    // State signature handler
2638    let state_signature_block = |State(state): State<StateSignatureState>,
2639                                 Path(height): Path<u64>| async move {
2640        state
2641            .get_state_signature(height)
2642            .await
2643            .map(ApiJson)
2644            .map_err(classify_availability_error)
2645    };
2646
2647    ApiRouter::new()
2648        .api_route(
2649            routes::v1::STATE_SIGNATURE_BLOCK_ROUTE,
2650            get_with(state_signature_block, |op| {
2651                op.summary("Get light client state signature").description(
2652                    "Get this node's signature for the light client state at the given block \
2653                     height.",
2654                )
2655            }),
2656        )
2657        .with_state(state)
2658}
2659
2660pub(crate) fn router_light_client(state: LightClientState) -> ApiRouter {
2661    // Light-client handlers
2662    let lc_leaf_by_height = |State(state): State<LightClientState>, Path(height): Path<u64>| async move {
2663        state
2664            .get_leaf_proof(v1::LeafQuery::Height(height), None)
2665            .await
2666            .map(ApiJson)
2667            .map_err(classify_availability_error)
2668    };
2669
2670    let lc_leaf_by_height_finalized =
2671        |State(state): State<LightClientState>, Path((height, finalized)): Path<(u64, u64)>| async move {
2672            state
2673                .get_leaf_proof(v1::LeafQuery::Height(height), Some(finalized))
2674                .await
2675                .map(ApiJson)
2676                .map_err(classify_availability_error)
2677        };
2678
2679    let lc_leaf_by_hash = |State(state): State<LightClientState>, Path(hash): Path<String>| async move {
2680        state
2681            .get_leaf_proof(v1::LeafQuery::Hash(hash), None)
2682            .await
2683            .map(ApiJson)
2684            .map_err(classify_availability_error)
2685    };
2686
2687    let lc_leaf_by_hash_finalized =
2688        |State(state): State<LightClientState>, Path((hash, finalized)): Path<(String, u64)>| async move {
2689            state
2690                .get_leaf_proof(v1::LeafQuery::Hash(hash), Some(finalized))
2691                .await
2692                .map(ApiJson)
2693                .map_err(classify_availability_error)
2694        };
2695
2696    let lc_leaf_by_block_hash = |State(state): State<LightClientState>,
2697                                 Path(block_hash): Path<String>| async move {
2698        state
2699            .get_leaf_proof(v1::LeafQuery::BlockHash(block_hash), None)
2700            .await
2701            .map(ApiJson)
2702            .map_err(classify_availability_error)
2703    };
2704
2705    let lc_leaf_by_block_hash_finalized =
2706        |State(state): State<LightClientState>,
2707         Path((block_hash, finalized)): Path<(String, u64)>| async move {
2708            state
2709                .get_leaf_proof(v1::LeafQuery::BlockHash(block_hash), Some(finalized))
2710                .await
2711                .map(ApiJson)
2712                .map_err(classify_availability_error)
2713        };
2714
2715    let lc_leaf_by_payload_hash =
2716        |State(state): State<LightClientState>, Path(payload_hash): Path<String>| async move {
2717            state
2718                .get_leaf_proof(v1::LeafQuery::PayloadHash(payload_hash), None)
2719                .await
2720                .map(ApiJson)
2721                .map_err(classify_availability_error)
2722        };
2723
2724    let lc_leaf_by_payload_hash_finalized =
2725        |State(state): State<LightClientState>,
2726         Path((payload_hash, finalized)): Path<(String, u64)>| async move {
2727            state
2728                .get_leaf_proof(v1::LeafQuery::PayloadHash(payload_hash), Some(finalized))
2729                .await
2730                .map(ApiJson)
2731                .map_err(classify_availability_error)
2732        };
2733
2734    let lc_header_by_height =
2735        |State(state): State<LightClientState>, Path((root, height)): Path<(u64, u64)>| async move {
2736            state
2737                .get_header_proof(root, v1::HeaderQuery::Height(height))
2738                .await
2739                .map(ApiJson)
2740                .map_err(classify_availability_error)
2741        };
2742
2743    let lc_header_by_hash = |State(state): State<LightClientState>,
2744                             Path((root, hash)): Path<(u64, String)>| async move {
2745        state
2746            .get_header_proof(root, v1::HeaderQuery::Hash(hash))
2747            .await
2748            .map(ApiJson)
2749            .map_err(classify_availability_error)
2750    };
2751
2752    let lc_header_by_payload_hash =
2753        |State(state): State<LightClientState>, Path((root, payload_hash)): Path<(u64, String)>| async move {
2754            state
2755                .get_header_proof(root, v1::HeaderQuery::PayloadHash(payload_hash))
2756                .await
2757                .map(ApiJson)
2758                .map_err(classify_availability_error)
2759        };
2760
2761    let lc_stake_table = |State(state): State<LightClientState>, Path(epoch): Path<u64>| async move {
2762        state
2763            .get_light_client_stake_table(epoch)
2764            .await
2765            .map(ApiJson)
2766            .map_err(classify_availability_error)
2767    };
2768
2769    let lc_payload = |State(state): State<LightClientState>, Path(height): Path<u64>| async move {
2770        state
2771            .get_payload_proof(height)
2772            .await
2773            .map(ApiJson)
2774            .map_err(classify_availability_error)
2775    };
2776
2777    let lc_payload_range = |State(state): State<LightClientState>,
2778                            Path((start, end)): Path<(u64, u64)>| async move {
2779        state
2780            .get_payload_proof_range(start, end)
2781            .await
2782            .map(ApiJson)
2783            .map_err(classify_availability_error)
2784    };
2785
2786    let lc_payload_ranges =
2787        |State(state): State<LightClientState>, headers: HeaderMap, body: Bytes| async move {
2788            let ranges: Vec<Range<u64>> = decode_body(&headers, &body)?;
2789            let proofs = state
2790                .get_payload_proof_ranges(ranges)
2791                .await
2792                .map_err(classify_availability_error)?;
2793            Ok::<_, ApiError>(encode_response(&headers, proofs))
2794        };
2795
2796    let lc_namespace = |State(state): State<LightClientState>,
2797                        Path((height, namespace)): Path<(u64, u64)>| async move {
2798        state
2799            .get_lc_namespace_proof(height, namespace)
2800            .await
2801            .map(ApiJson)
2802            .map_err(classify_availability_error)
2803    };
2804
2805    let lc_namespace_range =
2806        |State(state): State<LightClientState>,
2807         Path((start, end, namespace)): Path<(u64, u64, u64)>| async move {
2808            state
2809                .get_lc_namespace_proof_range(start, end, namespace)
2810                .await
2811                .map(ApiJson)
2812                .map_err(classify_availability_error)
2813        };
2814
2815    let lc_namespaces_range =
2816        |State(state): State<LightClientState>,
2817         Path((start, end, namespaces)): Path<(u64, u64, String)>| async move {
2818            state
2819                .get_lc_namespaces_proof_range(start, end, namespaces)
2820                .await
2821                .map(ApiJson)
2822                .map_err(classify_availability_error)
2823        };
2824
2825    ApiRouter::new()
2826        .api_route(
2827            routes::v1::LC_LEAF_BY_HEIGHT_ROUTE,
2828            get_with(lc_leaf_by_height, |op| {
2829                op.summary("Get leaf with finality proof").description(
2830                    "Fetch a leaf by height plus a proof of its finality, optionally relative to \
2831                     an already-known-finalized height.",
2832                )
2833            }),
2834        )
2835        .api_route(
2836            routes::v1::LC_LEAF_BY_HEIGHT_FINALIZED_ROUTE,
2837            get_with(lc_leaf_by_height_finalized, |op| {
2838                op.summary("Get leaf with finality proof").description(
2839                    "Fetch a leaf by height plus a proof of its finality, optionally relative to \
2840                     an already-known-finalized height.",
2841                )
2842            }),
2843        )
2844        .api_route(
2845            routes::v1::LC_LEAF_BY_HASH_ROUTE,
2846            get_with(lc_leaf_by_hash, |op| {
2847                op.summary("Get leaf with finality proof").description(
2848                    "Fetch a leaf by hash plus a proof of its finality, optionally relative to an \
2849                     already-known-finalized height.",
2850                )
2851            }),
2852        )
2853        .api_route(
2854            routes::v1::LC_LEAF_BY_HASH_FINALIZED_ROUTE,
2855            get_with(lc_leaf_by_hash_finalized, |op| {
2856                op.summary("Get leaf with finality proof").description(
2857                    "Fetch a leaf by hash plus a proof of its finality, optionally relative to an \
2858                     already-known-finalized height.",
2859                )
2860            }),
2861        )
2862        .api_route(
2863            routes::v1::LC_LEAF_BY_BLOCK_HASH_ROUTE,
2864            get_with(lc_leaf_by_block_hash, |op| {
2865                op.summary("Get leaf with finality proof").description(
2866                    "Fetch a leaf by block hash plus a proof of its finality, optionally relative \
2867                     to an already-known-finalized height.",
2868                )
2869            }),
2870        )
2871        .api_route(
2872            routes::v1::LC_LEAF_BY_BLOCK_HASH_FINALIZED_ROUTE,
2873            get_with(lc_leaf_by_block_hash_finalized, |op| {
2874                op.summary("Get leaf with finality proof").description(
2875                    "Fetch a leaf by block hash plus a proof of its finality, optionally relative \
2876                     to an already-known-finalized height.",
2877                )
2878            }),
2879        )
2880        .api_route(
2881            routes::v1::LC_LEAF_BY_PAYLOAD_HASH_ROUTE,
2882            get_with(lc_leaf_by_payload_hash, |op| {
2883                op.summary("Get leaf with finality proof").description(
2884                    "Fetch a leaf by payload hash plus a proof of its finality, optionally \
2885                     relative to an already-known-finalized height.",
2886                )
2887            }),
2888        )
2889        .api_route(
2890            routes::v1::LC_LEAF_BY_PAYLOAD_HASH_FINALIZED_ROUTE,
2891            get_with(lc_leaf_by_payload_hash_finalized, |op| {
2892                op.summary("Get leaf with finality proof").description(
2893                    "Fetch a leaf by payload hash plus a proof of its finality, optionally \
2894                     relative to an already-known-finalized height.",
2895                )
2896            }),
2897        )
2898        .api_route(
2899            routes::v1::LC_HEADER_BY_HEIGHT_ROUTE,
2900            get_with(lc_header_by_height, |op| {
2901                op.summary("Get header with inclusion proof").description(
2902                    "Fetch a header plus a Merkle proof that it belongs to the blocks Merkle tree \
2903                     rooted at the given root height.",
2904                )
2905            }),
2906        )
2907        .api_route(
2908            routes::v1::LC_HEADER_BY_HASH_ROUTE,
2909            get_with(lc_header_by_hash, |op| {
2910                op.summary("Get header with inclusion proof").description(
2911                    "Fetch a header plus a Merkle proof that it belongs to the blocks Merkle tree \
2912                     rooted at the given root height.",
2913                )
2914            }),
2915        )
2916        .api_route(
2917            routes::v1::LC_HEADER_BY_PAYLOAD_HASH_ROUTE,
2918            get_with(lc_header_by_payload_hash, |op| {
2919                op.summary("Get header with inclusion proof").description(
2920                    "Fetch a header plus a Merkle proof that it belongs to the blocks Merkle tree \
2921                     rooted at the given root height.",
2922                )
2923            }),
2924        )
2925        .api_route(
2926            routes::v1::LC_STAKE_TABLE_ROUTE,
2927            get_with(lc_stake_table, |op| {
2928                op.summary("Get stake table events for epoch").description(
2929                    "Get the events needed to transform the stake table from the previous epoch \
2930                     into the given epoch.",
2931                )
2932            }),
2933        )
2934        .api_route(
2935            routes::v1::LC_PAYLOAD_ROUTE,
2936            get_with(lc_payload, |op| {
2937                op.summary("Get payload with VID common data").description(
2938                    "Fetch a payload plus the VID common data needed to recompute and verify its \
2939                     hash.",
2940                )
2941            }),
2942        )
2943        .api_route(
2944            routes::v1::LC_PAYLOAD_RANGE_ROUTE,
2945            get_with(lc_payload_range, |op| {
2946                op.summary("Get payload proofs in range").description(
2947                    "Fetch a list of payload proofs for each block in the given range.",
2948                )
2949            }),
2950        )
2951        .api_route(
2952            routes::v1::LC_PAYLOAD_RANGES_ROUTE,
2953            post_with(lc_payload_ranges, |op| {
2954                op.summary("Get payload proofs for a set of height ranges")
2955                    .description(
2956                        "Fetch payload proofs for the height ranges in the request body, which \
2957                         must be ascending and disjoint. Answers in full or not at all, like the \
2958                         range endpoint: heights this node lacks are fetched from peers, and a \
2959                         404 means at least one was not available in time.",
2960                    )
2961            }),
2962        )
2963        .api_route(
2964            routes::v1::LC_NAMESPACE_ROUTE,
2965            get_with(lc_namespace, |op| {
2966                op.summary("Get namespace proof with VID common data")
2967                    .description(
2968                        "Fetch a namespace proof plus the VID common data needed to verify it.",
2969                    )
2970            }),
2971        )
2972        .api_route(
2973            routes::v1::LC_NAMESPACE_RANGE_ROUTE,
2974            get_with(lc_namespace_range, |op| {
2975                op.summary("Get namespace proofs in range").description(
2976                    "Fetch a list of namespace proofs for each block in the given range.",
2977                )
2978            }),
2979        )
2980        .api_route(
2981            routes::v1::LC_NAMESPACES_RANGE_ROUTE,
2982            get_with(lc_namespaces_range, |op| {
2983                op.summary("Get proofs for multiple namespaces in range")
2984                    .description(
2985                        "Fetch namespace proofs for each block in the given range, restricted to \
2986                         a caller-specified set of namespaces.",
2987                    )
2988            }),
2989        )
2990        .with_state(state)
2991}
2992
2993pub(crate) fn router_explorer(state: ExplorerState) -> ApiRouter {
2994    // Explorer handlers
2995    let explorer_block_detail_by_height =
2996        |State(state): State<ExplorerState>, Path(height): Path<u64>| async move {
2997            state
2998                .get_block_detail(v1::BlockIdent::Height(height))
2999                .await
3000                .map(ApiJson)
3001                .map_err(classify_availability_error)
3002        };
3003
3004    let explorer_block_detail_by_hash =
3005        |State(state): State<ExplorerState>, Path(hash): Path<String>| async move {
3006            state
3007                .get_block_detail(v1::BlockIdent::Hash(hash))
3008                .await
3009                .map(ApiJson)
3010                .map_err(classify_availability_error)
3011        };
3012
3013    let explorer_block_summaries_latest =
3014        |State(state): State<ExplorerState>, Path(limit): Path<u64>| async move {
3015            state
3016                .get_block_summaries(v1::BlockIdent::Latest, limit)
3017                .await
3018                .map(ApiJson)
3019                .map_err(classify_availability_error)
3020        };
3021
3022    let explorer_block_summaries_from =
3023        |State(state): State<ExplorerState>, Path((from, limit)): Path<(u64, u64)>| async move {
3024            state
3025                .get_block_summaries(v1::BlockIdent::Height(from), limit)
3026                .await
3027                .map(ApiJson)
3028                .map_err(classify_availability_error)
3029        };
3030
3031    let explorer_tx_detail_by_position =
3032        |State(state): State<ExplorerState>, Path((height, offset)): Path<(u64, u64)>| async move {
3033            state
3034                .get_transaction_detail(v1::TxIdent::HeightAndOffset(height, offset))
3035                .await
3036                .map(ApiJson)
3037                .map_err(classify_availability_error)
3038        };
3039
3040    let explorer_tx_detail_by_hash =
3041        |State(state): State<ExplorerState>, Path(hash): Path<String>| async move {
3042            state
3043                .get_transaction_detail(v1::TxIdent::Hash(hash))
3044                .await
3045                .map(ApiJson)
3046                .map_err(classify_availability_error)
3047        };
3048
3049    let explorer_tx_summaries_latest_block =
3050        |State(state): State<ExplorerState>, Path((limit, block)): Path<(u64, u64)>| async move {
3051            state
3052                .get_transaction_summaries(
3053                    v1::TxIdent::Latest,
3054                    limit,
3055                    v1::TxSummaryFilter::Block(block),
3056                )
3057                .await
3058                .map(ApiJson)
3059                .map_err(classify_availability_error)
3060        };
3061
3062    let explorer_tx_summaries_from_block =
3063        |State(state): State<ExplorerState>,
3064         Path((height, offset, limit, block)): Path<(u64, u64, u64, u64)>| async move {
3065            state
3066                .get_transaction_summaries(
3067                    v1::TxIdent::HeightAndOffset(height, offset),
3068                    limit,
3069                    v1::TxSummaryFilter::Block(block),
3070                )
3071                .await
3072                .map(ApiJson)
3073                .map_err(classify_availability_error)
3074        };
3075
3076    let explorer_tx_summaries_by_hash_block =
3077        |State(state): State<ExplorerState>,
3078         Path((hash, limit, block)): Path<(String, u64, u64)>| async move {
3079            state
3080                .get_transaction_summaries(
3081                    v1::TxIdent::Hash(hash),
3082                    limit,
3083                    v1::TxSummaryFilter::Block(block),
3084                )
3085                .await
3086                .map(ApiJson)
3087                .map_err(classify_availability_error)
3088        };
3089
3090    let explorer_tx_summaries_latest_ns =
3091        |State(state): State<ExplorerState>, Path((limit, namespace)): Path<(u64, i64)>| async move {
3092            state
3093                .get_transaction_summaries(
3094                    v1::TxIdent::Latest,
3095                    limit,
3096                    v1::TxSummaryFilter::Namespace(namespace),
3097                )
3098                .await
3099                .map(ApiJson)
3100                .map_err(classify_availability_error)
3101        };
3102
3103    let explorer_tx_summaries_from_ns =
3104        |State(state): State<ExplorerState>,
3105         Path((height, offset, limit, namespace)): Path<(u64, u64, u64, i64)>| async move {
3106            state
3107                .get_transaction_summaries(
3108                    v1::TxIdent::HeightAndOffset(height, offset),
3109                    limit,
3110                    v1::TxSummaryFilter::Namespace(namespace),
3111                )
3112                .await
3113                .map(ApiJson)
3114                .map_err(classify_availability_error)
3115        };
3116
3117    let explorer_tx_summaries_by_hash_ns =
3118        |State(state): State<ExplorerState>,
3119         Path((hash, limit, namespace)): Path<(String, u64, i64)>| async move {
3120            state
3121                .get_transaction_summaries(
3122                    v1::TxIdent::Hash(hash),
3123                    limit,
3124                    v1::TxSummaryFilter::Namespace(namespace),
3125                )
3126                .await
3127                .map(ApiJson)
3128                .map_err(classify_availability_error)
3129        };
3130
3131    let explorer_tx_summaries_latest =
3132        |State(state): State<ExplorerState>, Path(limit): Path<u64>| async move {
3133            state
3134                .get_transaction_summaries(v1::TxIdent::Latest, limit, v1::TxSummaryFilter::None)
3135                .await
3136                .map(ApiJson)
3137                .map_err(classify_availability_error)
3138        };
3139
3140    let explorer_tx_summaries_from =
3141        |State(state): State<ExplorerState>,
3142         Path((height, offset, limit)): Path<(u64, u64, u64)>| async move {
3143            state
3144                .get_transaction_summaries(
3145                    v1::TxIdent::HeightAndOffset(height, offset),
3146                    limit,
3147                    v1::TxSummaryFilter::None,
3148                )
3149                .await
3150                .map(ApiJson)
3151                .map_err(classify_availability_error)
3152        };
3153
3154    let explorer_tx_summaries_by_hash =
3155        |State(state): State<ExplorerState>, Path((hash, limit)): Path<(String, u64)>| async move {
3156            state
3157                .get_transaction_summaries(
3158                    v1::TxIdent::Hash(hash),
3159                    limit,
3160                    v1::TxSummaryFilter::None,
3161                )
3162                .await
3163                .map(ApiJson)
3164                .map_err(classify_availability_error)
3165        };
3166
3167    let explorer_summary = |State(state): State<ExplorerState>| async move {
3168        state
3169            .get_explorer_summary()
3170            .await
3171            .map(ApiJson)
3172            .map_err(classify_availability_error)
3173    };
3174
3175    let explorer_search = |State(state): State<ExplorerState>, Path(query): Path<String>| async move {
3176        state
3177            .get_search_result(query)
3178            .await
3179            .map(ApiJson)
3180            .map_err(classify_availability_error)
3181    };
3182
3183    ApiRouter::new()
3184        .api_route(
3185            routes::v1::EXPLORER_BLOCK_DETAIL_BY_HEIGHT_ROUTE,
3186            get_with(explorer_block_detail_by_height, |op| {
3187                op.summary("Get block detail")
3188                    .description("Get details for a block identified by height or hash.")
3189            }),
3190        )
3191        .api_route(
3192            routes::v1::EXPLORER_BLOCK_DETAIL_BY_HASH_ROUTE,
3193            get_with(explorer_block_detail_by_hash, |op| {
3194                op.summary("Get block detail")
3195                    .description("Get details for a block identified by height or hash.")
3196            }),
3197        )
3198        .api_route(
3199            routes::v1::EXPLORER_BLOCK_SUMMARIES_LATEST_ROUTE,
3200            get_with(explorer_block_summaries_latest, |op| {
3201                op.summary("List block summaries").description(
3202                    "Retrieve up to `limit` block summaries, targeting the latest block or a \
3203                     block identified by height.",
3204                )
3205            }),
3206        )
3207        .api_route(
3208            routes::v1::EXPLORER_BLOCK_SUMMARIES_FROM_ROUTE,
3209            get_with(explorer_block_summaries_from, |op| {
3210                op.summary("List block summaries").description(
3211                    "Retrieve up to `limit` block summaries, targeting the latest block or a \
3212                     block identified by height.",
3213                )
3214            }),
3215        )
3216        .api_route(
3217            routes::v1::EXPLORER_TX_DETAIL_BY_POSITION_ROUTE,
3218            get_with(explorer_tx_detail_by_position, |op| {
3219                op.summary("Get transaction detail").description(
3220                    "Get details for a transaction identified by height and offset, or by hash.",
3221                )
3222            }),
3223        )
3224        .api_route(
3225            routes::v1::EXPLORER_TX_DETAIL_BY_HASH_ROUTE,
3226            get_with(explorer_tx_detail_by_hash, |op| {
3227                op.summary("Get transaction detail").description(
3228                    "Get details for a transaction identified by height and offset, or by hash.",
3229                )
3230            }),
3231        )
3232        .api_route(
3233            routes::v1::EXPLORER_TX_SUMMARIES_LATEST_BLOCK_ROUTE,
3234            get_with(explorer_tx_summaries_latest_block, |op| {
3235                op.summary("List transaction summaries").description(
3236                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3237                     transaction, one identified by height/offset, or by hash; optionally \
3238                     filtered by block or namespace.",
3239                )
3240            }),
3241        )
3242        .api_route(
3243            routes::v1::EXPLORER_TX_SUMMARIES_FROM_BLOCK_ROUTE,
3244            get_with(explorer_tx_summaries_from_block, |op| {
3245                op.summary("List transaction summaries").description(
3246                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3247                     transaction, one identified by height/offset, or by hash; optionally \
3248                     filtered by block or namespace.",
3249                )
3250            }),
3251        )
3252        .api_route(
3253            routes::v1::EXPLORER_TX_SUMMARIES_BY_HASH_BLOCK_ROUTE,
3254            get_with(explorer_tx_summaries_by_hash_block, |op| {
3255                op.summary("List transaction summaries").description(
3256                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3257                     transaction, one identified by height/offset, or by hash; optionally \
3258                     filtered by block or namespace.",
3259                )
3260            }),
3261        )
3262        .api_route(
3263            routes::v1::EXPLORER_TX_SUMMARIES_LATEST_NS_ROUTE,
3264            get_with(explorer_tx_summaries_latest_ns, |op| {
3265                op.summary("List transaction summaries").description(
3266                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3267                     transaction, one identified by height/offset, or by hash; optionally \
3268                     filtered by block or namespace.",
3269                )
3270            }),
3271        )
3272        .api_route(
3273            routes::v1::EXPLORER_TX_SUMMARIES_FROM_NS_ROUTE,
3274            get_with(explorer_tx_summaries_from_ns, |op| {
3275                op.summary("List transaction summaries").description(
3276                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3277                     transaction, one identified by height/offset, or by hash; optionally \
3278                     filtered by block or namespace.",
3279                )
3280            }),
3281        )
3282        .api_route(
3283            routes::v1::EXPLORER_TX_SUMMARIES_BY_HASH_NS_ROUTE,
3284            get_with(explorer_tx_summaries_by_hash_ns, |op| {
3285                op.summary("List transaction summaries").description(
3286                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3287                     transaction, one identified by height/offset, or by hash; optionally \
3288                     filtered by block or namespace.",
3289                )
3290            }),
3291        )
3292        .api_route(
3293            routes::v1::EXPLORER_TX_SUMMARIES_LATEST_ROUTE,
3294            get_with(explorer_tx_summaries_latest, |op| {
3295                op.summary("List transaction summaries").description(
3296                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3297                     transaction, one identified by height/offset, or by hash; optionally \
3298                     filtered by block or namespace.",
3299                )
3300            }),
3301        )
3302        .api_route(
3303            routes::v1::EXPLORER_TX_SUMMARIES_FROM_ROUTE,
3304            get_with(explorer_tx_summaries_from, |op| {
3305                op.summary("List transaction summaries").description(
3306                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3307                     transaction, one identified by height/offset, or by hash; optionally \
3308                     filtered by block or namespace.",
3309                )
3310            }),
3311        )
3312        .api_route(
3313            routes::v1::EXPLORER_TX_SUMMARIES_BY_HASH_ROUTE,
3314            get_with(explorer_tx_summaries_by_hash, |op| {
3315                op.summary("List transaction summaries").description(
3316                    "Retrieve up to `limit` transaction summaries, targeting the latest \
3317                     transaction, one identified by height/offset, or by hash; optionally \
3318                     filtered by block or namespace.",
3319                )
3320            }),
3321        )
3322        .api_route(
3323            routes::v1::EXPLORER_SUMMARY_ROUTE,
3324            get_with(explorer_summary, |op| {
3325                op.summary("Get explorer summary")
3326                    .description("Get the current chain explorer summary.")
3327            }),
3328        )
3329        .api_route(
3330            routes::v1::EXPLORER_SEARCH_ROUTE,
3331            get_with(explorer_search, |op| {
3332                op.summary("Search blocks and transactions").description(
3333                    "Search for blocks or transactions matching the given query string; currently \
3334                     matched against hash.",
3335                )
3336            }),
3337        )
3338        .with_state(state)
3339}
3340
3341pub(crate) fn router_token(state: TokenState) -> ApiRouter {
3342    // Token handlers
3343    let token_total_minted = |State(state): State<TokenState>| async move {
3344        state
3345            .total_minted_supply()
3346            .await
3347            .map(ApiJson)
3348            .map_err(classify_availability_error)
3349    };
3350
3351    let token_circulating = |State(state): State<TokenState>| async move {
3352        state
3353            .circulating_supply()
3354            .await
3355            .map(ApiJson)
3356            .map_err(classify_availability_error)
3357    };
3358
3359    let token_circulating_eth = |State(state): State<TokenState>| async move {
3360        state
3361            .circulating_supply_ethereum()
3362            .await
3363            .map(ApiJson)
3364            .map_err(classify_availability_error)
3365    };
3366
3367    let token_total_issued = |State(state): State<TokenState>| async move {
3368        state
3369            .total_issued_supply()
3370            .await
3371            .map(ApiJson)
3372            .map_err(classify_availability_error)
3373    };
3374
3375    let token_total_reward_distributed = |State(state): State<TokenState>| async move {
3376        state
3377            .total_reward_distributed()
3378            .await
3379            .map(ApiJson)
3380            .map_err(classify_availability_error)
3381    };
3382
3383    ApiRouter::new()
3384        .api_route(
3385            routes::v1::TOKEN_TOTAL_MINTED_SUPPLY_ROUTE,
3386            get_with(token_total_minted, |op| {
3387                op.summary("Get total minted supply").description(
3388                    "Total supply of the ESP token minted on Ethereum; excludes unclaimed \
3389                     rewards. Cached for an hour.",
3390                )
3391            }),
3392        )
3393        .api_route(
3394            routes::v1::TOKEN_CIRCULATING_SUPPLY_ROUTE,
3395            get_with(token_circulating, |op| {
3396                op.summary("Get circulating supply").description(
3397                    "Circulating supply: initial_supply + reward_distributed - locked, following \
3398                     the mainnet unlock schedule.",
3399                )
3400            }),
3401        )
3402        .api_route(
3403            routes::v1::TOKEN_CIRCULATING_SUPPLY_ETHEREUM_ROUTE,
3404            get_with(token_circulating_eth, |op| {
3405                op.summary("Get circulating supply (Ethereum L1)")
3406                    .description(
3407                        "Circulating supply of ESP tokens on Ethereum L1: total_supply_l1 - \
3408                         locked.",
3409                    )
3410            }),
3411        )
3412        .api_route(
3413            routes::v1::TOKEN_TOTAL_ISSUED_SUPPLY_ROUTE,
3414            get_with(token_total_issued, |op| {
3415                op.summary("Get total issued supply").description(
3416                    "Total issued supply: initial_supply + total_reward_distributed, including \
3417                     rewards not yet claimed on Ethereum.",
3418                )
3419            }),
3420        )
3421        .api_route(
3422            routes::v1::TOKEN_TOTAL_REWARD_DISTRIBUTED_ROUTE,
3423            get_with(token_total_reward_distributed, |op| {
3424                op.summary("Get total reward distributed").description(
3425                    "Total rewards distributed by consensus, including rewards not yet claimed on \
3426                     Ethereum.",
3427                )
3428            }),
3429        )
3430        .with_state(state)
3431}
3432
3433pub(crate) fn router_database(state: DatabaseState) -> ApiRouter {
3434    // Database handlers
3435    let database_table_sizes = |State(state): State<DatabaseState>| async move {
3436        state
3437            .get_table_sizes()
3438            .await
3439            .map(ApiJson)
3440            .map_err(ApiError::Internal)
3441    };
3442    let database_migration_status = |State(state): State<DatabaseState>| async move {
3443        state
3444            .get_migration_status()
3445            .await
3446            .map(ApiJson)
3447            .map_err(ApiError::Internal)
3448    };
3449
3450    ApiRouter::new()
3451        .api_route(
3452            routes::v1::DATABASE_TABLE_SIZES_ROUTE,
3453            get_with(database_table_sizes, |op| {
3454                op.summary("Get database table sizes")
3455                    .description("Get the sizes of all database tables: row counts and disk usage.")
3456            }),
3457        )
3458        .api_route(
3459            routes::v1::DATABASE_MIGRATION_STATUS_ROUTE,
3460            get_with(database_migration_status, |op| {
3461                op.summary("Get migration status").description(
3462                    "Get the status of all deferred background migrations: start/completion time \
3463                     and last processed offset.",
3464                )
3465            }),
3466        )
3467        .with_state(state)
3468}
3469
3470/// Create v1 router with OpenAPI documentation.
3471///
3472/// Unlike v2 (whose spec comes from the protos, with real schemas for every request and response;
3473/// see [`router_v2_docs`]), most v1 handlers return internal domain types that don't implement
3474/// `schemars::JsonSchema` by design - see [`ApiJson`]. The generated spec therefore documents
3475/// routes, parameters, and summaries, but response bodies are mostly untyped.
3476pub fn create_router_v1<S>(state: S) -> Router
3477where
3478    S: v1::RewardApi
3479        + v1::AvailabilityApi
3480        + v1::HotShotAvailabilityApi
3481        + v1::BlockStateApi
3482        + v1::FeeStateApi
3483        + v1::StatusApi
3484        + v1::ConfigApi
3485        + v1::NodeApi
3486        + v1::CatchupApi
3487        + v1::SubmitApi
3488        + v1::StateSignatureApi
3489        + v1::LightClientApi
3490        + v1::ExplorerApi
3491        + v1::TokenApi
3492        + v1::DatabaseApi
3493        + Send
3494        + Sync
3495        + 'static,
3496{
3497    // Each `router_*` function already calls `with_state`, so the merged router is already
3498    // stateless (`ApiRouter<()>`) by the time it reaches `finish_api`.
3499    let state = Arc::new(state);
3500    let router = router_reward(state.clone())
3501        .merge(router_availability(state.clone()))
3502        .merge(router_block_state(state.clone()))
3503        .merge(router_fee_state(state.clone()))
3504        .merge(router_status(state.clone()))
3505        .merge(router_config(state.clone()))
3506        .merge(router_node(state.clone()))
3507        .merge(router_catchup(state.clone()))
3508        .merge(router_submit(state.clone()))
3509        .merge(router_state_signature(state.clone()))
3510        .merge(router_light_client(state.clone()))
3511        .merge(router_explorer(state.clone()))
3512        .merge(router_token(state.clone()))
3513        .merge(router_database(state));
3514
3515    finish_v1_docs(router)
3516}
3517
3518/// Give framework-level rejections on the v2 routes the same error envelope as handler errors.
3519///
3520/// The generated handlers extract with `Query<T>` or, on a POST, `Json<T>`, and axum answers a
3521/// rejected query string or body itself, in `text/plain`, before `tonic_rest::RestError` is ever
3522/// involved. protoJSON rejects unknown fields, which makes this the most common client error on
3523/// these routes. Rebuilding the rejection as a [`tonic::Status`] reuses tonic-rest's envelope.
3524pub(crate) async fn v2_error_envelope(req: Request, next: axum::middleware::Next) -> Response {
3525    /// Rejection bodies are single-line messages; this only needs to be larger than one.
3526    const MAX_REJECTION_BODY: usize = 8 * 1024;
3527
3528    let response = next.run(req).await;
3529
3530    // Only the statuses whose gRPC code maps back to the same HTTP status, so the rewrite cannot
3531    // change what the client sees beyond the body shape. The body extractor's 413, 415 and 422
3532    // are the exception: the Google error model has no code for any of them, so they become the
3533    // 400 every other malformed request gets.
3534    let code = match response.status() {
3535        StatusCode::BAD_REQUEST
3536        | StatusCode::PAYLOAD_TOO_LARGE
3537        | StatusCode::UNSUPPORTED_MEDIA_TYPE
3538        | StatusCode::UNPROCESSABLE_ENTITY => tonic::Code::InvalidArgument,
3539        StatusCode::NOT_FOUND => tonic::Code::NotFound,
3540        _ => return response,
3541    };
3542    let already_enveloped = response
3543        .headers()
3544        .get(header::CONTENT_TYPE)
3545        .is_some_and(|value| value.as_bytes().starts_with(b"application/json"));
3546    if already_enveloped {
3547        return response;
3548    }
3549
3550    let (parts, body) = response.into_parts();
3551    let message = match axum::body::to_bytes(body, MAX_REJECTION_BODY).await {
3552        Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
3553        Err(_) => parts
3554            .status
3555            .canonical_reason()
3556            .unwrap_or("request rejected")
3557            .to_string(),
3558    };
3559    tonic_rest::RestError::from(tonic::Status::new(code, message)).into_response()
3560}
3561
3562/// A POST reads its whole request from the body, so the generated handler never looks at the
3563/// query string. Refusing one keeps the rule that a misspelled parameter is a 400, not ignored.
3564pub(crate) async fn v2_refuse_post_query(req: Request, next: axum::middleware::Next) -> Response {
3565    if req.method() == axum::http::Method::POST && req.uri().query().is_some() {
3566        return tonic_rest::RestError::from(tonic::Status::invalid_argument(
3567            "a POST takes its request as a JSON body, not query parameters",
3568        ))
3569        .into_response();
3570    }
3571    next.run(req).await
3572}
3573
3574/// The v2 OpenAPI document, generated by the build script from the protos.
3575pub(crate) const V2_OPENAPI: &str =
3576    include_str!(concat!(env!("OUT_DIR"), "/espresso.api.v2.openapi.json"));
3577
3578/// Serve the v2 API documentation: the build-time OpenAPI document and the two UIs that render
3579/// it. Unlike [`finish_v1_docs`], nothing here inspects the router, so a route that is generated
3580/// but never mounted would still appear in the document; `v2_documented_routes_are_mounted`
3581/// asserts that never ships.
3582pub fn router_v2_docs() -> Router {
3583    Router::new()
3584        .route(
3585            routes::v2::OPENAPI_SPEC_ROUTE,
3586            get(|| async { ([(header::CONTENT_TYPE, "application/json")], V2_OPENAPI) }),
3587        )
3588        .route(
3589            routes::v2::SWAGGER_ROUTE,
3590            get(|| async { swagger_html(routes::v2::OPENAPI_SPEC_ROUTE) }),
3591        )
3592        .route(
3593            routes::v2::SWAGGER_SLASH_ROUTE,
3594            get(|| async { swagger_html(routes::v2::OPENAPI_SPEC_ROUTE) }),
3595        )
3596        .route(
3597            routes::v2::SCALAR_ROUTE,
3598            get(Scalar::new(routes::v2::OPENAPI_SPEC_ROUTE)
3599                .with_title("Espresso Node API v2")
3600                .axum_handler()),
3601        )
3602}
3603
3604/// A gated module's v2 paths when its flag is off. `serve_axum` merges [`router_v2_docs`] last and
3605/// axum keeps the last merged router's fallback, so an unregistered v2 path would get axum's empty
3606/// 404 instead of the [`v2_error_envelope`] one.
3607pub(crate) fn router_module_disabled(module: &'static str, paths: &[&str]) -> Router {
3608    let mut router = Router::new();
3609    for path in paths {
3610        router = router.route(
3611            path,
3612            any(move || async move {
3613                tonic_rest::RestError::from(tonic::Status::not_found(format!(
3614                    "{module} module disabled"
3615                )))
3616            }),
3617        );
3618    }
3619    router
3620}
3621
3622/// Build the OpenAPI spec for the mounted routes and attach the docs routes; every serve mode
3623/// must route through this.
3624pub fn finish_v1_docs(router: ApiRouter) -> Router {
3625    let mut api = OpenApi {
3626        info: Info {
3627            title: "Espresso Node API v1".to_string(),
3628            description: None,
3629            version: "1.0.0".to_string(),
3630            ..Default::default()
3631        },
3632        ..Default::default()
3633    };
3634
3635    let router = router.finish_api(&mut api);
3636
3637    declare_path_template_parameters(&mut api);
3638    tag_operations_by_module(&mut api);
3639
3640    // Transform examples (array) to example (singular) for OpenAPI 3.0/Swagger compatibility
3641    // (a no-op unless a future v1 route adds a JsonSchema body/query).
3642    if let Some(ref mut components) = api.components {
3643        let mut transform = schemars::transform::SetSingleExample::default();
3644        for schema in components.schemas.values_mut() {
3645            transform.transform(&mut schema.json_schema);
3646        }
3647    }
3648
3649    // Not an Extension layer: Router::layer clones it into every route.
3650    let spec = Bytes::from(serde_json::to_vec(&api).expect("OpenAPI document serializes"));
3651
3652    router
3653        .route(
3654            routes::v1::OPENAPI_SPEC_ROUTE,
3655            get(move || async move { ([(header::CONTENT_TYPE, "application/json")], spec) }),
3656        )
3657        .route(
3658            routes::v1::SWAGGER_ROUTE,
3659            get(|| async { swagger_html(routes::v1::OPENAPI_SPEC_ROUTE) }),
3660        )
3661        .route(
3662            "/v1/",
3663            get(|| async { swagger_html(routes::v1::OPENAPI_SPEC_ROUTE) }),
3664        )
3665        .route(
3666            routes::v1::SCALAR_ROUTE,
3667            get(Scalar::new(routes::v1::OPENAPI_SPEC_ROUTE)
3668                .with_title("Espresso Node API v1")
3669                .axum_handler()),
3670        )
3671}
3672
3673/// Declare a path parameter for every `{name}` template segment of every operation.
3674///
3675/// aide only derives path parameters from `Path<T>` extractors whose `T` is a named-field
3676/// struct; the v1 handlers all use primitives and tuples (`Path<u64>`, `Path<(u64, String)>`),
3677/// so nothing is derived and Swagger's try-it-out cannot fill the URL templates. The template
3678/// itself names every parameter, so declare them from it.
3679///
3680/// Parameter types come from [`path_parameter_schema`]; the handlers parse the raw segment
3681/// either way, so a wrong entry there affects only documentation, not behavior.
3682/// Tag each operation with its module (first path segment after `/v1/`) so Swagger groups them.
3683fn tag_operations_by_module(api: &mut OpenApi) {
3684    let Some(ref mut paths) = api.paths else {
3685        return;
3686    };
3687    let mut modules = std::collections::BTreeSet::new();
3688    for (path, path_item_ref) in paths.paths.iter_mut() {
3689        let ReferenceOr::Item(path_item) = path_item_ref else {
3690            continue;
3691        };
3692        let Some(module) = path
3693            .strip_prefix("/v1/")
3694            .and_then(|rest| rest.split('/').next())
3695        else {
3696            continue;
3697        };
3698        modules.insert(module.to_string());
3699        for operation in [
3700            &mut path_item.get,
3701            &mut path_item.post,
3702            &mut path_item.put,
3703            &mut path_item.delete,
3704            &mut path_item.patch,
3705        ]
3706        .into_iter()
3707        .flatten()
3708        {
3709            operation.tags = vec![module.to_string()];
3710        }
3711    }
3712    api.tags = modules
3713        .into_iter()
3714        .map(|name| aide::openapi::Tag {
3715            name,
3716            ..Default::default()
3717        })
3718        .collect();
3719}
3720
3721/// Types read off the handlers' `Path<T>` extractors; unknown names are strings.
3722fn path_parameter_schema(name: &str) -> schemars::Schema {
3723    match name {
3724        "height" | "block_number" | "from" | "until" | "to" | "start" | "end" | "epoch"
3725        | "epoch_number" | "view" | "index" | "limit" | "offset" | "namespace" | "finalized" => {
3726            schemars::json_schema!({"type": "integer", "minimum": 0})
3727        },
3728        _ => schemars::json_schema!({"type": "string"}),
3729    }
3730}
3731
3732fn declare_path_template_parameters(api: &mut OpenApi) {
3733    let Some(ref mut paths) = api.paths else {
3734        return;
3735    };
3736    for (path, path_item_ref) in paths.paths.iter_mut() {
3737        let ReferenceOr::Item(path_item) = path_item_ref else {
3738            continue;
3739        };
3740        let names: Vec<&str> = path
3741            .split('/')
3742            .filter_map(|seg| seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
3743            .collect();
3744        if names.is_empty() {
3745            continue;
3746        }
3747        for operation in [
3748            &mut path_item.get,
3749            &mut path_item.post,
3750            &mut path_item.put,
3751            &mut path_item.delete,
3752            &mut path_item.patch,
3753        ]
3754        .into_iter()
3755        .flatten()
3756        {
3757            for name in &names {
3758                let already_declared = operation.parameters.iter().any(|p| {
3759                    matches!(
3760                        p,
3761                        ReferenceOr::Item(Parameter::Path {
3762                            parameter_data,
3763                            ..
3764                        }) if parameter_data.name == *name
3765                    )
3766                });
3767                if already_declared {
3768                    continue;
3769                }
3770                operation
3771                    .parameters
3772                    .push(ReferenceOr::Item(Parameter::Path {
3773                        parameter_data: ParameterData {
3774                            name: (*name).to_string(),
3775                            description: None,
3776                            required: true,
3777                            deprecated: None,
3778                            format: ParameterSchemaOrContent::Schema(SchemaObject {
3779                                json_schema: path_parameter_schema(name),
3780                                external_docs: None,
3781                                example: None,
3782                            }),
3783                            example: None,
3784                            examples: Default::default(),
3785                            explode: None,
3786                            extensions: Default::default(),
3787                        },
3788                        style: PathStyle::Simple,
3789                    }));
3790            }
3791        }
3792    }
3793}
3794
3795#[cfg(test)]
3796mod tests {
3797    use std::collections::BTreeMap;
3798
3799    use espresso_types::SeqTypes;
3800    use futures::stream::BoxStream;
3801    use http_wire::WireVersion;
3802    use tokio::io::{AsyncReadExt, AsyncWriteExt};
3803    use vbs::{BinarySerializer, Serializer};
3804
3805    use super::*;
3806
3807    fn rewritten_uri(uri: &str) -> String {
3808        let req = Request::builder()
3809            .uri(uri)
3810            .body(axum::body::Body::empty())
3811            .unwrap();
3812        rewrite_legacy_uri(req).uri().to_string()
3813    }
3814
3815    #[test]
3816    fn rewrite_legacy_uri_prefixes_unversioned_paths() {
3817        assert_eq!(
3818            rewritten_uri("/status/block-height"),
3819            "/v1/status/block-height"
3820        );
3821    }
3822
3823    #[test]
3824    fn rewrite_legacy_uri_rewrites_v0_to_v1() {
3825        assert_eq!(
3826            rewritten_uri("/v0/status/block-height"),
3827            "/v1/status/block-height"
3828        );
3829        assert_eq!(rewritten_uri("/v0"), "/v1");
3830    }
3831
3832    #[test]
3833    fn rewrite_legacy_uri_rewrites_v0_availability_paths() {
3834        assert_eq!(
3835            rewritten_uri("/v0/availability/block/1/namespace/2"),
3836            "/v1/availability/block/1/namespace/2"
3837        );
3838        assert_eq!(
3839            rewritten_uri("/v0/availability/leaf/1"),
3840            "/v1/availability/leaf/1"
3841        );
3842        assert_eq!(
3843            rewritten_uri("/v0/availability/vid/common/1"),
3844            "/v1/availability/vid/common/1"
3845        );
3846        assert_eq!(
3847            rewritten_uri("/v0/availability/stream/leaves/0"),
3848            "/v1/availability/stream/leaves/0"
3849        );
3850        assert_eq!(
3851            rewritten_uri("/availability/block/1/namespace/2"),
3852            "/v1/availability/block/1/namespace/2"
3853        );
3854        assert_eq!(
3855            rewritten_uri("/availability/leaf/1"),
3856            "/v1/availability/leaf/1"
3857        );
3858    }
3859
3860    #[test]
3861    fn rewrite_legacy_uri_leaves_v1_unchanged() {
3862        assert_eq!(
3863            rewritten_uri("/v1/node/block-height"),
3864            "/v1/node/block-height"
3865        );
3866    }
3867
3868    #[test]
3869    fn rewrite_legacy_uri_leaves_v2_unchanged() {
3870        assert_eq!(
3871            rewritten_uri("/v2/status/block-height"),
3872            "/v2/status/block-height"
3873        );
3874    }
3875
3876    #[test]
3877    fn rewrite_legacy_uri_respects_version_prefix_boundaries() {
3878        assert_eq!(rewritten_uri("/v1"), "/v1");
3879        assert_eq!(rewritten_uri("/v2"), "/v2");
3880        assert_eq!(rewritten_uri("/v1x"), "/v1/v1x");
3881        assert_eq!(rewritten_uri("/v2-foo/bar"), "/v1/v2-foo/bar");
3882        assert_eq!(rewritten_uri("/v0x/leaf"), "/v1/v0x/leaf");
3883    }
3884
3885    #[test]
3886    fn rewrite_legacy_uri_leaves_reserved_paths_unchanged() {
3887        assert_eq!(rewritten_uri("/"), "/");
3888        assert_eq!(rewritten_uri("/healthcheck"), "/healthcheck");
3889        assert_eq!(rewritten_uri("/version"), "/version");
3890    }
3891
3892    #[test]
3893    fn rewrite_legacy_uri_maps_versioned_healthchecks() {
3894        assert_eq!(rewritten_uri("/v1/healthcheck"), "/v1/healthcheck");
3895        assert_eq!(rewritten_uri("/v2/healthcheck"), "/v2/healthcheck");
3896        assert_eq!(rewritten_uri("/v0/healthcheck"), "/v1/healthcheck");
3897    }
3898
3899    #[test]
3900    fn rewrite_legacy_uri_preserves_query_string() {
3901        assert_eq!(
3902            rewritten_uri("/availability/leaf/1?foo=bar"),
3903            "/v1/availability/leaf/1?foo=bar"
3904        );
3905    }
3906
3907    /// Error bodies go to unauthenticated callers, so an L1 provider URL in the message must not
3908    /// reach them.
3909    #[test]
3910    fn error_response_redacts_provider_credentials() {
3911        let msg = r#"failed to get total supply: reqwest::Error { url: "https://u:p@rpc.invalid/v1/FAKEKEY" }"#;
3912
3913        let body = ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, msg.to_string());
3914
3915        assert!(!body.custom.message.contains("FAKEKEY"), "{body:?}");
3916        assert!(!body.custom.message.contains("u:p"), "{body:?}");
3917        assert!(body.custom.message.contains("rpc.invalid"), "{body:?}");
3918    }
3919
3920    /// Implements every v1 API trait with `unimplemented!()` bodies, purely so `create_router_v1`
3921    /// can be instantiated in tests that only exercise the static docs routes (root redirect,
3922    /// swagger UI, OpenAPI spec) and never call into a handler. The one exception is `keys`,
3923    /// which answers like a node without validator keys.
3924    #[derive(Clone)]
3925    struct MockState;
3926
3927    #[async_trait::async_trait]
3928    impl v1::RewardApi for MockState {
3929        async fn get_reward_state_height(&self) -> anyhow::Result<u64> {
3930            unimplemented!()
3931        }
3932        async fn get_reward_state_v2_height(&self) -> anyhow::Result<u64> {
3933            unimplemented!()
3934        }
3935        async fn get_reward_account_proof_v1(
3936            &self,
3937            _height: u64,
3938            _address: String,
3939        ) -> anyhow::Result<espresso_types::v0_3::RewardAccountQueryDataV1> {
3940            unimplemented!()
3941        }
3942        async fn get_reward_claim_input(
3943            &self,
3944            _block_height: u64,
3945            _address: String,
3946        ) -> anyhow::Result<hotshot_contract_adapter::reward::RewardClaimInput> {
3947            unimplemented!()
3948        }
3949        async fn get_reward_balance(
3950            &self,
3951            _height: u64,
3952            _address: String,
3953        ) -> anyhow::Result<espresso_types::v0_3::RewardAmount> {
3954            unimplemented!()
3955        }
3956        async fn get_latest_reward_balance(
3957            &self,
3958            _address: String,
3959        ) -> anyhow::Result<espresso_types::v0_3::RewardAmount> {
3960            unimplemented!()
3961        }
3962        async fn get_reward_account_proof(
3963            &self,
3964            _height: u64,
3965            _address: String,
3966        ) -> anyhow::Result<espresso_types::v0_4::RewardAccountQueryDataV2> {
3967            unimplemented!()
3968        }
3969        async fn get_latest_reward_account_proof(
3970            &self,
3971            _address: String,
3972        ) -> anyhow::Result<espresso_types::v0_4::RewardAccountQueryDataV2> {
3973            unimplemented!()
3974        }
3975        async fn get_reward_amounts(
3976            &self,
3977            _height: u64,
3978            _offset: u64,
3979            _limit: u64,
3980        ) -> anyhow::Result<
3981            Vec<(
3982                alloy::primitives::Address,
3983                espresso_types::v0_3::RewardAmount,
3984            )>,
3985        > {
3986            unimplemented!()
3987        }
3988        async fn get_reward_merkle_tree_v2(&self, _height: u64) -> anyhow::Result<Vec<u8>> {
3989            unimplemented!()
3990        }
3991        async fn get_reward_state_path_v1(
3992            &self,
3993            _snapshot: v1::merklized_state::Snapshot,
3994            _key: String,
3995        ) -> anyhow::Result<v1::reward_state_v2::RewardStatePathV1> {
3996            unimplemented!()
3997        }
3998        async fn get_reward_state_path_v2(
3999            &self,
4000            _snapshot: v1::merklized_state::Snapshot,
4001            _key: String,
4002        ) -> anyhow::Result<v1::reward_state_v2::RewardStatePathV2> {
4003            unimplemented!()
4004        }
4005    }
4006
4007    #[async_trait::async_trait]
4008    impl v1::AvailabilityApi for MockState {
4009        async fn get_namespace_proof(
4010            &self,
4011            _block_id: v1::BlockId,
4012            _namespace: u32,
4013        ) -> anyhow::Result<espresso_types::NamespaceProofQueryData> {
4014            unimplemented!()
4015        }
4016        async fn get_namespace_proof_range(
4017            &self,
4018            _from: u64,
4019            _until: u64,
4020            _namespace: u32,
4021        ) -> anyhow::Result<Vec<espresso_types::NamespaceProofQueryData>> {
4022            unimplemented!()
4023        }
4024        async fn stream_namespace_proofs(
4025            &self,
4026            _from: usize,
4027            _namespace: u32,
4028        ) -> anyhow::Result<BoxStream<'static, espresso_types::NamespaceProofQueryData>> {
4029            unimplemented!()
4030        }
4031        async fn get_incorrect_encoding_proof(
4032            &self,
4033            _block_id: v1::BlockId,
4034            _namespace: u32,
4035        ) -> anyhow::Result<espresso_types::v0_3::AvidMIncorrectEncodingNsProof> {
4036            unimplemented!()
4037        }
4038        async fn get_state_cert(
4039            &self,
4040            _epoch: u64,
4041        ) -> anyhow::Result<espresso_types::StateCertQueryDataV1<SeqTypes>> {
4042            unimplemented!()
4043        }
4044        async fn get_state_cert_v2(
4045            &self,
4046            _epoch: u64,
4047        ) -> anyhow::Result<espresso_types::StateCertQueryDataV2<SeqTypes>> {
4048            unimplemented!()
4049        }
4050    }
4051
4052    #[async_trait::async_trait]
4053    impl v1::HotShotAvailabilityApi for MockState {
4054        async fn get_leaf(
4055            &self,
4056            _id: v1::LeafId,
4057        ) -> anyhow::Result<hotshot_query_service_types::availability::LeafQueryData<SeqTypes>>
4058        {
4059            unimplemented!()
4060        }
4061        async fn get_leaf_range(
4062            &self,
4063            _from: usize,
4064            _until: usize,
4065        ) -> anyhow::Result<Vec<hotshot_query_service_types::availability::LeafQueryData<SeqTypes>>>
4066        {
4067            unimplemented!()
4068        }
4069        async fn get_header(
4070            &self,
4071            _id: v1::BlockId,
4072        ) -> anyhow::Result<hotshot_query_service_types::Header<SeqTypes>> {
4073            unimplemented!()
4074        }
4075        async fn get_header_range(
4076            &self,
4077            _from: usize,
4078            _until: usize,
4079        ) -> anyhow::Result<Vec<hotshot_query_service_types::Header<SeqTypes>>> {
4080            unimplemented!()
4081        }
4082        async fn get_block(
4083            &self,
4084            _id: v1::BlockId,
4085        ) -> anyhow::Result<hotshot_query_service_types::availability::BlockQueryData<SeqTypes>>
4086        {
4087            unimplemented!()
4088        }
4089        async fn get_block_range(
4090            &self,
4091            _from: usize,
4092            _until: usize,
4093        ) -> anyhow::Result<Vec<hotshot_query_service_types::availability::BlockQueryData<SeqTypes>>>
4094        {
4095            unimplemented!()
4096        }
4097        async fn get_payload(
4098            &self,
4099            _id: v1::PayloadId,
4100        ) -> anyhow::Result<hotshot_query_service_types::availability::PayloadQueryData<SeqTypes>>
4101        {
4102            unimplemented!()
4103        }
4104        async fn get_payload_range(
4105            &self,
4106            _from: usize,
4107            _until: usize,
4108        ) -> anyhow::Result<
4109            Vec<hotshot_query_service_types::availability::PayloadQueryData<SeqTypes>>,
4110        > {
4111            unimplemented!()
4112        }
4113        async fn get_vid_common(
4114            &self,
4115            _id: v1::BlockId,
4116        ) -> anyhow::Result<hotshot_query_service_types::availability::VidCommonQueryData<SeqTypes>>
4117        {
4118            unimplemented!()
4119        }
4120        async fn get_vid_common_range(
4121            &self,
4122            _from: usize,
4123            _until: usize,
4124        ) -> anyhow::Result<
4125            Vec<hotshot_query_service_types::availability::VidCommonQueryData<SeqTypes>>,
4126        > {
4127            unimplemented!()
4128        }
4129        async fn get_leaf_ranges(
4130            &self,
4131            _ranges: Vec<Range<u64>>,
4132        ) -> anyhow::Result<Vec<hotshot_query_service_types::availability::LeafQueryData<SeqTypes>>>
4133        {
4134            unimplemented!()
4135        }
4136        async fn get_block_ranges(
4137            &self,
4138            _ranges: Vec<Range<u64>>,
4139        ) -> anyhow::Result<Vec<hotshot_query_service_types::availability::BlockQueryData<SeqTypes>>>
4140        {
4141            unimplemented!()
4142        }
4143        async fn get_vid_common_ranges(
4144            &self,
4145            _ranges: Vec<Range<u64>>,
4146        ) -> anyhow::Result<
4147            Vec<hotshot_query_service_types::availability::VidCommonQueryData<SeqTypes>>,
4148        > {
4149            unimplemented!()
4150        }
4151        async fn get_transaction_by_position(
4152            &self,
4153            _height: u64,
4154            _index: u64,
4155        ) -> anyhow::Result<hotshot_query_service_types::availability::TransactionQueryData<SeqTypes>>
4156        {
4157            unimplemented!()
4158        }
4159        async fn get_transaction_by_hash(
4160            &self,
4161            _hash: String,
4162        ) -> anyhow::Result<hotshot_query_service_types::availability::TransactionQueryData<SeqTypes>>
4163        {
4164            unimplemented!()
4165        }
4166        async fn get_transaction_proof_by_position(
4167            &self,
4168            _height: u64,
4169            _index: u64,
4170        ) -> anyhow::Result<
4171            hotshot_query_service_types::availability::TransactionWithProofQueryData<SeqTypes>,
4172        > {
4173            unimplemented!()
4174        }
4175        async fn get_transaction_proof_by_hash(
4176            &self,
4177            _hash: String,
4178        ) -> anyhow::Result<
4179            hotshot_query_service_types::availability::TransactionWithProofQueryData<SeqTypes>,
4180        > {
4181            unimplemented!()
4182        }
4183        async fn get_block_summary(
4184            &self,
4185            _height: usize,
4186        ) -> anyhow::Result<
4187            hotshot_query_service_types::availability::BlockSummaryQueryData<SeqTypes>,
4188        > {
4189            unimplemented!()
4190        }
4191        async fn get_block_summary_range(
4192            &self,
4193            _from: usize,
4194            _until: usize,
4195        ) -> anyhow::Result<
4196            Vec<hotshot_query_service_types::availability::BlockSummaryQueryData<SeqTypes>>,
4197        > {
4198            unimplemented!()
4199        }
4200        async fn get_limits(
4201            &self,
4202        ) -> anyhow::Result<hotshot_query_service_types::availability::Limits> {
4203            unimplemented!()
4204        }
4205        async fn get_cert2(
4206            &self,
4207            _height: u64,
4208        ) -> anyhow::Result<Option<hotshot_types::simple_certificate::Certificate2<SeqTypes>>>
4209        {
4210            unimplemented!()
4211        }
4212        async fn stream_leaves(
4213            &self,
4214            _from: usize,
4215        ) -> anyhow::Result<
4216            BoxStream<'static, hotshot_query_service_types::availability::LeafQueryData<SeqTypes>>,
4217        > {
4218            unimplemented!()
4219        }
4220        async fn stream_headers(
4221            &self,
4222            _from: usize,
4223        ) -> anyhow::Result<BoxStream<'static, hotshot_query_service_types::Header<SeqTypes>>>
4224        {
4225            unimplemented!()
4226        }
4227        async fn stream_blocks(
4228            &self,
4229            _from: usize,
4230        ) -> anyhow::Result<
4231            BoxStream<'static, hotshot_query_service_types::availability::BlockQueryData<SeqTypes>>,
4232        > {
4233            unimplemented!()
4234        }
4235        async fn stream_payloads(
4236            &self,
4237            _from: usize,
4238        ) -> anyhow::Result<
4239            BoxStream<
4240                'static,
4241                hotshot_query_service_types::availability::PayloadQueryData<SeqTypes>,
4242            >,
4243        > {
4244            unimplemented!()
4245        }
4246        async fn stream_vid_common(
4247            &self,
4248            _from: usize,
4249        ) -> anyhow::Result<
4250            BoxStream<
4251                'static,
4252                hotshot_query_service_types::availability::VidCommonQueryData<SeqTypes>,
4253            >,
4254        > {
4255            unimplemented!()
4256        }
4257        async fn stream_transactions(
4258            &self,
4259            _from: usize,
4260            _namespace: Option<u32>,
4261        ) -> anyhow::Result<
4262            BoxStream<
4263                'static,
4264                hotshot_query_service_types::availability::TransactionQueryData<SeqTypes>,
4265            >,
4266        > {
4267            unimplemented!()
4268        }
4269    }
4270
4271    #[async_trait::async_trait]
4272    impl v1::BlockStateApi for MockState {
4273        async fn get_block_state_path(
4274            &self,
4275            _snapshot: v1::merklized_state::Snapshot,
4276            _key: String,
4277        ) -> anyhow::Result<v1::merklized_state::BlockStatePath> {
4278            unimplemented!()
4279        }
4280        async fn get_block_state_height(&self) -> anyhow::Result<u64> {
4281            unimplemented!()
4282        }
4283    }
4284
4285    #[async_trait::async_trait]
4286    impl v1::FeeStateApi for MockState {
4287        async fn get_fee_state_path(
4288            &self,
4289            _snapshot: v1::merklized_state::Snapshot,
4290            _key: String,
4291        ) -> anyhow::Result<v1::merklized_state::FeeStatePath> {
4292            unimplemented!()
4293        }
4294        async fn get_fee_state_height(&self) -> anyhow::Result<u64> {
4295            unimplemented!()
4296        }
4297        async fn get_fee_balance_latest(
4298            &self,
4299            _address: String,
4300        ) -> anyhow::Result<Option<espresso_types::FeeAmount>> {
4301            unimplemented!()
4302        }
4303    }
4304
4305    #[async_trait::async_trait]
4306    impl v1::StatusApi for MockState {
4307        async fn block_height(&self) -> anyhow::Result<u64> {
4308            unimplemented!()
4309        }
4310        async fn success_rate(&self) -> anyhow::Result<f64> {
4311            unimplemented!()
4312        }
4313        async fn time_since_last_decide(&self) -> anyhow::Result<u64> {
4314            unimplemented!()
4315        }
4316        async fn metrics(&self) -> anyhow::Result<String> {
4317            unimplemented!()
4318        }
4319        async fn keys(&self) -> anyhow::Result<v1::status::NodePublicKeys> {
4320            Err(
4321                crate::error::AvailabilityError::NotFound("this node has no validator keys".into())
4322                    .into(),
4323            )
4324        }
4325    }
4326
4327    #[async_trait::async_trait]
4328    impl v1::ConfigApi for MockState {
4329        async fn hotshot_config(
4330            &self,
4331        ) -> anyhow::Result<espresso_types::config::PublicNetworkConfig> {
4332            unimplemented!()
4333        }
4334        async fn env(&self) -> anyhow::Result<Vec<String>> {
4335            unimplemented!()
4336        }
4337        async fn runtime_config(&self) -> anyhow::Result<v1::config::RuntimeConfig> {
4338            unimplemented!()
4339        }
4340    }
4341
4342    #[async_trait::async_trait]
4343    impl v1::NodeApi for MockState {
4344        async fn block_height(&self) -> anyhow::Result<u64> {
4345            unimplemented!()
4346        }
4347        async fn count_transactions(
4348            &self,
4349            _from: Option<u64>,
4350            _to: Option<u64>,
4351            _namespace: Option<u64>,
4352        ) -> anyhow::Result<u64> {
4353            unimplemented!()
4354        }
4355        async fn payload_size(
4356            &self,
4357            _from: Option<u64>,
4358            _to: Option<u64>,
4359            _namespace: Option<u64>,
4360        ) -> anyhow::Result<u64> {
4361            unimplemented!()
4362        }
4363        async fn get_vid_share(
4364            &self,
4365            _id: v1::VidShareId,
4366        ) -> anyhow::Result<hotshot_types::data::VidShare> {
4367            unimplemented!()
4368        }
4369        async fn sync_status(
4370            &self,
4371        ) -> anyhow::Result<hotshot_query_service_types::node::SyncStatusQueryData> {
4372            unimplemented!()
4373        }
4374        async fn get_header_window(
4375            &self,
4376            _start: v1::HeaderWindowStart,
4377            _end: u64,
4378        ) -> anyhow::Result<
4379            hotshot_query_service_types::node::TimeWindowQueryData<
4380                hotshot_query_service_types::Header<SeqTypes>,
4381            >,
4382        > {
4383            unimplemented!()
4384        }
4385        async fn limits(&self) -> anyhow::Result<hotshot_query_service_types::node::Limits> {
4386            unimplemented!()
4387        }
4388        async fn stake_table(
4389            &self,
4390            _epoch: u64,
4391        ) -> anyhow::Result<Vec<hotshot_types::PeerConfig<SeqTypes>>> {
4392            unimplemented!()
4393        }
4394        async fn stake_table_current(
4395            &self,
4396        ) -> anyhow::Result<v1::node::StakeTableWithEpochNumber<SeqTypes>> {
4397            unimplemented!()
4398        }
4399        async fn da_stake_table(
4400            &self,
4401            _epoch: u64,
4402        ) -> anyhow::Result<Vec<hotshot_types::PeerConfig<SeqTypes>>> {
4403            unimplemented!()
4404        }
4405        async fn da_stake_table_current(
4406            &self,
4407        ) -> anyhow::Result<v1::node::StakeTableWithEpochNumber<SeqTypes>> {
4408            unimplemented!()
4409        }
4410        async fn get_validators(
4411            &self,
4412            _epoch: u64,
4413        ) -> anyhow::Result<
4414            indexmap::IndexMap<
4415                alloy::primitives::Address,
4416                espresso_types::v0_3::AuthenticatedValidator<espresso_types::PubKey>,
4417            >,
4418        > {
4419            unimplemented!()
4420        }
4421        async fn get_all_validators(
4422            &self,
4423            _epoch: u64,
4424            _offset: u64,
4425            _limit: u64,
4426        ) -> anyhow::Result<Vec<espresso_types::v0_3::RegisteredValidator<espresso_types::PubKey>>>
4427        {
4428            unimplemented!()
4429        }
4430        async fn current_proposal_participation(
4431            &self,
4432        ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
4433            unimplemented!()
4434        }
4435        async fn proposal_participation(
4436            &self,
4437            _epoch: u64,
4438        ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
4439            unimplemented!()
4440        }
4441        async fn current_vote_participation(
4442            &self,
4443        ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
4444            unimplemented!()
4445        }
4446        async fn vote_participation(
4447            &self,
4448            _epoch: u64,
4449        ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
4450            unimplemented!()
4451        }
4452        async fn get_block_reward(
4453            &self,
4454            _epoch: Option<u64>,
4455        ) -> anyhow::Result<Option<espresso_types::v0_3::RewardAmount>> {
4456            unimplemented!()
4457        }
4458        async fn get_oldest_block(
4459            &self,
4460        ) -> anyhow::Result<
4461            Option<hotshot_query_service_types::availability::BlockQueryData<SeqTypes>>,
4462        > {
4463            unimplemented!()
4464        }
4465        async fn get_oldest_leaf(
4466            &self,
4467        ) -> anyhow::Result<
4468            Option<hotshot_query_service_types::availability::LeafQueryData<SeqTypes>>,
4469        > {
4470            unimplemented!()
4471        }
4472    }
4473
4474    #[async_trait::async_trait]
4475    impl v1::CatchupApi for MockState {
4476        async fn get_account(
4477            &self,
4478            _height: u64,
4479            _view: u64,
4480            _address: String,
4481        ) -> anyhow::Result<espresso_types::AccountQueryData> {
4482            unimplemented!()
4483        }
4484        async fn get_accounts(
4485            &self,
4486            _height: u64,
4487            _view: u64,
4488            _accounts: Vec<espresso_types::FeeAccount>,
4489        ) -> anyhow::Result<espresso_types::FeeMerkleTree> {
4490            unimplemented!()
4491        }
4492        async fn get_blocks_frontier(
4493            &self,
4494            _height: u64,
4495            _view: u64,
4496        ) -> anyhow::Result<<espresso_types::BlockMerkleTree as jf_merkle_tree_compat::MerkleTreeScheme>::MembershipProof>{
4497            unimplemented!()
4498        }
4499        async fn get_chain_config(
4500            &self,
4501            _commitment: String,
4502        ) -> anyhow::Result<espresso_types::v0_3::ChainConfig> {
4503            unimplemented!()
4504        }
4505        async fn get_leaf_chain(&self, _height: u64) -> anyhow::Result<Vec<espresso_types::Leaf2>> {
4506            unimplemented!()
4507        }
4508        async fn get_cert2(
4509            &self,
4510            _height: u64,
4511        ) -> anyhow::Result<hotshot_types::simple_certificate::Certificate2<SeqTypes>> {
4512            unimplemented!()
4513        }
4514        async fn get_reward_account_v1(
4515            &self,
4516            _height: u64,
4517            _view: u64,
4518            _address: String,
4519        ) -> anyhow::Result<espresso_types::v0_3::RewardAccountQueryDataV1> {
4520            unimplemented!()
4521        }
4522        async fn get_reward_accounts_v1(
4523            &self,
4524            _height: u64,
4525            _view: u64,
4526            _accounts: Vec<espresso_types::v0_3::RewardAccountV1>,
4527        ) -> anyhow::Result<espresso_types::v0_3::RewardMerkleTreeV1> {
4528            unimplemented!()
4529        }
4530        async fn get_reward_account_v2(
4531            &self,
4532            _height: u64,
4533            _view: u64,
4534            _address: String,
4535        ) -> anyhow::Result<espresso_types::v0_4::RewardAccountQueryDataV2> {
4536            unimplemented!()
4537        }
4538        async fn get_reward_merkle_tree_v2(
4539            &self,
4540            _height: u64,
4541            _view: u64,
4542        ) -> anyhow::Result<serde_json::Value> {
4543            unimplemented!()
4544        }
4545        async fn get_state_cert(
4546            &self,
4547            _epoch: u64,
4548        ) -> anyhow::Result<
4549            hotshot_types::simple_certificate::LightClientStateUpdateCertificateV2<SeqTypes>,
4550        > {
4551            unimplemented!()
4552        }
4553    }
4554
4555    #[async_trait::async_trait]
4556    impl v1::SubmitApi for MockState {
4557        async fn submit(
4558            &self,
4559            _tx: espresso_types::Transaction,
4560        ) -> anyhow::Result<committable::Commitment<espresso_types::Transaction>> {
4561            unimplemented!()
4562        }
4563    }
4564
4565    #[async_trait::async_trait]
4566    impl v1::StateSignatureApi for MockState {
4567        async fn get_state_signature(
4568            &self,
4569            _height: u64,
4570        ) -> anyhow::Result<hotshot_types::light_client::LCV3StateSignatureRequestBody> {
4571            unimplemented!()
4572        }
4573    }
4574
4575    #[async_trait::async_trait]
4576    impl v1::LightClientApi for MockState {
4577        async fn get_leaf_proof(
4578            &self,
4579            _query: v1::LeafQuery,
4580            _finalized: Option<u64>,
4581        ) -> anyhow::Result<light_client::consensus::leaf::LeafProof> {
4582            unimplemented!()
4583        }
4584        async fn get_header_proof(
4585            &self,
4586            _root: u64,
4587            _requested: v1::HeaderQuery,
4588        ) -> anyhow::Result<light_client::consensus::header::HeaderProof> {
4589            unimplemented!()
4590        }
4591        async fn get_light_client_stake_table(
4592            &self,
4593            _epoch: u64,
4594        ) -> anyhow::Result<Vec<espresso_types::v0_3::StakeTableEvent>> {
4595            unimplemented!()
4596        }
4597        async fn get_payload_proof(
4598            &self,
4599            _height: u64,
4600        ) -> anyhow::Result<light_client::consensus::payload::PayloadProof> {
4601            unimplemented!()
4602        }
4603        async fn get_payload_proof_range(
4604            &self,
4605            _start: u64,
4606            _end: u64,
4607        ) -> anyhow::Result<Vec<light_client::consensus::payload::PayloadProof>> {
4608            unimplemented!()
4609        }
4610        async fn get_payload_proof_ranges(
4611            &self,
4612            _ranges: Vec<Range<u64>>,
4613        ) -> anyhow::Result<Vec<light_client::consensus::payload::PayloadProof>> {
4614            unimplemented!()
4615        }
4616        async fn get_lc_namespace_proof(
4617            &self,
4618            _height: u64,
4619            _namespace: u64,
4620        ) -> anyhow::Result<light_client::consensus::namespace::NamespaceProof> {
4621            unimplemented!()
4622        }
4623        async fn get_lc_namespace_proof_range(
4624            &self,
4625            _start: u64,
4626            _end: u64,
4627            _namespace: u64,
4628        ) -> anyhow::Result<Vec<light_client::consensus::namespace::NamespaceProof>> {
4629            unimplemented!()
4630        }
4631        async fn get_lc_namespaces_proof_range(
4632            &self,
4633            _start: u64,
4634            _end: u64,
4635            _namespaces: String,
4636        ) -> anyhow::Result<
4637            Vec<std::collections::HashMap<u64, light_client::consensus::namespace::NamespaceProof>>,
4638        > {
4639            unimplemented!()
4640        }
4641    }
4642
4643    #[async_trait::async_trait]
4644    impl v1::ExplorerApi for MockState {
4645        async fn get_block_detail(
4646            &self,
4647            _ident: v1::BlockIdent,
4648        ) -> anyhow::Result<
4649            hotshot_query_service_types::explorer::response::BlockDetailResponse<SeqTypes>,
4650        > {
4651            unimplemented!()
4652        }
4653        async fn get_block_summaries(
4654            &self,
4655            _target: v1::BlockIdent,
4656            _limit: u64,
4657        ) -> anyhow::Result<
4658            hotshot_query_service_types::explorer::response::BlockSummaryResponse<SeqTypes>,
4659        > {
4660            unimplemented!()
4661        }
4662        async fn get_transaction_detail(
4663            &self,
4664            _ident: v1::TxIdent,
4665        ) -> anyhow::Result<
4666            hotshot_query_service_types::explorer::response::TransactionDetailResponse<SeqTypes>,
4667        > {
4668            unimplemented!()
4669        }
4670        async fn get_transaction_summaries(
4671            &self,
4672            _target: v1::TxIdent,
4673            _limit: u64,
4674            _filter: v1::TxSummaryFilter,
4675        ) -> anyhow::Result<
4676            hotshot_query_service_types::explorer::response::TransactionSummariesResponse<SeqTypes>,
4677        > {
4678            unimplemented!()
4679        }
4680        async fn get_explorer_summary(
4681            &self,
4682        ) -> anyhow::Result<
4683            hotshot_query_service_types::explorer::response::ExplorerSummaryResponse<SeqTypes>,
4684        > {
4685            unimplemented!()
4686        }
4687        async fn get_search_result(
4688            &self,
4689            _query: String,
4690        ) -> anyhow::Result<
4691            hotshot_query_service_types::explorer::response::SearchResultResponse<SeqTypes>,
4692        > {
4693            unimplemented!()
4694        }
4695    }
4696
4697    #[async_trait::async_trait]
4698    impl v1::TokenApi for MockState {
4699        async fn total_minted_supply(&self) -> anyhow::Result<String> {
4700            unimplemented!()
4701        }
4702        async fn circulating_supply(&self) -> anyhow::Result<String> {
4703            unimplemented!()
4704        }
4705        async fn circulating_supply_ethereum(&self) -> anyhow::Result<String> {
4706            unimplemented!()
4707        }
4708        async fn total_issued_supply(&self) -> anyhow::Result<String> {
4709            unimplemented!()
4710        }
4711        async fn total_reward_distributed(&self) -> anyhow::Result<String> {
4712            unimplemented!()
4713        }
4714    }
4715
4716    #[async_trait::async_trait]
4717    impl v1::DatabaseApi for MockState {
4718        async fn get_table_sizes(&self) -> anyhow::Result<Vec<v1::database::TableSize>> {
4719            unimplemented!()
4720        }
4721        async fn get_migration_status(&self) -> anyhow::Result<Vec<v1::database::MigrationStatus>> {
4722            unimplemented!()
4723        }
4724    }
4725
4726    async fn body_string(resp: Response) -> String {
4727        let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
4728            .await
4729            .expect("read response body");
4730        String::from_utf8(bytes.to_vec()).expect("response body is utf8")
4731    }
4732
4733    /// Checks that every response carries
4734    /// `Access-Control-Allow-Origin: *`: top-level routes, API routes merged in by the caller,
4735    /// error responses, and 404s. Also checks that an OPTIONS preflight is answered with the
4736    /// allow-origin, allow-methods, and allow-headers a browser requires.
4737    #[tokio::test]
4738    async fn responses_carry_cors_headers() {
4739        let router = with_top_level_routes(
4740            Router::new()
4741                .route("/v1/status/block-height", get(|| async { "0" }))
4742                .route(
4743                    "/v1/failing",
4744                    get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
4745                ),
4746        )
4747        .layer(http_wire::cors_layer());
4748
4749        let allow_origin = |resp: &Response, uri: &str| {
4750            resp.headers()
4751                .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
4752                .unwrap_or_else(|| panic!("no CORS header on {uri}"))
4753                .clone()
4754        };
4755
4756        for (uri, expected_status) in [
4757            ("/healthcheck", StatusCode::OK),
4758            ("/v1/status/block-height", StatusCode::OK),
4759            ("/v1/failing", StatusCode::INTERNAL_SERVER_ERROR),
4760            ("/no/such/route", StatusCode::NOT_FOUND),
4761        ] {
4762            let req = Request::builder()
4763                .uri(uri)
4764                .header(header::ORIGIN, "https://example.com")
4765                .body(axum::body::Body::empty())
4766                .unwrap();
4767            let resp = tower::ServiceExt::oneshot(router.clone(), req)
4768                .await
4769                .unwrap();
4770            assert_eq!(resp.status(), expected_status, "{uri}");
4771            assert_eq!(allow_origin(&resp, uri), "*", "{uri}");
4772        }
4773
4774        // Browsers preflight non-simple requests (e.g. a JSON POST to submit) with OPTIONS and
4775        // require allow-origin, allow-methods, and allow-headers in the answer, even on routes
4776        // that only register GET handlers.
4777        let preflight = Request::builder()
4778            .method(axum::http::Method::OPTIONS)
4779            .uri("/v1/status/block-height")
4780            .header(header::ORIGIN, "https://example.com")
4781            .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
4782            .header(header::ACCESS_CONTROL_REQUEST_HEADERS, "content-type")
4783            .body(axum::body::Body::empty())
4784            .unwrap();
4785        let resp = tower::ServiceExt::oneshot(router, preflight).await.unwrap();
4786        assert_eq!(resp.status(), StatusCode::OK);
4787        assert_eq!(allow_origin(&resp, "preflight"), "*");
4788        assert_eq!(
4789            resp.headers()
4790                .get(header::ACCESS_CONTROL_ALLOW_METHODS)
4791                .expect("allow-methods on preflight"),
4792            "*"
4793        );
4794        assert_eq!(
4795            resp.headers()
4796                .get(header::ACCESS_CONTROL_ALLOW_HEADERS)
4797                .expect("allow-headers on preflight"),
4798            "*"
4799        );
4800    }
4801
4802    /// Serves with zero connection slots so every request is shed, and checks the 429 still
4803    /// carries `Access-Control-Allow-Origin: *`.
4804    #[tokio::test]
4805    async fn shed_requests_carry_cors_headers() {
4806        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
4807        let addr = listener.local_addr().unwrap();
4808        tokio::spawn(crate::serve_router(
4809            listener,
4810            "test",
4811            Router::new().route("/v1/status/block-height", get(|| async { "0" })),
4812            Some(0),
4813        ));
4814
4815        let mut sock = tokio::net::TcpStream::connect(addr).await.unwrap();
4816        sock.write_all(
4817            b"GET /v1/status/block-height HTTP/1.1\r\nHost: localhost\r\nOrigin: https://example.com\r\n\r\n",
4818        )
4819        .await
4820        .unwrap();
4821        let mut head = Vec::new();
4822        let mut buf = [0u8; 512];
4823        while !head.windows(4).any(|w| w == b"\r\n\r\n") {
4824            let n = sock.read(&mut buf).await.unwrap();
4825            assert!(
4826                n > 0,
4827                "connection closed before the response head: {head:?}"
4828            );
4829            head.extend_from_slice(&buf[..n]);
4830        }
4831        let head = String::from_utf8_lossy(&head).to_ascii_lowercase();
4832        assert!(head.contains("429"), "request must be shed: {head}");
4833        assert!(
4834            head.contains("access-control-allow-origin: *"),
4835            "no CORS header on 429: {head}"
4836        );
4837    }
4838
4839    #[tokio::test]
4840    async fn root_redirects_to_v1() {
4841        let router = with_top_level_routes(Router::new());
4842        let req = Request::builder()
4843            .uri("/")
4844            .body(axum::body::Body::empty())
4845            .unwrap();
4846        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
4847        assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
4848        assert_eq!(
4849            resp.headers().get(axum::http::header::LOCATION).unwrap(),
4850            "/v1"
4851        );
4852    }
4853
4854    /// The app-level `/healthcheck` reports `tide_disco::app::AppHealth`, which is what every
4855    /// non-singleton tide app served; a module-level one reports the bare `HealthStatus`. Both
4856    /// shapes are load-bearing for clients built against the tide-disco servers.
4857    #[tokio::test]
4858    async fn healthcheck_shapes_match_tide() {
4859        let router = with_top_level_routes(Router::new());
4860
4861        async fn get(router: &Router, uri: &str, accept: &str) -> Vec<u8> {
4862            let req = Request::builder()
4863                .uri(uri)
4864                .header(header::ACCEPT, accept)
4865                .body(axum::body::Body::empty())
4866                .unwrap();
4867            let resp = tower::ServiceExt::oneshot(router.clone(), req)
4868                .await
4869                .unwrap();
4870            assert_eq!(resp.status(), StatusCode::OK, "{uri}");
4871            axum::body::to_bytes(resp.into_body(), usize::MAX)
4872                .await
4873                .unwrap()
4874                .to_vec()
4875        }
4876
4877        assert_eq!(
4878            get(&router, "/healthcheck", "application/json").await,
4879            br#"{"status":"available","modules":{}}"#
4880        );
4881        assert_eq!(
4882            get(&router, "/v1/status/healthcheck", "application/json").await,
4883            br#""available""#
4884        );
4885        for uri in ["/v1/healthcheck", "/v2/healthcheck"] {
4886            assert_eq!(
4887                get(&router, uri, "application/json").await,
4888                br#"{"status":"available","modules":{}}"#,
4889                "{uri} should serve the app health, not the module shape"
4890            );
4891        }
4892
4893        // vbs field order (status ordinal, then modules map) must not change either: surf-disco
4894        // clients default to `Accept: application/octet-stream`.
4895        #[derive(Debug, PartialEq, serde::Deserialize)]
4896        enum TideHealthStatus {
4897            Available,
4898        }
4899        #[derive(Debug, PartialEq, serde::Deserialize)]
4900        struct TideAppHealth {
4901            status: TideHealthStatus,
4902            modules: BTreeMap<String, BTreeMap<u64, u16>>,
4903        }
4904        let binary = get(&router, "/healthcheck", "application/octet-stream").await;
4905        // `BuilderClient::connect` and the events-service wrapper poll this route and decode the
4906        // body as a bare `HealthStatus`. That only works because bincode allows trailing bytes and
4907        // `AppHealth`'s first field is the status ordinal, so the bare enum is a prefix of the
4908        // object. Reordering `AppHealth`'s fields would break both clients silently.
4909        assert_eq!(
4910            Serializer::<WireVersion>::deserialize::<TideHealthStatus>(&binary).unwrap(),
4911            TideHealthStatus::Available
4912        );
4913        assert_eq!(
4914            Serializer::<WireVersion>::deserialize::<TideAppHealth>(&binary).unwrap(),
4915            TideAppHealth {
4916                status: TideHealthStatus::Available,
4917                modules: BTreeMap::new(),
4918            }
4919        );
4920    }
4921
4922    /// Adding `/v1/healthcheck` on top of the real v1 route table would panic if it collided with
4923    /// a declared route, so this also guards against a future single-segment `/v1/{param}`.
4924    #[tokio::test]
4925    async fn versioned_healthcheck_coexists_with_v1_routes() {
4926        let router = with_top_level_routes(create_router_v1(MockState));
4927        let req = Request::builder()
4928            .uri("/v1/healthcheck")
4929            .header(header::ACCEPT, "application/json")
4930            .body(axum::body::Body::empty())
4931            .unwrap();
4932        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
4933        assert_eq!(resp.status(), StatusCode::OK);
4934        assert_eq!(
4935            body_string(resp).await,
4936            r#"{"status":"available","modules":{}}"#
4937        );
4938    }
4939
4940    #[tokio::test]
4941    async fn status_keys_without_validator_keys_is_not_found() {
4942        let router = create_router_v1(MockState);
4943        let req = Request::builder()
4944            .uri(routes::v1::STATUS_KEYS_ROUTE)
4945            .body(axum::body::Body::empty())
4946            .unwrap();
4947        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
4948        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
4949    }
4950
4951    #[tokio::test]
4952    async fn v1_swagger_ui_serves_html() {
4953        let router = create_router_v1(MockState);
4954        let req = Request::builder()
4955            .uri("/v1")
4956            .body(axum::body::Body::empty())
4957            .unwrap();
4958        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
4959        assert_eq!(resp.status(), StatusCode::OK);
4960        let content_type = resp
4961            .headers()
4962            .get(axum::http::header::CONTENT_TYPE)
4963            .unwrap()
4964            .to_str()
4965            .unwrap()
4966            .to_string();
4967        assert!(content_type.contains("text/html"));
4968        let body = body_string(resp).await;
4969        assert!(body.contains(routes::v1::OPENAPI_SPEC_ROUTE));
4970    }
4971
4972    /// The spec is served as JSON and is stable across requests.
4973    #[tokio::test]
4974    async fn v1_openapi_spec_served_as_json() {
4975        let router = create_router_v1(MockState);
4976        let get_spec = || async {
4977            let req = Request::builder()
4978                .uri(routes::v1::OPENAPI_SPEC_ROUTE)
4979                .body(axum::body::Body::empty())
4980                .unwrap();
4981            let resp = tower::ServiceExt::oneshot(router.clone(), req)
4982                .await
4983                .unwrap();
4984            assert_eq!(
4985                resp.headers().get(header::CONTENT_TYPE).unwrap(),
4986                "application/json"
4987            );
4988            axum::body::to_bytes(resp.into_body(), usize::MAX)
4989                .await
4990                .unwrap()
4991        };
4992        let (first, second) = (get_spec().await, get_spec().await);
4993        assert_eq!(first, second);
4994    }
4995
4996    #[tokio::test]
4997    async fn v1_openapi_spec_contains_known_route() {
4998        let router = create_router_v1(MockState);
4999        let req = Request::builder()
5000            .uri(routes::v1::OPENAPI_SPEC_ROUTE)
5001            .body(axum::body::Body::empty())
5002            .unwrap();
5003        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
5004        assert_eq!(resp.status(), StatusCode::OK);
5005        let body = body_string(resp).await;
5006        let spec: serde_json::Value = serde_json::from_str(&body).expect("valid JSON");
5007        assert!(
5008            spec["paths"]
5009                .as_object()
5010                .expect("spec has paths")
5011                .contains_key(routes::v1::STATUS_BLOCK_HEIGHT_ROUTE),
5012            "expected {} in spec paths: {}",
5013            routes::v1::STATUS_BLOCK_HEIGHT_ROUTE,
5014            body
5015        );
5016    }
5017
5018    #[tokio::test]
5019    async fn v2_openapi_spec_documents_the_proto_routes() {
5020        let req = Request::builder()
5021            .uri(routes::v2::OPENAPI_SPEC_ROUTE)
5022            .body(axum::body::Body::empty())
5023            .unwrap();
5024        let resp = tower::ServiceExt::oneshot(router_v2_docs(), req)
5025            .await
5026            .unwrap();
5027        assert_eq!(resp.status(), StatusCode::OK);
5028        let body = body_string(resp).await;
5029        let spec: serde_json::Value = serde_json::from_str(&body).expect("valid JSON");
5030        let documented: std::collections::BTreeSet<&str> = spec["paths"]
5031            .as_object()
5032            .expect("spec has paths")
5033            .keys()
5034            .map(String::as_str)
5035            .collect();
5036
5037        // Every documented route is one `serve_axum` mounts, so a generated client cannot ship a
5038        // method that always 404s. Adding an endpoint has to update this list.
5039        let expected: std::collections::BTreeSet<&str> = [
5040            "/v2/config/env",
5041            "/v2/config/hotshot",
5042            "/v2/config/runtime",
5043            "/v2/database/migration-status",
5044            "/v2/database/table-sizes",
5045            "/v2/node/all-validators",
5046            "/v2/node/block-height",
5047            "/v2/node/block-reward",
5048            "/v2/node/header-window",
5049            "/v2/node/limits",
5050            "/v2/node/participation/proposal",
5051            "/v2/node/participation/vote",
5052            "/v2/node/payload-size",
5053            "/v2/node/stake-table",
5054            "/v2/node/sync-status",
5055            "/v2/node/transaction-count",
5056            "/v2/node/validators",
5057            "/v2/node/vid-share",
5058            "/v2/status/block-height",
5059            "/v2/status/keys",
5060            "/v2/status/success-rate",
5061            "/v2/status/time-since-last-decide",
5062            "/v2/token/circulating-supply",
5063            "/v2/token/circulating-supply-ethereum",
5064            "/v2/token/total-issued-supply",
5065            "/v2/token/total-minted-supply",
5066            "/v2/token/total-reward-distributed",
5067            "/v2/merklized-state/block/path",
5068            "/v2/merklized-state/fee/path",
5069            "/v2/merklized-state/reward/amounts",
5070            "/v2/merklized-state/reward/balance",
5071            "/v2/merklized-state/reward/claim-input",
5072            "/v2/merklized-state/reward/proof",
5073            "/v2/merklized-state/reward/tree",
5074            "/v2/merklized-state/fee/balance",
5075            "/v2/merklized-state/height",
5076            "/v2/availability/limits",
5077            "/v2/availability/header",
5078            "/v2/availability/header-range",
5079            "/v2/availability/leaf",
5080            "/v2/availability/leaf-range",
5081            "/v2/availability/leaf-ranges",
5082            "/v2/availability/cert2",
5083            "/v2/availability/block",
5084            "/v2/availability/block-range",
5085            "/v2/availability/block-ranges",
5086            "/v2/availability/payload",
5087            "/v2/availability/payload-range",
5088            "/v2/availability/vid-common",
5089            "/v2/availability/vid-common-range",
5090            "/v2/availability/vid-common-ranges",
5091            "/v2/availability/transaction",
5092            "/v2/availability/transaction-proof",
5093            "/v2/availability/block-summary",
5094            "/v2/availability/block-summary-range",
5095            "/v2/availability/namespace-proof",
5096            "/v2/availability/namespace-proof-range",
5097            "/v2/availability/incorrect-encoding-proof",
5098            "/v2/availability/state-cert",
5099            "/v2/availability/state-cert-v2",
5100            "/v2/availability/stream/leaves",
5101            "/v2/availability/stream/headers",
5102            "/v2/availability/stream/blocks",
5103            "/v2/availability/stream/payloads",
5104            "/v2/availability/stream/vid-common",
5105            "/v2/availability/stream/transactions",
5106            "/v2/availability/stream/namespace-proofs",
5107            "/v2/state-signature/block",
5108            "/v2/submit/transaction",
5109            "/v2/explorer/block",
5110            "/v2/explorer/blocks",
5111            "/v2/explorer/transaction",
5112            "/v2/explorer/transactions",
5113            "/v2/explorer/summary",
5114            "/v2/explorer/search",
5115            "/v2/catchup/fee-account",
5116            "/v2/catchup/fee-accounts",
5117            "/v2/catchup/blocks-frontier",
5118            "/v2/catchup/chain-config",
5119            "/v2/catchup/leaf-chain",
5120            "/v2/catchup/cert2",
5121            "/v2/catchup/reward-account",
5122            "/v2/catchup/reward-accounts",
5123            "/v2/catchup/reward-account-v2",
5124            "/v2/catchup/reward-merkle-tree-v2",
5125            "/v2/catchup/state-cert",
5126            "/v2/light-client/leaf",
5127            "/v2/light-client/header",
5128            "/v2/light-client/stake-table",
5129            "/v2/light-client/payload",
5130            "/v2/light-client/payload-range",
5131            "/v2/light-client/payload-ranges",
5132            "/v2/light-client/namespace",
5133            "/v2/light-client/namespace-range",
5134            "/v2/light-client/namespaces-range",
5135        ]
5136        .into_iter()
5137        .collect();
5138        assert_eq!(documented, expected);
5139    }
5140
5141    /// Implements the v2 tonic service traits with `Err(Status::internal)` bodies rather than
5142    /// `unimplemented!()`: the mounted-routes test below invokes the handlers, and any response
5143    /// at all proves the route is mounted, while a panic would abort the test.
5144    #[derive(Clone)]
5145    struct MockV2State;
5146
5147    #[tonic::async_trait]
5148    impl crate::proto::status_service_server::StatusService for MockV2State {
5149        async fn get_block_height(
5150            &self,
5151            _request: tonic::Request<crate::proto::GetBlockHeightRequest>,
5152        ) -> Result<tonic::Response<crate::proto::BlockHeightResponse>, tonic::Status> {
5153            Err(tonic::Status::internal("mock"))
5154        }
5155
5156        async fn get_success_rate(
5157            &self,
5158            _request: tonic::Request<crate::proto::GetSuccessRateRequest>,
5159        ) -> Result<tonic::Response<crate::proto::SuccessRateResponse>, tonic::Status> {
5160            Err(tonic::Status::internal("mock"))
5161        }
5162
5163        async fn get_time_since_last_decide(
5164            &self,
5165            _request: tonic::Request<crate::proto::GetTimeSinceLastDecideRequest>,
5166        ) -> Result<tonic::Response<crate::proto::TimeSinceLastDecideResponse>, tonic::Status>
5167        {
5168            Err(tonic::Status::internal("mock"))
5169        }
5170
5171        async fn get_node_keys(
5172            &self,
5173            _request: tonic::Request<crate::proto::GetNodeKeysRequest>,
5174        ) -> Result<tonic::Response<crate::proto::NodeKeysResponse>, tonic::Status> {
5175            Err(tonic::Status::internal("mock"))
5176        }
5177    }
5178
5179    #[tonic::async_trait]
5180    impl crate::proto::token_service_server::TokenService for MockV2State {
5181        async fn get_total_minted_supply(
5182            &self,
5183            _request: tonic::Request<crate::proto::GetTotalMintedSupplyRequest>,
5184        ) -> Result<tonic::Response<crate::proto::TotalMintedSupplyResponse>, tonic::Status>
5185        {
5186            Err(tonic::Status::internal("mock"))
5187        }
5188
5189        async fn get_circulating_supply(
5190            &self,
5191            _request: tonic::Request<crate::proto::GetCirculatingSupplyRequest>,
5192        ) -> Result<tonic::Response<crate::proto::CirculatingSupplyResponse>, tonic::Status>
5193        {
5194            Err(tonic::Status::internal("mock"))
5195        }
5196
5197        async fn get_circulating_supply_ethereum(
5198            &self,
5199            _request: tonic::Request<crate::proto::GetCirculatingSupplyEthereumRequest>,
5200        ) -> Result<tonic::Response<crate::proto::CirculatingSupplyEthereumResponse>, tonic::Status>
5201        {
5202            Err(tonic::Status::internal("mock"))
5203        }
5204
5205        async fn get_total_issued_supply(
5206            &self,
5207            _request: tonic::Request<crate::proto::GetTotalIssuedSupplyRequest>,
5208        ) -> Result<tonic::Response<crate::proto::TotalIssuedSupplyResponse>, tonic::Status>
5209        {
5210            Err(tonic::Status::internal("mock"))
5211        }
5212
5213        async fn get_total_reward_distributed(
5214            &self,
5215            _request: tonic::Request<crate::proto::GetTotalRewardDistributedRequest>,
5216        ) -> Result<tonic::Response<crate::proto::TotalRewardDistributedResponse>, tonic::Status>
5217        {
5218            Err(tonic::Status::internal("mock"))
5219        }
5220    }
5221
5222    #[tonic::async_trait]
5223    impl crate::proto::node_service_server::NodeService for MockV2State {
5224        async fn get_transaction_count(
5225            &self,
5226            _request: tonic::Request<crate::proto::GetTransactionCountRequest>,
5227        ) -> Result<tonic::Response<crate::proto::TransactionCountResponse>, tonic::Status>
5228        {
5229            Err(tonic::Status::internal("mock"))
5230        }
5231
5232        async fn get_payload_size(
5233            &self,
5234            _request: tonic::Request<crate::proto::GetPayloadSizeRequest>,
5235        ) -> Result<tonic::Response<crate::proto::PayloadSizeResponse>, tonic::Status> {
5236            Err(tonic::Status::internal("mock"))
5237        }
5238
5239        async fn get_sync_status(
5240            &self,
5241            _request: tonic::Request<crate::proto::GetSyncStatusRequest>,
5242        ) -> Result<tonic::Response<crate::proto::SyncStatusResponse>, tonic::Status> {
5243            Err(tonic::Status::internal("mock"))
5244        }
5245
5246        async fn get_block_reward(
5247            &self,
5248            _request: tonic::Request<crate::proto::GetBlockRewardRequest>,
5249        ) -> Result<tonic::Response<crate::proto::BlockRewardResponse>, tonic::Status> {
5250            Err(tonic::Status::internal("mock"))
5251        }
5252
5253        async fn get_vid_share(
5254            &self,
5255            _request: tonic::Request<crate::proto::GetVidShareRequest>,
5256        ) -> Result<tonic::Response<crate::proto::VidShareResponse>, tonic::Status> {
5257            Err(tonic::Status::internal("mock"))
5258        }
5259
5260        async fn get_header_window(
5261            &self,
5262            _request: tonic::Request<crate::proto::GetHeaderWindowRequest>,
5263        ) -> Result<tonic::Response<crate::proto::HeaderWindowResponse>, tonic::Status> {
5264            Err(tonic::Status::internal("mock"))
5265        }
5266
5267        async fn get_node_block_height(
5268            &self,
5269            _request: tonic::Request<crate::proto::GetNodeBlockHeightRequest>,
5270        ) -> Result<tonic::Response<crate::proto::NodeBlockHeightResponse>, tonic::Status> {
5271            Err(tonic::Status::internal("mock"))
5272        }
5273
5274        async fn get_node_limits(
5275            &self,
5276            _request: tonic::Request<crate::proto::GetNodeLimitsRequest>,
5277        ) -> Result<tonic::Response<crate::proto::NodeLimitsResponse>, tonic::Status> {
5278            Err(tonic::Status::internal("mock"))
5279        }
5280
5281        async fn get_stake_table(
5282            &self,
5283            _request: tonic::Request<crate::proto::GetStakeTableRequest>,
5284        ) -> Result<tonic::Response<crate::proto::StakeTableResponse>, tonic::Status> {
5285            Err(tonic::Status::internal("mock"))
5286        }
5287
5288        async fn get_validators(
5289            &self,
5290            _request: tonic::Request<crate::proto::GetValidatorsRequest>,
5291        ) -> Result<tonic::Response<crate::proto::ValidatorsResponse>, tonic::Status> {
5292            Err(tonic::Status::internal("mock"))
5293        }
5294
5295        async fn get_all_validators(
5296            &self,
5297            _request: tonic::Request<crate::proto::GetAllValidatorsRequest>,
5298        ) -> Result<tonic::Response<crate::proto::ValidatorsResponse>, tonic::Status> {
5299            Err(tonic::Status::internal("mock"))
5300        }
5301
5302        async fn get_proposal_participation(
5303            &self,
5304            _request: tonic::Request<crate::proto::GetProposalParticipationRequest>,
5305        ) -> Result<tonic::Response<crate::proto::ParticipationResponse>, tonic::Status> {
5306            Err(tonic::Status::internal("mock"))
5307        }
5308
5309        async fn get_vote_participation(
5310            &self,
5311            _request: tonic::Request<crate::proto::GetVoteParticipationRequest>,
5312        ) -> Result<tonic::Response<crate::proto::ParticipationResponse>, tonic::Status> {
5313            Err(tonic::Status::internal("mock"))
5314        }
5315    }
5316
5317    #[tonic::async_trait]
5318    impl crate::proto::config_service_server::ConfigService for MockV2State {
5319        async fn get_hotshot_config(
5320            &self,
5321            _request: tonic::Request<crate::proto::GetHotshotConfigRequest>,
5322        ) -> Result<tonic::Response<crate::proto::HotshotConfigResponse>, tonic::Status> {
5323            Err(tonic::Status::internal("mock"))
5324        }
5325
5326        async fn get_env(
5327            &self,
5328            _request: tonic::Request<crate::proto::GetEnvRequest>,
5329        ) -> Result<tonic::Response<crate::proto::EnvResponse>, tonic::Status> {
5330            Err(tonic::Status::internal("mock"))
5331        }
5332
5333        async fn get_runtime_config(
5334            &self,
5335            _request: tonic::Request<crate::proto::GetRuntimeConfigRequest>,
5336        ) -> Result<tonic::Response<crate::proto::RuntimeConfigResponse>, tonic::Status> {
5337            Err(tonic::Status::internal("mock"))
5338        }
5339    }
5340
5341    #[tonic::async_trait]
5342    impl crate::proto::availability_service_server::AvailabilityService for MockV2State {
5343        async fn get_limits(
5344            &self,
5345            _request: tonic::Request<crate::proto::GetLimitsRequest>,
5346        ) -> Result<tonic::Response<crate::proto::LimitsResponse>, tonic::Status> {
5347            Err(tonic::Status::internal("mock"))
5348        }
5349
5350        async fn get_header(
5351            &self,
5352            _request: tonic::Request<crate::proto::GetHeaderRequest>,
5353        ) -> Result<tonic::Response<crate::proto::HeaderResponse>, tonic::Status> {
5354            Err(tonic::Status::internal("mock"))
5355        }
5356
5357        async fn get_header_range(
5358            &self,
5359            _request: tonic::Request<crate::proto::GetHeaderRangeRequest>,
5360        ) -> Result<tonic::Response<crate::proto::HeaderRangeResponse>, tonic::Status> {
5361            Err(tonic::Status::internal("mock"))
5362        }
5363
5364        async fn get_leaf(
5365            &self,
5366            _request: tonic::Request<crate::proto::GetLeafRequest>,
5367        ) -> Result<tonic::Response<crate::proto::LeafResponse>, tonic::Status> {
5368            Err(tonic::Status::internal("mock"))
5369        }
5370
5371        async fn get_leaf_range(
5372            &self,
5373            _request: tonic::Request<crate::proto::GetLeafRangeRequest>,
5374        ) -> Result<tonic::Response<crate::proto::LeafRangeResponse>, tonic::Status> {
5375            Err(tonic::Status::internal("mock"))
5376        }
5377
5378        async fn get_leaf_ranges(
5379            &self,
5380            _request: tonic::Request<crate::proto::GetLeafRangesRequest>,
5381        ) -> Result<tonic::Response<crate::proto::LeafRangeResponse>, tonic::Status> {
5382            Err(tonic::Status::internal("mock"))
5383        }
5384
5385        async fn get_cert2(
5386            &self,
5387            _request: tonic::Request<crate::proto::GetCert2Request>,
5388        ) -> Result<tonic::Response<crate::proto::Cert2Response>, tonic::Status> {
5389            Err(tonic::Status::internal("mock"))
5390        }
5391
5392        async fn get_block(
5393            &self,
5394            _request: tonic::Request<crate::proto::GetBlockRequest>,
5395        ) -> Result<tonic::Response<crate::proto::BlockResponse>, tonic::Status> {
5396            Err(tonic::Status::internal("mock"))
5397        }
5398
5399        async fn get_block_range(
5400            &self,
5401            _request: tonic::Request<crate::proto::GetBlockRangeRequest>,
5402        ) -> Result<tonic::Response<crate::proto::BlockRangeResponse>, tonic::Status> {
5403            Err(tonic::Status::internal("mock"))
5404        }
5405
5406        async fn get_block_ranges(
5407            &self,
5408            _request: tonic::Request<crate::proto::GetBlockRangesRequest>,
5409        ) -> Result<tonic::Response<crate::proto::BlockRangeResponse>, tonic::Status> {
5410            Err(tonic::Status::internal("mock"))
5411        }
5412
5413        async fn get_payload(
5414            &self,
5415            _request: tonic::Request<crate::proto::GetPayloadRequest>,
5416        ) -> Result<tonic::Response<crate::proto::PayloadResponse>, tonic::Status> {
5417            Err(tonic::Status::internal("mock"))
5418        }
5419
5420        async fn get_payload_range(
5421            &self,
5422            _request: tonic::Request<crate::proto::GetPayloadRangeRequest>,
5423        ) -> Result<tonic::Response<crate::proto::PayloadRangeResponse>, tonic::Status> {
5424            Err(tonic::Status::internal("mock"))
5425        }
5426
5427        async fn get_vid_common(
5428            &self,
5429            _request: tonic::Request<crate::proto::GetVidCommonRequest>,
5430        ) -> Result<tonic::Response<crate::proto::VidCommonResponse>, tonic::Status> {
5431            Err(tonic::Status::internal("mock"))
5432        }
5433
5434        async fn get_vid_common_range(
5435            &self,
5436            _request: tonic::Request<crate::proto::GetVidCommonRangeRequest>,
5437        ) -> Result<tonic::Response<crate::proto::VidCommonRangeResponse>, tonic::Status> {
5438            Err(tonic::Status::internal("mock"))
5439        }
5440
5441        async fn get_vid_common_ranges(
5442            &self,
5443            _request: tonic::Request<crate::proto::GetVidCommonRangesRequest>,
5444        ) -> Result<tonic::Response<crate::proto::VidCommonRangeResponse>, tonic::Status> {
5445            Err(tonic::Status::internal("mock"))
5446        }
5447
5448        async fn get_transaction(
5449            &self,
5450            _request: tonic::Request<crate::proto::GetTransactionRequest>,
5451        ) -> Result<tonic::Response<crate::proto::TransactionResponse>, tonic::Status> {
5452            Err(tonic::Status::internal("mock"))
5453        }
5454
5455        async fn get_transaction_proof(
5456            &self,
5457            _request: tonic::Request<crate::proto::GetTransactionProofRequest>,
5458        ) -> Result<tonic::Response<crate::proto::TransactionWithProofResponse>, tonic::Status>
5459        {
5460            Err(tonic::Status::internal("mock"))
5461        }
5462
5463        async fn get_block_summary(
5464            &self,
5465            _request: tonic::Request<crate::proto::GetBlockSummaryRequest>,
5466        ) -> Result<tonic::Response<crate::proto::BlockSummaryResponse>, tonic::Status> {
5467            Err(tonic::Status::internal("mock"))
5468        }
5469
5470        async fn get_block_summary_range(
5471            &self,
5472            _request: tonic::Request<crate::proto::GetBlockSummaryRangeRequest>,
5473        ) -> Result<tonic::Response<crate::proto::BlockSummaryRangeResponse>, tonic::Status>
5474        {
5475            Err(tonic::Status::internal("mock"))
5476        }
5477
5478        async fn get_namespace_proof(
5479            &self,
5480            _request: tonic::Request<crate::proto::GetNamespaceProofRequest>,
5481        ) -> Result<tonic::Response<crate::proto::NamespaceProofResponse>, tonic::Status> {
5482            Err(tonic::Status::internal("mock"))
5483        }
5484
5485        async fn get_namespace_proof_range(
5486            &self,
5487            _request: tonic::Request<crate::proto::GetNamespaceProofRangeRequest>,
5488        ) -> Result<tonic::Response<crate::proto::NamespaceProofRangeResponse>, tonic::Status>
5489        {
5490            Err(tonic::Status::internal("mock"))
5491        }
5492
5493        async fn get_incorrect_encoding_proof(
5494            &self,
5495            _request: tonic::Request<crate::proto::GetIncorrectEncodingProofRequest>,
5496        ) -> Result<tonic::Response<crate::proto::IncorrectEncodingProofResponse>, tonic::Status>
5497        {
5498            Err(tonic::Status::internal("mock"))
5499        }
5500
5501        async fn get_state_cert(
5502            &self,
5503            _request: tonic::Request<crate::proto::GetStateCertRequest>,
5504        ) -> Result<tonic::Response<crate::proto::StateCertV1Response>, tonic::Status> {
5505            Err(tonic::Status::internal("mock"))
5506        }
5507
5508        async fn get_state_cert_v2(
5509            &self,
5510            _request: tonic::Request<crate::proto::GetStateCertV2Request>,
5511        ) -> Result<tonic::Response<crate::proto::StateCertV2Response>, tonic::Status> {
5512            Err(tonic::Status::internal("mock"))
5513        }
5514
5515        type StreamLeavesStream =
5516            BoxStream<'static, Result<crate::proto::LeafResponse, tonic::Status>>;
5517
5518        async fn stream_leaves(
5519            &self,
5520            _request: tonic::Request<crate::proto::StreamLeavesRequest>,
5521        ) -> Result<tonic::Response<Self::StreamLeavesStream>, tonic::Status> {
5522            Err(tonic::Status::internal("mock"))
5523        }
5524
5525        type StreamHeadersStream =
5526            BoxStream<'static, Result<crate::proto::HeaderResponse, tonic::Status>>;
5527
5528        async fn stream_headers(
5529            &self,
5530            _request: tonic::Request<crate::proto::StreamHeadersRequest>,
5531        ) -> Result<tonic::Response<Self::StreamHeadersStream>, tonic::Status> {
5532            Err(tonic::Status::internal("mock"))
5533        }
5534
5535        type StreamBlocksStream =
5536            BoxStream<'static, Result<crate::proto::BlockResponse, tonic::Status>>;
5537
5538        async fn stream_blocks(
5539            &self,
5540            _request: tonic::Request<crate::proto::StreamBlocksRequest>,
5541        ) -> Result<tonic::Response<Self::StreamBlocksStream>, tonic::Status> {
5542            Err(tonic::Status::internal("mock"))
5543        }
5544
5545        type StreamPayloadsStream =
5546            BoxStream<'static, Result<crate::proto::PayloadResponse, tonic::Status>>;
5547
5548        async fn stream_payloads(
5549            &self,
5550            _request: tonic::Request<crate::proto::StreamPayloadsRequest>,
5551        ) -> Result<tonic::Response<Self::StreamPayloadsStream>, tonic::Status> {
5552            Err(tonic::Status::internal("mock"))
5553        }
5554
5555        type StreamVidCommonStream =
5556            BoxStream<'static, Result<crate::proto::VidCommonResponse, tonic::Status>>;
5557
5558        async fn stream_vid_common(
5559            &self,
5560            _request: tonic::Request<crate::proto::StreamVidCommonRequest>,
5561        ) -> Result<tonic::Response<Self::StreamVidCommonStream>, tonic::Status> {
5562            Err(tonic::Status::internal("mock"))
5563        }
5564
5565        type StreamTransactionsStream =
5566            BoxStream<'static, Result<crate::proto::TransactionResponse, tonic::Status>>;
5567
5568        async fn stream_transactions(
5569            &self,
5570            _request: tonic::Request<crate::proto::StreamTransactionsRequest>,
5571        ) -> Result<tonic::Response<Self::StreamTransactionsStream>, tonic::Status> {
5572            Err(tonic::Status::internal("mock"))
5573        }
5574
5575        type StreamNamespaceProofsStream =
5576            BoxStream<'static, Result<crate::proto::NamespaceProofResponse, tonic::Status>>;
5577
5578        async fn stream_namespace_proofs(
5579            &self,
5580            _request: tonic::Request<crate::proto::StreamNamespaceProofsRequest>,
5581        ) -> Result<tonic::Response<Self::StreamNamespaceProofsStream>, tonic::Status> {
5582            Err(tonic::Status::internal("mock"))
5583        }
5584    }
5585
5586    #[tonic::async_trait]
5587    impl crate::proto::database_service_server::DatabaseService for MockV2State {
5588        async fn get_table_sizes(
5589            &self,
5590            _request: tonic::Request<crate::proto::GetTableSizesRequest>,
5591        ) -> Result<tonic::Response<crate::proto::TableSizesResponse>, tonic::Status> {
5592            Err(tonic::Status::internal("mock"))
5593        }
5594
5595        async fn get_migration_status(
5596            &self,
5597            _request: tonic::Request<crate::proto::GetMigrationStatusRequest>,
5598        ) -> Result<tonic::Response<crate::proto::MigrationStatusResponse>, tonic::Status> {
5599            Err(tonic::Status::internal("mock"))
5600        }
5601    }
5602
5603    #[tonic::async_trait]
5604    impl crate::proto::merklized_state_service_server::MerklizedStateService for MockV2State {
5605        async fn get_block_state_path(
5606            &self,
5607            _request: tonic::Request<crate::proto::GetBlockStatePathRequest>,
5608        ) -> Result<tonic::Response<crate::proto::MerklePathResponse>, tonic::Status> {
5609            Err(tonic::Status::internal("mock"))
5610        }
5611
5612        async fn get_fee_state_path(
5613            &self,
5614            _request: tonic::Request<crate::proto::GetFeeStatePathRequest>,
5615        ) -> Result<tonic::Response<crate::proto::MerklePathResponse>, tonic::Status> {
5616            Err(tonic::Status::internal("mock"))
5617        }
5618
5619        async fn get_latest_fee_balance(
5620            &self,
5621            _request: tonic::Request<crate::proto::GetLatestFeeBalanceRequest>,
5622        ) -> Result<tonic::Response<crate::proto::FeeBalanceResponse>, tonic::Status> {
5623            Err(tonic::Status::internal("mock"))
5624        }
5625
5626        async fn get_state_height(
5627            &self,
5628            _request: tonic::Request<crate::proto::GetStateHeightRequest>,
5629        ) -> Result<tonic::Response<crate::proto::StateHeightResponse>, tonic::Status> {
5630            Err(tonic::Status::internal("mock"))
5631        }
5632    }
5633
5634    #[tonic::async_trait]
5635    impl crate::proto::reward_state_service_server::RewardStateService for MockV2State {
5636        async fn get_reward_balance(
5637            &self,
5638            _request: tonic::Request<crate::proto::GetRewardBalanceRequest>,
5639        ) -> Result<tonic::Response<crate::proto::RewardBalanceResponse>, tonic::Status> {
5640            Err(tonic::Status::internal("mock"))
5641        }
5642
5643        async fn get_reward_account_proof(
5644            &self,
5645            _request: tonic::Request<crate::proto::GetRewardAccountProofRequest>,
5646        ) -> Result<tonic::Response<crate::proto::RewardAccountProofResponse>, tonic::Status>
5647        {
5648            Err(tonic::Status::internal("mock"))
5649        }
5650
5651        async fn get_reward_claim_input(
5652            &self,
5653            _request: tonic::Request<crate::proto::GetRewardClaimInputRequest>,
5654        ) -> Result<tonic::Response<crate::proto::RewardClaimInputResponse>, tonic::Status>
5655        {
5656            Err(tonic::Status::internal("mock"))
5657        }
5658
5659        async fn get_reward_amounts(
5660            &self,
5661            _request: tonic::Request<crate::proto::GetRewardAmountsRequest>,
5662        ) -> Result<tonic::Response<crate::proto::RewardAmountsResponse>, tonic::Status> {
5663            Err(tonic::Status::internal("mock"))
5664        }
5665
5666        async fn get_reward_merkle_tree_v2(
5667            &self,
5668            _request: tonic::Request<crate::proto::GetRewardMerkleTreeV2Request>,
5669        ) -> Result<tonic::Response<crate::proto::RewardMerkleTreeV2Response>, tonic::Status>
5670        {
5671            Err(tonic::Status::internal("mock"))
5672        }
5673    }
5674
5675    #[tonic::async_trait]
5676    impl crate::proto::state_signature_service_server::StateSignatureService for MockV2State {
5677        async fn get_state_signature(
5678            &self,
5679            _request: tonic::Request<crate::proto::GetStateSignatureRequest>,
5680        ) -> Result<tonic::Response<crate::proto::StateSignatureResponse>, tonic::Status> {
5681            Err(tonic::Status::internal("mock"))
5682        }
5683    }
5684
5685    #[tonic::async_trait]
5686    impl crate::proto::light_client_service_server::LightClientService for MockV2State {
5687        async fn get_light_client_leaf_proof(
5688            &self,
5689            _request: tonic::Request<crate::proto::GetLightClientLeafProofRequest>,
5690        ) -> Result<tonic::Response<crate::proto::LightClientLeafProofResponse>, tonic::Status>
5691        {
5692            Err(tonic::Status::internal("mock"))
5693        }
5694
5695        async fn get_light_client_header_proof(
5696            &self,
5697            _request: tonic::Request<crate::proto::GetLightClientHeaderProofRequest>,
5698        ) -> Result<tonic::Response<crate::proto::LightClientHeaderProofResponse>, tonic::Status>
5699        {
5700            Err(tonic::Status::internal("mock"))
5701        }
5702
5703        async fn get_light_client_stake_table(
5704            &self,
5705            _request: tonic::Request<crate::proto::GetLightClientStakeTableRequest>,
5706        ) -> Result<tonic::Response<crate::proto::LightClientStakeTableResponse>, tonic::Status>
5707        {
5708            Err(tonic::Status::internal("mock"))
5709        }
5710
5711        async fn get_light_client_payload_proof(
5712            &self,
5713            _request: tonic::Request<crate::proto::GetLightClientPayloadProofRequest>,
5714        ) -> Result<tonic::Response<crate::proto::LightClientPayloadProofResponse>, tonic::Status>
5715        {
5716            Err(tonic::Status::internal("mock"))
5717        }
5718
5719        async fn get_light_client_payload_proof_range(
5720            &self,
5721            _request: tonic::Request<crate::proto::GetLightClientPayloadProofRangeRequest>,
5722        ) -> Result<
5723            tonic::Response<crate::proto::LightClientPayloadProofRangeResponse>,
5724            tonic::Status,
5725        > {
5726            Err(tonic::Status::internal("mock"))
5727        }
5728
5729        async fn get_light_client_payload_proof_ranges(
5730            &self,
5731            _request: tonic::Request<crate::proto::GetLightClientPayloadProofRangesRequest>,
5732        ) -> Result<
5733            tonic::Response<crate::proto::LightClientPayloadProofRangeResponse>,
5734            tonic::Status,
5735        > {
5736            Err(tonic::Status::internal("mock"))
5737        }
5738
5739        async fn get_light_client_namespace_proof(
5740            &self,
5741            _request: tonic::Request<crate::proto::GetLightClientNamespaceProofRequest>,
5742        ) -> Result<tonic::Response<crate::proto::LightClientNamespaceProofResponse>, tonic::Status>
5743        {
5744            Err(tonic::Status::internal("mock"))
5745        }
5746
5747        async fn get_light_client_namespace_proof_range(
5748            &self,
5749            _request: tonic::Request<crate::proto::GetLightClientNamespaceProofRangeRequest>,
5750        ) -> Result<
5751            tonic::Response<crate::proto::LightClientNamespaceProofRangeResponse>,
5752            tonic::Status,
5753        > {
5754            Err(tonic::Status::internal("mock"))
5755        }
5756
5757        async fn get_light_client_namespaces_proof_range(
5758            &self,
5759            _request: tonic::Request<crate::proto::GetLightClientNamespacesProofRangeRequest>,
5760        ) -> Result<
5761            tonic::Response<crate::proto::LightClientNamespacesProofRangeResponse>,
5762            tonic::Status,
5763        > {
5764            Err(tonic::Status::internal("mock"))
5765        }
5766    }
5767
5768    #[tonic::async_trait]
5769    impl crate::proto::catchup_service_server::CatchupService for MockV2State {
5770        async fn get_catchup_fee_account(
5771            &self,
5772            _request: tonic::Request<crate::proto::GetCatchupFeeAccountRequest>,
5773        ) -> Result<tonic::Response<crate::proto::FeeAccountProofResponse>, tonic::Status> {
5774            Err(tonic::Status::internal("mock"))
5775        }
5776
5777        async fn get_catchup_fee_accounts(
5778            &self,
5779            _request: tonic::Request<crate::proto::GetCatchupFeeAccountsRequest>,
5780        ) -> Result<tonic::Response<crate::proto::FeeMerkleTreeResponse>, tonic::Status> {
5781            Err(tonic::Status::internal("mock"))
5782        }
5783
5784        async fn get_catchup_blocks_frontier(
5785            &self,
5786            _request: tonic::Request<crate::proto::GetCatchupBlocksFrontierRequest>,
5787        ) -> Result<tonic::Response<crate::proto::MerklePathResponse>, tonic::Status> {
5788            Err(tonic::Status::internal("mock"))
5789        }
5790
5791        async fn get_catchup_chain_config(
5792            &self,
5793            _request: tonic::Request<crate::proto::GetCatchupChainConfigRequest>,
5794        ) -> Result<tonic::Response<crate::proto::CatchupChainConfigResponse>, tonic::Status>
5795        {
5796            Err(tonic::Status::internal("mock"))
5797        }
5798
5799        async fn get_catchup_leaf_chain(
5800            &self,
5801            _request: tonic::Request<crate::proto::GetCatchupLeafChainRequest>,
5802        ) -> Result<tonic::Response<crate::proto::CatchupLeafChainResponse>, tonic::Status>
5803        {
5804            Err(tonic::Status::internal("mock"))
5805        }
5806
5807        async fn get_catchup_cert2(
5808            &self,
5809            _request: tonic::Request<crate::proto::GetCatchupCert2Request>,
5810        ) -> Result<tonic::Response<crate::proto::Cert2Response>, tonic::Status> {
5811            Err(tonic::Status::internal("mock"))
5812        }
5813
5814        async fn get_catchup_reward_account(
5815            &self,
5816            _request: tonic::Request<crate::proto::GetCatchupRewardAccountRequest>,
5817        ) -> Result<tonic::Response<crate::proto::RewardAccountProofResponse>, tonic::Status>
5818        {
5819            Err(tonic::Status::internal("mock"))
5820        }
5821
5822        async fn get_catchup_reward_accounts(
5823            &self,
5824            _request: tonic::Request<crate::proto::GetCatchupRewardAccountsRequest>,
5825        ) -> Result<tonic::Response<crate::proto::RewardMerkleTreeV1Response>, tonic::Status>
5826        {
5827            Err(tonic::Status::internal("mock"))
5828        }
5829
5830        async fn get_catchup_reward_account_v2(
5831            &self,
5832            _request: tonic::Request<crate::proto::GetCatchupRewardAccountV2Request>,
5833        ) -> Result<tonic::Response<crate::proto::RewardAccountProofResponse>, tonic::Status>
5834        {
5835            Err(tonic::Status::internal("mock"))
5836        }
5837
5838        async fn get_catchup_reward_merkle_tree_v2(
5839            &self,
5840            _request: tonic::Request<crate::proto::GetCatchupRewardMerkleTreeV2Request>,
5841        ) -> Result<tonic::Response<crate::proto::RewardMerkleTreeV2Response>, tonic::Status>
5842        {
5843            Err(tonic::Status::internal("mock"))
5844        }
5845
5846        async fn get_catchup_state_cert(
5847            &self,
5848            _request: tonic::Request<crate::proto::GetCatchupStateCertRequest>,
5849        ) -> Result<tonic::Response<crate::proto::StateCertV2Response>, tonic::Status> {
5850            Err(tonic::Status::internal("mock"))
5851        }
5852    }
5853
5854    #[tonic::async_trait]
5855    impl crate::proto::explorer_service_server::ExplorerService for MockV2State {
5856        async fn get_explorer_block_detail(
5857            &self,
5858            _request: tonic::Request<crate::proto::GetExplorerBlockDetailRequest>,
5859        ) -> Result<tonic::Response<crate::proto::ExplorerBlockDetailResponse>, tonic::Status>
5860        {
5861            Err(tonic::Status::internal("mock"))
5862        }
5863
5864        async fn get_explorer_block_summaries(
5865            &self,
5866            _request: tonic::Request<crate::proto::GetExplorerBlockSummariesRequest>,
5867        ) -> Result<tonic::Response<crate::proto::ExplorerBlockSummariesResponse>, tonic::Status>
5868        {
5869            Err(tonic::Status::internal("mock"))
5870        }
5871
5872        async fn get_explorer_transaction_detail(
5873            &self,
5874            _request: tonic::Request<crate::proto::GetExplorerTransactionDetailRequest>,
5875        ) -> Result<tonic::Response<crate::proto::ExplorerTransactionDetailResponse>, tonic::Status>
5876        {
5877            Err(tonic::Status::internal("mock"))
5878        }
5879
5880        async fn get_explorer_transaction_summaries(
5881            &self,
5882            _request: tonic::Request<crate::proto::GetExplorerTransactionSummariesRequest>,
5883        ) -> Result<
5884            tonic::Response<crate::proto::ExplorerTransactionSummariesResponse>,
5885            tonic::Status,
5886        > {
5887            Err(tonic::Status::internal("mock"))
5888        }
5889
5890        async fn get_explorer_summary(
5891            &self,
5892            _request: tonic::Request<crate::proto::GetExplorerSummaryRequest>,
5893        ) -> Result<tonic::Response<crate::proto::ExplorerSummaryResponse>, tonic::Status> {
5894            Err(tonic::Status::internal("mock"))
5895        }
5896
5897        async fn get_explorer_search(
5898            &self,
5899            _request: tonic::Request<crate::proto::GetExplorerSearchRequest>,
5900        ) -> Result<tonic::Response<crate::proto::ExplorerSearchResponse>, tonic::Status> {
5901            Err(tonic::Status::internal("mock"))
5902        }
5903    }
5904
5905    #[tonic::async_trait]
5906    impl crate::proto::submit_service_server::SubmitService for MockV2State {
5907        async fn submit_transaction(
5908            &self,
5909            _request: tonic::Request<crate::proto::SubmitTransactionRequest>,
5910        ) -> Result<tonic::Response<crate::proto::SubmitTransactionResponse>, tonic::Status>
5911        {
5912            Err(tonic::Status::internal("mock"))
5913        }
5914    }
5915
5916    /// Every path in the OpenAPI document must be a route [`crate::router_v2`] mounts, so a
5917    /// generated client cannot ship a method that always 404s.
5918    #[tokio::test]
5919    async fn v2_documented_routes_are_mounted() {
5920        let spec: serde_json::Value = serde_json::from_str(super::V2_OPENAPI).expect("valid JSON");
5921        let router = crate::router_v2(
5922            Arc::new(MockV2State),
5923            crate::OptionalModules {
5924                config: true,
5925                submit: true,
5926                explorer: true,
5927                ..Default::default()
5928            },
5929        );
5930        for (path, item) in spec["paths"].as_object().expect("spec has paths") {
5931            for verb in item.as_object().expect("path item has operations").keys() {
5932                let req = Request::builder()
5933                    .method(verb.to_uppercase().as_str())
5934                    .uri(path)
5935                    .body(axum::body::Body::empty())
5936                    .unwrap();
5937                let resp = tower::ServiceExt::oneshot(router.clone(), req)
5938                    .await
5939                    .unwrap();
5940                // A 405 would mean the path is mounted under another verb than the documented one.
5941                assert!(
5942                    ![StatusCode::NOT_FOUND, StatusCode::METHOD_NOT_ALLOWED]
5943                        .contains(&resp.status()),
5944                    "{verb} {path} is documented but not mounted: {}",
5945                    resp.status()
5946                );
5947            }
5948        }
5949    }
5950
5951    /// A subscription is served as server-sent events, and its documentation has to say so: a
5952    /// generated client reading it as `application/json` would try to parse the stream as one
5953    /// body. Everything else stays JSON.
5954    #[test]
5955    fn v2_streams_are_documented_as_event_streams() {
5956        use prost::Message as _;
5957
5958        let descriptors = prost_types::FileDescriptorSet::decode(crate::FILE_DESCRIPTOR_SET)
5959            .expect("valid descriptor set");
5960        let streaming: std::collections::BTreeSet<&str> = descriptors
5961            .file
5962            .iter()
5963            .flat_map(|file| &file.service)
5964            .flat_map(|service| &service.method)
5965            .filter(|method| method.server_streaming())
5966            .map(|method| method.name())
5967            .collect();
5968        assert!(!streaming.is_empty());
5969
5970        let spec: serde_json::Value = serde_json::from_str(super::V2_OPENAPI).expect("valid JSON");
5971        let mut documented = std::collections::BTreeSet::new();
5972        for (path, item) in spec["paths"].as_object().expect("spec has paths") {
5973            for operation in item.as_object().expect("path item has operations").values() {
5974                let id = operation["operationId"].as_str().expect("operation id");
5975                let content = &operation["responses"]["200"]["content"];
5976                let is_stream = streaming.contains(id);
5977                if is_stream {
5978                    documented.insert(id);
5979                }
5980                assert_eq!(
5981                    content.get("text/event-stream").is_some(),
5982                    is_stream,
5983                    "{path}"
5984                );
5985                assert_eq!(
5986                    content.get("application/json").is_some(),
5987                    !is_stream,
5988                    "{path}"
5989                );
5990            }
5991        }
5992        assert_eq!(documented, streaming, "every streaming rpc is documented");
5993    }
5994
5995    /// A `map` field is a repeated synthetic entry message in the descriptor, which the generator
5996    /// would otherwise publish as an array of a schema that does not exist.
5997    #[test]
5998    fn v2_maps_are_documented_as_objects() {
5999        let spec: serde_json::Value = serde_json::from_str(super::V2_OPENAPI).expect("valid JSON");
6000        let schemas = spec["components"]["schemas"]
6001            .as_object()
6002            .expect("spec has schemas");
6003        let namespaces = &schemas["BlockSummaryResponse"]["properties"]["namespaces"];
6004        assert_eq!(namespaces["type"], "object");
6005        assert_eq!(
6006            namespaces["additionalProperties"]["$ref"],
6007            "#/components/schemas/NamespaceInfo"
6008        );
6009        assert!(schemas.contains_key("NamespaceInfo"));
6010        // protoc names the synthetic entry after the field, so this is what leaks if the
6011        // generator ever registers one as a schema of its own.
6012        assert!(
6013            !schemas.contains_key("NamespacesEntry"),
6014            "a map entry leaked into the published schemas"
6015        );
6016    }
6017
6018    /// The docs router is merged in as `serve_axum` does: that merge swaps `router_v2`'s layered
6019    /// fallback for a plain one, so `router_v2` alone passes even with the paths unregistered.
6020    #[tokio::test]
6021    async fn disabled_modules_answer_in_the_envelope() {
6022        let spec: serde_json::Value = serde_json::from_str(super::V2_OPENAPI).expect("valid JSON");
6023        let paths = spec["paths"].as_object().expect("spec has paths");
6024        let router = crate::router_v2(Arc::new(MockV2State), crate::OptionalModules::default())
6025            .merge(router_v2_docs());
6026        for (prefix, registered) in [
6027            ("/v2/config/", routes::v2::CONFIG_ROUTES),
6028            ("/v2/submit/", routes::v2::SUBMIT_ROUTES),
6029            ("/v2/explorer/", routes::v2::EXPLORER_ROUTES),
6030        ] {
6031            let mut documented: Vec<&str> = paths
6032                .keys()
6033                .map(String::as_str)
6034                .filter(|path| path.starts_with(prefix))
6035                .collect();
6036            documented.sort_unstable();
6037            let mut registered = registered.to_vec();
6038            registered.sort_unstable();
6039            assert_eq!(registered, documented);
6040            for path in documented {
6041                let method = paths[path]
6042                    .as_object()
6043                    .unwrap()
6044                    .keys()
6045                    .next()
6046                    .unwrap()
6047                    .to_uppercase();
6048                assert_disabled_in_envelope(&router, &method, path).await;
6049            }
6050        }
6051    }
6052
6053    async fn assert_disabled_in_envelope(router: &Router, method: &str, path: &str) {
6054        let req = Request::builder()
6055            .method(method)
6056            .uri(path)
6057            .body(axum::body::Body::empty())
6058            .unwrap();
6059        let resp = tower::ServiceExt::oneshot(router.clone(), req)
6060            .await
6061            .unwrap();
6062        assert_eq!(resp.status(), StatusCode::NOT_FOUND, "{method} {path}");
6063        let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
6064            .await
6065            .unwrap();
6066        let envelope: serde_json::Value = serde_json::from_slice(&body)
6067            .unwrap_or_else(|err| panic!("{path}: {err}: {:?}", String::from_utf8_lossy(&body)));
6068        assert_eq!(envelope["error"]["code"], 404, "{path}");
6069        assert_eq!(envelope["error"]["status"], "NOT_FOUND", "{path}");
6070    }
6071
6072    /// A bad query parameter is refused by the extractor and wrapped by the envelope layer,
6073    /// neither of which is handler code, so a dependency bump could change either without any
6074    /// other test noticing.
6075    #[tokio::test]
6076    async fn v2_rejects_malformed_query_parameters() {
6077        let router = crate::router_v2(Arc::new(MockV2State), crate::OptionalModules::default());
6078        // v2 paths come from the proto annotations, not a constants module.
6079        let count = "/v2/node/transaction-count";
6080        for query in [
6081            "from=abc",
6082            "from=-1",
6083            "from=",
6084            "from=1.0",
6085            // A leading `+` is accepted, so it is not a rejection case; written raw it decoded to
6086            // a space and passed here for the wrong reason.
6087            // One past u64, and a repeat of a field that is not repeated.
6088            "from=18446744073709551616",
6089            "from=1&from=2",
6090            // Unknown fields are refused, so a misspelling is not silently a different query.
6091            "From=7",
6092            "bogus=1",
6093        ] {
6094            let req = Request::builder()
6095                .uri(format!("{count}?{query}"))
6096                .body(axum::body::Body::empty())
6097                .unwrap();
6098            let resp = tower::ServiceExt::oneshot(router.clone(), req)
6099                .await
6100                .unwrap();
6101            assert_eq!(resp.status(), StatusCode::BAD_REQUEST, "{query}");
6102
6103            let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
6104                .await
6105                .unwrap();
6106            let envelope: serde_json::Value = serde_json::from_slice(&body).unwrap();
6107            assert_eq!(envelope["error"]["code"], 400, "{query}");
6108            assert_eq!(envelope["error"]["status"], "INVALID_ARGUMENT", "{query}");
6109            assert!(
6110                envelope["error"]["message"]
6111                    .as_str()
6112                    .is_some_and(|m| !m.is_empty()),
6113                "{query}: empty message"
6114            );
6115        }
6116
6117        // An rpc whose request has no fields still refuses one.
6118        let req = Request::builder()
6119            .uri("/v2/node/sync-status?from=1")
6120            .body(axum::body::Body::empty())
6121            .unwrap();
6122        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
6123        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
6124    }
6125
6126    /// A POST body is refused by the `Json` extractor, whose 413, 415 and 422 the envelope layer
6127    /// turns into the same 400 a bad query parameter gets. A query string is refused too, since the
6128    /// handler would ignore it.
6129    #[tokio::test]
6130    async fn v2_rejects_malformed_request_bodies() {
6131        let router = crate::router_v2(Arc::new(MockV2State), crate::OptionalModules::default());
6132        let ranges = "/v2/availability/leaf-ranges";
6133        let with_query = "/v2/availability/leaf-ranges?bogus=1";
6134        // Past axum's default body limit, which the extractor answers with 413.
6135        let oversized = format!(r#"{{"ranges": [{}]}}"#, " ".repeat(3 << 20));
6136        for (uri, content_type, body) in [
6137            (ranges, "application/json", oversized.as_str()),
6138            (ranges, "application/json", "not json"),
6139            (ranges, "application/json", r#"{"ranges": 1}"#),
6140            // Unknown fields are refused in a body as in a query string.
6141            (ranges, "application/json", r#"{"bogus": []}"#),
6142            (ranges, "text/plain", r#"{"ranges": []}"#),
6143            (with_query, "application/json", r#"{"ranges": []}"#),
6144        ] {
6145            let req = Request::builder()
6146                .method("POST")
6147                .uri(uri)
6148                .header(header::CONTENT_TYPE, content_type)
6149                .body(axum::body::Body::from(body.to_owned()))
6150                .unwrap();
6151            let resp = tower::ServiceExt::oneshot(router.clone(), req)
6152                .await
6153                .unwrap();
6154            let case = body.get(..40).unwrap_or(body);
6155            assert_eq!(resp.status(), StatusCode::BAD_REQUEST, "{uri} {case}");
6156            let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
6157                .await
6158                .unwrap();
6159            let envelope: serde_json::Value = serde_json::from_slice(&body).unwrap();
6160            assert_eq!(envelope["error"]["status"], "INVALID_ARGUMENT");
6161        }
6162    }
6163
6164    #[tokio::test]
6165    async fn v2_docs_uis_serve_html() {
6166        for uri in [
6167            routes::v2::SWAGGER_ROUTE,
6168            routes::v2::SWAGGER_SLASH_ROUTE,
6169            routes::v2::SCALAR_ROUTE,
6170        ] {
6171            let req = Request::builder()
6172                .uri(uri)
6173                .body(axum::body::Body::empty())
6174                .unwrap();
6175            let resp = tower::ServiceExt::oneshot(router_v2_docs(), req)
6176                .await
6177                .unwrap();
6178            assert_eq!(resp.status(), StatusCode::OK, "{uri} should serve docs");
6179            let content_type = resp
6180                .headers()
6181                .get(axum::http::header::CONTENT_TYPE)
6182                .and_then(|value| value.to_str().ok())
6183                .unwrap_or_default()
6184                .to_string();
6185            assert!(content_type.contains("text/html"), "{uri}: {content_type}");
6186            assert!(
6187                body_string(resp)
6188                    .await
6189                    .contains(routes::v2::OPENAPI_SPEC_ROUTE)
6190            );
6191        }
6192    }
6193
6194    /// `submit` and the bulk `catchup` routes take bodies over axum's 2 MiB `Bytes` default, and
6195    /// the chain's `max_block_size` is what decides whether a transaction is too big, so the body
6196    /// has to reach the handler. Drives the real `serve_router`.
6197    #[tokio::test]
6198    async fn served_router_admits_bodies_over_the_axum_default() {
6199        const LEN: usize = 3 * 1024 * 1024;
6200        let router = Router::new().route(
6201            "/v1/submit/submit",
6202            axum::routing::post(|body: Bytes| async move { body.len().to_string() }),
6203        );
6204        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6205        let addr = listener.local_addr().unwrap();
6206        tokio::spawn(crate::serve_router(listener, "test", router, None));
6207
6208        let mut sock = tokio::net::TcpStream::connect(addr).await.unwrap();
6209        sock.write_all(
6210            format!(
6211                "POST /v1/submit/submit HTTP/1.1\r\nHost: localhost\r\nContent-Type: \
6212                 application/octet-stream\r\nContent-Length: {LEN}\r\n\r\n"
6213            )
6214            .as_bytes(),
6215        )
6216        .await
6217        .unwrap();
6218        // The server may reset the connection mid-write if it rejects the body early; the asserts
6219        // below report that legibly.
6220        let _ = sock.write_all(&vec![b'x'; LEN]).await;
6221
6222        let mut resp = String::new();
6223        let read = async {
6224            loop {
6225                let mut buf = [0u8; 1024];
6226                // A read error is end-of-input too: the server resets the connection after an
6227                // early rejection, and whatever arrived before the reset belongs in the asserts.
6228                let Ok(n) = sock.read(&mut buf).await else {
6229                    break;
6230                };
6231                if n == 0 {
6232                    break;
6233                }
6234                resp.push_str(&String::from_utf8_lossy(&buf[..n]));
6235                if resp.contains(&LEN.to_string()) || resp.len() > 4096 {
6236                    break;
6237                }
6238            }
6239        };
6240        tokio::time::timeout(std::time::Duration::from_secs(30), read)
6241            .await
6242            .expect("server never answered; a 413 would produce no matching body");
6243        assert!(resp.starts_with("HTTP/1.1 200 OK"), "{resp}");
6244        let body = resp.split_once("\r\n\r\n").map_or("", |(_, body)| body);
6245        assert!(
6246            body.contains(&LEN.to_string()),
6247            "handler saw a truncated body: {resp}"
6248        );
6249    }
6250
6251    /// The control: the same handler without the layer, pinning that the test above can fail.
6252    #[tokio::test]
6253    async fn axum_default_body_limit_rejects_the_same_request() {
6254        let router = Router::new().route(
6255            "/v1/submit/submit",
6256            axum::routing::post(|body: Bytes| async move { body.len().to_string() }),
6257        );
6258        let req = Request::builder()
6259            .method("POST")
6260            .uri("/v1/submit/submit")
6261            .header(header::CONTENT_TYPE, "application/octet-stream")
6262            .body(axum::body::Body::from(vec![b'x'; 3 * 1024 * 1024]))
6263            .unwrap();
6264        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
6265        assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
6266    }
6267
6268    #[tokio::test]
6269    async fn max_connections_limits_in_flight_requests() {
6270        let router = Router::new().route(
6271            "/slow",
6272            get(|| async {
6273                tokio::time::sleep(std::time::Duration::from_secs(2)).await;
6274                "ok"
6275            }),
6276        );
6277        let router = crate::apply_connection_limit(router, 2);
6278        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6279        let addr = listener.local_addr().unwrap();
6280        tokio::spawn(async move {
6281            axum::serve(listener, router).await.unwrap();
6282        });
6283
6284        async fn get_slow(addr: std::net::SocketAddr) -> (tokio::net::TcpStream, String) {
6285            use tokio::io::{AsyncReadExt, AsyncWriteExt};
6286            let mut sock = tokio::net::TcpStream::connect(addr).await.unwrap();
6287            sock.write_all(b"GET /slow HTTP/1.1\r\nHost: localhost\r\n\r\n")
6288                .await
6289                .unwrap();
6290            let mut buf = [0u8; 32];
6291            let n = sock.read(&mut buf).await.unwrap();
6292            (sock, String::from_utf8_lossy(&buf[..n]).to_string())
6293        }
6294
6295        use tokio::io::AsyncWriteExt;
6296        let mut s1 = tokio::net::TcpStream::connect(addr).await.unwrap();
6297        s1.write_all(b"GET /slow HTTP/1.1\r\nHost: localhost\r\n\r\n")
6298            .await
6299            .unwrap();
6300        let mut s2 = tokio::net::TcpStream::connect(addr).await.unwrap();
6301        s2.write_all(b"GET /slow HTTP/1.1\r\nHost: localhost\r\n\r\n")
6302            .await
6303            .unwrap();
6304        // Both requests in flight (each sleeps 2s); the third must be shed.
6305        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
6306        let (_s3, status) = get_slow(addr).await;
6307        assert!(
6308            status.contains("429"),
6309            "third request must be limited: {status}"
6310        );
6311    }
6312
6313    /// Regression test: the docs routes must exist in the app a serve mode actually builds, not
6314    /// only in `create_router_v1` (which the serve modes don't call). Assembles a router the way
6315    /// `serve_axum_status` does, wrapped in the same top-level routes and legacy-URI rewrite
6316    /// layers as `serve_router`, and checks the docs are reachable and the spec reflects only
6317    /// the mounted modules.
6318    #[tokio::test]
6319    async fn serve_mode_assembly_serves_v1_docs() {
6320        let api_router =
6321            router_status(Arc::new(MockState)).merge(router_state_signature(Arc::new(MockState)));
6322        let router = with_top_level_routes(finish_v1_docs(api_router));
6323        let app = tower::Layer::layer(
6324            &tower::util::MapRequestLayer::new(rewrite_legacy_uri),
6325            router,
6326        );
6327
6328        let get = |uri: &'static str| {
6329            let app = app.clone();
6330            async move {
6331                let req = Request::builder()
6332                    .uri(uri)
6333                    .body(axum::body::Body::empty())
6334                    .unwrap();
6335                tower::ServiceExt::oneshot(app, req).await.unwrap()
6336            }
6337        };
6338
6339        let resp = get("/").await;
6340        assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
6341        assert_eq!(
6342            resp.headers().get(axum::http::header::LOCATION).unwrap(),
6343            "/v1"
6344        );
6345
6346        let resp = get("/v1").await;
6347        assert_eq!(resp.status(), StatusCode::OK, "/v1 must serve the docs UI");
6348
6349        let resp = get(routes::v1::OPENAPI_SPEC_ROUTE).await;
6350        assert_eq!(resp.status(), StatusCode::OK);
6351        let spec: serde_json::Value =
6352            serde_json::from_str(&body_string(resp).await).expect("valid JSON");
6353        let paths = spec["paths"].as_object().expect("spec has paths");
6354        assert!(paths.contains_key(routes::v1::STATUS_BLOCK_HEIGHT_ROUTE));
6355        assert!(
6356            !paths.contains_key(routes::v1::LEAF_BY_HEIGHT_ROUTE),
6357            "spec must only document the modules this mode mounts"
6358        );
6359
6360        // Every `{name}` template segment must be declared as a path parameter, or Swagger's
6361        // try-it-out cannot fill the URL.
6362        let params = &paths[routes::v1::STATE_SIGNATURE_BLOCK_ROUTE]["get"]["parameters"];
6363        assert_eq!(
6364            params[0]["name"], "height",
6365            "template parameters must be declared: {params}"
6366        );
6367        assert_eq!(params[0]["in"], "path");
6368        assert_eq!(params[0]["required"], true);
6369        assert_eq!(params[0]["schema"]["type"], "integer");
6370    }
6371
6372    /// Multi-segment templates declare one parameter per `{name}`, in template order.
6373    #[tokio::test]
6374    async fn v1_spec_declares_all_template_parameters() {
6375        let router = create_router_v1(MockState);
6376        let req = Request::builder()
6377            .uri(routes::v1::OPENAPI_SPEC_ROUTE)
6378            .body(axum::body::Body::empty())
6379            .unwrap();
6380        let resp = tower::ServiceExt::oneshot(router, req).await.unwrap();
6381        let spec: serde_json::Value =
6382            serde_json::from_str(&body_string(resp).await).expect("valid JSON");
6383        let paths = spec["paths"].as_object().expect("spec has paths");
6384        for (path, item) in paths {
6385            let names: Vec<&str> = path
6386                .split('/')
6387                .filter_map(|s| s.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
6388                .collect();
6389            for op in item.as_object().unwrap().values() {
6390                let declared: Vec<&str> = op["parameters"]
6391                    .as_array()
6392                    .map(|ps| {
6393                        ps.iter()
6394                            .filter(|p| p["in"] == "path")
6395                            .map(|p| p["name"].as_str().unwrap())
6396                            .collect()
6397                    })
6398                    .unwrap_or_default();
6399                assert_eq!(
6400                    declared, names,
6401                    "path {path} must declare its template params"
6402                );
6403            }
6404        }
6405
6406        // Numeric segments are typed integer, hash/key-like segments string.
6407        let key_path = &paths[routes::v1::REWARD_STATE_PATH_BY_HEIGHT_ROUTE]["get"]["parameters"];
6408        assert_eq!(key_path[0]["name"], "height");
6409        assert_eq!(key_path[0]["schema"]["type"], "integer");
6410        assert_eq!(key_path[1]["name"], "key");
6411        assert_eq!(key_path[1]["schema"]["type"], "string");
6412
6413        // Operations are grouped by module tag.
6414        assert_eq!(
6415            paths[routes::v1::LEAF_BY_HEIGHT_ROUTE]["get"]["tags"][0],
6416            "availability"
6417        );
6418        assert!(
6419            spec["tags"]
6420                .as_array()
6421                .expect("spec has tags")
6422                .iter()
6423                .any(|t| t["name"] == "status")
6424        );
6425    }
6426}