1pub 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
40type 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
56pub(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#[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 message: String,
83 status: u16,
84}
85
86impl ErrorResponse {
87 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
124fn encode_response<T: Serialize>(headers: &HeaderMap, value: T) -> Response {
131 http_wire::encode_ok::<ErrorResponse, _>(headers, value)
132}
133
134pub(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
164struct 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#[derive(Clone)]
190pub(crate) struct RequestLimit(pub(crate) Arc<Semaphore>);
191
192pub(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
207fn swagger_html(spec_route: &str) -> Html<String> {
209 Html(include_str!("../templates/swagger.html").replace("{{OPENAPI_SPEC_ROUTE}}", spec_route))
210}
211
212async fn redirect_to_docs() -> axum::response::Redirect {
214 axum::response::Redirect::temporary("/v1")
215}
216
217pub(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
258pub(crate) fn with_top_level_routes(router: Router) -> Router {
262 router
263 .route("/", get(redirect_to_docs))
264 .route("/healthcheck", get(healthcheck))
265 .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
277async fn module_healthcheck(headers: HeaderMap) -> Response {
283 module_healthcheck_response(&headers)
284}
285
286async 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 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 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 .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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
3470pub 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 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
3518pub(crate) async fn v2_error_envelope(req: Request, next: axum::middleware::Next) -> Response {
3525 const MAX_REJECTION_BODY: usize = 8 * 1024;
3527
3528 let response = next.run(req).await;
3529
3530 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
3562pub(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
3574pub(crate) const V2_OPENAPI: &str =
3576 include_str!(concat!(env!("OUT_DIR"), "/espresso.api.v2.openapi.json"));
3577
3578pub 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
3604pub(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
3622pub 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 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 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
3673fn 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
3721fn 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 #[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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[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 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 #[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 #[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 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 #[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 #[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 assert!(
6013 !schemas.contains_key("NamespacesEntry"),
6014 "a map entry leaked into the published schemas"
6015 );
6016 }
6017
6018 #[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 #[tokio::test]
6076 async fn v2_rejects_malformed_query_parameters() {
6077 let router = crate::router_v2(Arc::new(MockV2State), crate::OptionalModules::default());
6078 let count = "/v2/node/transaction-count";
6080 for query in [
6081 "from=abc",
6082 "from=-1",
6083 "from=",
6084 "from=1.0",
6085 "from=18446744073709551616",
6089 "from=1&from=2",
6090 "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 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 #[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 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 (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 #[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 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 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 #[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 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 #[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 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 #[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 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 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}