anvil/server/beacon/
handlers.rs1use super::{error::BeaconError, utils::must_be_ssz};
2use crate::eth::EthApi;
3use alloy_eips::BlockId;
4use alloy_primitives::{B256, aliases::B32};
5use alloy_rpc_types_beacon::{
6 genesis::{GenesisData, GenesisResponse},
7 sidecar::GetBlobsResponse,
8};
9use axum::{
10 Json,
11 extract::{Path, Query, State},
12 http::HeaderMap,
13 response::{IntoResponse, Response},
14};
15use ssz::Encode;
16use std::{collections::HashMap, str::FromStr as _};
17
18pub async fn handle_get_blob_sidecars(
24 State(_api): State<EthApi>,
25 Path(_block_id): Path<String>,
26 Query(_params): Query<HashMap<String, String>>,
27) -> Response {
28 BeaconError::deprecated_endpoint_with_hint("Use `GET /eth/v1/beacon/blobs/{block_id}` instead.")
29 .into_response()
30}
31
32pub async fn handle_get_blobs(
36 headers: HeaderMap,
37 State(api): State<EthApi>,
38 Path(block_id): Path<String>,
39 Query(versioned_hashes): Query<HashMap<String, String>>,
40) -> Response {
41 let Ok(block_id) = BlockId::from_str(&block_id) else {
43 return BeaconError::invalid_block_id(block_id).into_response();
44 };
45
46 let versioned_hashes: Vec<B256> = versioned_hashes
49 .get("versioned_hashes")
50 .map(|s| s.split(',').filter_map(|hash| B256::from_str(hash.trim()).ok()).collect())
51 .unwrap_or_default();
52
53 match api.anvil_get_blobs_by_block_id(block_id, versioned_hashes) {
55 Ok(Some(blobs)) => {
56 if must_be_ssz(&headers) {
57 blobs.as_ssz_bytes().into_response()
58 } else {
59 Json(GetBlobsResponse {
60 execution_optimistic: false,
61 finalized: false,
62 data: blobs,
63 })
64 .into_response()
65 }
66 }
67 Ok(None) => BeaconError::block_not_found().into_response(),
68 Err(_) => BeaconError::internal_error().into_response(),
69 }
70}
71
72pub async fn handle_get_genesis(State(api): State<EthApi>) -> Response {
78 match api.anvil_get_genesis_time() {
79 Ok(genesis_time) => Json(GenesisResponse {
80 data: GenesisData {
81 genesis_time,
82 genesis_validators_root: B256::ZERO,
83 genesis_fork_version: B32::ZERO,
84 },
85 })
86 .into_response(),
87 Err(_) => BeaconError::internal_error().into_response(),
88 }
89}
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use axum::http::HeaderValue;
94
95 fn header_map_with_accept(accept: &str) -> HeaderMap {
96 let mut headers = HeaderMap::new();
97 headers.insert(axum::http::header::ACCEPT, HeaderValue::from_str(accept).unwrap());
98 headers
99 }
100
101 #[test]
102 fn test_must_be_ssz() {
103 let test_cases = vec![
104 (None, false, "no Accept header"),
105 (Some("application/json"), false, "JSON only"),
106 (Some("application/octet-stream"), true, "octet-stream only"),
107 (Some("application/octet-stream;q=1.0,application/json;q=0.9"), true, "SSZ preferred"),
108 (
109 Some("application/json;q=1.0,application/octet-stream;q=0.9"),
110 false,
111 "JSON preferred",
112 ),
113 (Some("application/octet-stream;q=0.5,application/json;q=0.5"), false, "equal quality"),
114 (
115 Some("text/html;q=0.9, application/octet-stream;q=1.0, application/json;q=0.8"),
116 true,
117 "multiple types",
118 ),
119 (
120 Some("application/octet-stream ; q=1.0 , application/json ; q=0.9"),
121 true,
122 "whitespace handling",
123 ),
124 (Some("application/octet-stream, application/json;q=0.9"), true, "default quality"),
125 ];
126
127 for (accept_header, expected, description) in test_cases {
128 let headers = match accept_header {
129 None => HeaderMap::new(),
130 Some(header) => header_map_with_accept(header),
131 };
132 assert_eq!(
133 must_be_ssz(&headers),
134 expected,
135 "Test case '{}' failed: expected {}, got {}",
136 description,
137 expected,
138 !expected
139 );
140 }
141 }
142}