1use 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 foundry_primitives::FoundryNetwork;
16use ssz::Encode;
17use std::{collections::HashMap, str::FromStr as _};
18
19pub async fn handle_get_blob_sidecars(
25 State(_api): State<EthApi<FoundryNetwork>>,
26 Path(_block_id): Path<String>,
27 Query(_params): Query<HashMap<String, String>>,
28) -> Response {
29 BeaconError::deprecated_endpoint_with_hint("Use `GET /eth/v1/beacon/blobs/{block_id}` instead.")
30 .into_response()
31}
32
33pub async fn handle_get_blobs(
37 headers: HeaderMap,
38 State(api): State<EthApi<FoundryNetwork>>,
39 Path(block_id): Path<String>,
40 Query(versioned_hashes): Query<HashMap<String, String>>,
41) -> Response {
42 let Ok(block_id) = BlockId::from_str(&block_id) else {
44 return BeaconError::invalid_block_id(block_id).into_response();
45 };
46
47 let versioned_hashes: Vec<B256> = match versioned_hashes.get("versioned_hashes") {
50 Some(s) => {
51 let mut hashes = Vec::new();
52 for hash in s.split(',') {
53 let hash = hash.trim();
54 if hash.is_empty() {
55 continue;
56 }
57 match B256::from_str(hash) {
58 Ok(h) => hashes.push(h),
59 Err(_) => {
60 return BeaconError::new(
61 super::error::BeaconErrorCode::BadRequest,
62 format!("Invalid versioned hash: {hash}"),
63 )
64 .into_response();
65 }
66 }
67 }
68 hashes
69 }
70 None => Vec::new(),
71 };
72
73 match api.anvil_get_blobs_by_block_id(block_id, versioned_hashes) {
75 Ok(Some(blobs)) => {
76 if must_be_ssz(&headers) {
77 blobs.as_ssz_bytes().into_response()
78 } else {
79 Json(GetBlobsResponse {
80 execution_optimistic: false,
81 finalized: false,
82 data: blobs,
83 })
84 .into_response()
85 }
86 }
87 Ok(None) => BeaconError::block_not_found().into_response(),
88 Err(_) => BeaconError::internal_error().into_response(),
89 }
90}
91
92pub async fn handle_get_genesis(State(api): State<EthApi<FoundryNetwork>>) -> Response {
98 match api.anvil_get_genesis_time() {
99 Ok(genesis_time) => Json(GenesisResponse {
100 data: GenesisData {
101 genesis_time,
102 genesis_validators_root: B256::ZERO,
103 genesis_fork_version: B32::ZERO,
104 },
105 })
106 .into_response(),
107 Err(_) => BeaconError::internal_error().into_response(),
108 }
109}
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use axum::http::HeaderValue;
114
115 fn header_map_with_accept(accept: &str) -> HeaderMap {
116 let mut headers = HeaderMap::new();
117 headers.insert(axum::http::header::ACCEPT, HeaderValue::from_str(accept).unwrap());
118 headers
119 }
120
121 #[test]
122 fn test_must_be_ssz() {
123 let test_cases = vec![
124 (None, false, "no Accept header"),
125 (Some("application/json"), false, "JSON only"),
126 (Some("application/octet-stream"), true, "octet-stream only"),
127 (Some("application/octet-stream;q=1.0,application/json;q=0.9"), true, "SSZ preferred"),
128 (
129 Some("application/json;q=1.0,application/octet-stream;q=0.9"),
130 false,
131 "JSON preferred",
132 ),
133 (Some("application/octet-stream;q=0.5,application/json;q=0.5"), false, "equal quality"),
134 (
135 Some("text/html;q=0.9, application/octet-stream;q=1.0, application/json;q=0.8"),
136 true,
137 "multiple types",
138 ),
139 (
140 Some("application/octet-stream ; q=1.0 , application/json ; q=0.9"),
141 true,
142 "whitespace handling",
143 ),
144 (Some("application/octet-stream, application/json;q=0.9"), true, "default quality"),
145 ];
146
147 for (accept_header, expected, description) in test_cases {
148 let headers = match accept_header {
149 None => HeaderMap::new(),
150 Some(header) => header_map_with_accept(header),
151 };
152 assert_eq!(
153 must_be_ssz(&headers),
154 expected,
155 "Test case '{}' failed: expected {}, got {}",
156 description,
157 expected,
158 !expected
159 );
160 }
161 }
162}