From 520b58091ec9bbe18661edef5bd2425c74174f38 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Sun, 5 Oct 2025 19:10:23 +0200 Subject: [PATCH 1/2] feat(anvil): add basic Beacon API support - New Beacon response and error types - Updated the server router to handle Beacon REST API calls. - Implemented tests for the Beacon API endpoint to validate blob sidecar retrieval. --- Cargo.lock | 2 + crates/anvil/Cargo.toml | 2 + crates/anvil/src/eth/beacon/error.rs | 130 ++++++++++++++++ crates/anvil/src/eth/beacon/mod.rs | 10 ++ crates/anvil/src/eth/beacon/response.rs | 75 +++++++++ crates/anvil/src/eth/mod.rs | 1 + crates/anvil/src/server/beacon_handler.rs | 55 +++++++ crates/anvil/src/server/mod.rs | 28 +++- crates/anvil/tests/it/eip4844.rs | 176 ++++++++++++++++++++++ 9 files changed, 475 insertions(+), 4 deletions(-) create mode 100644 crates/anvil/src/eth/beacon/error.rs create mode 100644 crates/anvil/src/eth/beacon/mod.rs create mode 100644 crates/anvil/src/eth/beacon/response.rs create mode 100644 crates/anvil/src/server/beacon_handler.rs diff --git a/Cargo.lock b/Cargo.lock index 90689810bfc4d..a914d4b6936ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,6 +1089,7 @@ dependencies = [ "alloy-pubsub", "alloy-rlp", "alloy-rpc-types", + "alloy-rpc-types-beacon", "alloy-serde", "alloy-signer", "alloy-signer-local", @@ -1122,6 +1123,7 @@ dependencies = [ "parking_lot", "rand 0.8.5", "rand 0.9.2", + "reqwest", "revm", "revm-inspectors", "serde", diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index cb10c4c268dbb..134f836c81f6e 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -44,6 +44,7 @@ alloy-signer-local = { workspace = true, features = ["mnemonic"] } alloy-sol-types = { workspace = true, features = ["std"] } alloy-dyn-abi = { workspace = true, features = ["std", "eip712"] } alloy-rpc-types = { workspace = true, features = ["anvil", "trace", "txpool"] } +alloy-rpc-types-beacon.workspace = true alloy-serde.workspace = true alloy-provider = { workspace = true, features = [ "reqwest", @@ -111,6 +112,7 @@ alloy-provider = { workspace = true, features = ["txpool-api"] } alloy-pubsub.workspace = true alloy-eip5792.workspace = true rand.workspace = true +reqwest.workspace = true foundry-test-utils.workspace = true tokio = { workspace = true, features = ["full"] } diff --git a/crates/anvil/src/eth/beacon/error.rs b/crates/anvil/src/eth/beacon/error.rs new file mode 100644 index 0000000000000..6476db7d064bf --- /dev/null +++ b/crates/anvil/src/eth/beacon/error.rs @@ -0,0 +1,130 @@ +//! Beacon API error types + +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Cow, + fmt::{self, Display}, +}; + +/// Represents a Beacon API error response +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BeaconError { + /// HTTP status code + #[serde(skip)] + pub status_code: u16, + /// Error code + pub code: BeaconErrorCode, + /// Error message + pub message: Cow<'static, str>, +} + +impl BeaconError { + /// Creates a new beacon error with the given code + pub fn new(code: BeaconErrorCode, message: impl Into>) -> Self { + let status_code = code.status_code(); + Self { status_code, code, message: message.into() } + } + + /// Helper function to create a 400 Bad Request error for invalid block ID + pub fn invalid_block_id(block_id: impl Display) -> Self { + Self::new(BeaconErrorCode::BadRequest, format!("Invalid block ID: {block_id}")) + } + + /// Helper function to create a 404 Not Found error for block not found + pub fn block_not_found() -> Self { + Self::new(BeaconErrorCode::NotFound, "Block not found") + } + + /// Helper function to create a 500 Internal Server Error + pub fn internal_error() -> Self { + Self::new(BeaconErrorCode::InternalError, "Internal server error") + } + + /// Helper function to create a 500 Internal Server Error with the given details + pub fn internal_error_with_details(error: impl Display) -> Self { + Self::new(BeaconErrorCode::InternalError, format!("Internal server error: {error}")) + } + + /// Converts to an Axum response + pub fn into_response(self) -> Response { + let status = + StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + + ( + status, + Json(serde_json::json!({ + "code": self.code as u16, + "message": self.message, + })), + ) + .into_response() + } +} + +impl fmt::Display for BeaconError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.code.as_str(), self.message) + } +} + +impl std::error::Error for BeaconError {} + +impl IntoResponse for BeaconError { + fn into_response(self) -> Response { + Self::into_response(self) + } +} + +/// Beacon API error codes following the beacon chain specification +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[repr(u16)] +pub enum BeaconErrorCode { + BadRequest = 400, + NotFound = 404, + InternalError = 500, +} + +impl BeaconErrorCode { + /// Returns the HTTP status code for this error + pub const fn status_code(&self) -> u16 { + *self as u16 + } + + /// Returns a string representation of the error code + pub const fn as_str(&self) -> &'static str { + match self { + Self::BadRequest => "Bad Request", + Self::NotFound => "Not Found", + Self::InternalError => "Internal Server Error", + } + } +} + +impl fmt::Display for BeaconErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_beacon_error_codes() { + assert_eq!(BeaconErrorCode::BadRequest.status_code(), 400); + assert_eq!(BeaconErrorCode::NotFound.status_code(), 404); + assert_eq!(BeaconErrorCode::InternalError.status_code(), 500); + } + + #[test] + fn test_beacon_error_display() { + let err = BeaconError::invalid_block_id("current"); + assert_eq!(err.to_string(), "Bad Request: Invalid block ID: current"); + } +} diff --git a/crates/anvil/src/eth/beacon/mod.rs b/crates/anvil/src/eth/beacon/mod.rs new file mode 100644 index 0000000000000..4b3b4eda2abbc --- /dev/null +++ b/crates/anvil/src/eth/beacon/mod.rs @@ -0,0 +1,10 @@ +//! Beacon API types and utilities for Anvil +//! +//! This module provides types and utilities for implementing Beacon API endpoints +//! in Anvil, allowing testing of blob-based transactions with standard beacon chain APIs. + +pub mod error; +pub mod response; + +pub use error::BeaconError; +pub use response::{BeaconResponse, BeaconResult}; diff --git a/crates/anvil/src/eth/beacon/response.rs b/crates/anvil/src/eth/beacon/response.rs new file mode 100644 index 0000000000000..7e2fc490a16a2 --- /dev/null +++ b/crates/anvil/src/eth/beacon/response.rs @@ -0,0 +1,75 @@ +//! Beacon API response types + +use super::error::BeaconError; +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde::{Deserialize, Serialize}; + +/// Result type for beacon API operations +pub type BeaconResult = Result; + +/// Generic Beacon API response wrapper +/// +/// This follows the beacon chain API specification where responses include +/// the actual data plus metadata about execution optimism and finalization. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BeaconResponse { + /// The response data + pub data: T, + /// Whether the response references an unverified execution payload + /// + /// For Anvil, this is always `false` since there's no real consensus layer + #[serde(default, skip_serializing_if = "Option::is_none")] + pub execution_optimistic: Option, + /// Whether the response references finalized history + /// + /// For Anvil, this is always `false` since there's no real consensus layer + #[serde(default, skip_serializing_if = "Option::is_none")] + pub finalized: Option, +} + +impl BeaconResponse { + /// Creates a new beacon response with the given data + /// + /// For Anvil context, `execution_optimistic` and `finalized` are always `false` + pub fn new(data: T) -> Self { + Self { data, execution_optimistic: None, finalized: None } + } + + /// Creates a beacon response with custom execution_optimistic and finalized flags + pub fn with_flags(data: T, execution_optimistic: bool, finalized: bool) -> Self { + Self { data, execution_optimistic: Some(execution_optimistic), finalized: Some(finalized) } + } +} + +impl IntoResponse for BeaconResponse { + fn into_response(self) -> Response { + (StatusCode::OK, Json(self)).into_response() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_beacon_response_defaults() { + let response = BeaconResponse::new("test data"); + assert_eq!(response.data, "test data"); + assert!(response.execution_optimistic.is_none()); + assert!(response.finalized.is_none()); + } + + #[test] + fn test_beacon_response_serialization() { + let response = BeaconResponse::with_flags(vec![1, 2, 3], false, false); + let json = serde_json::to_value(&response).unwrap(); + + assert_eq!(json["data"], serde_json::json!([1, 2, 3])); + assert_eq!(json["execution_optimistic"], false); + assert_eq!(json["finalized"], false); + } +} diff --git a/crates/anvil/src/eth/mod.rs b/crates/anvil/src/eth/mod.rs index 393a9ff213306..f8d1509d64b83 100644 --- a/crates/anvil/src/eth/mod.rs +++ b/crates/anvil/src/eth/mod.rs @@ -1,4 +1,5 @@ pub mod api; +pub mod beacon; pub mod otterscan; pub mod sign; pub use api::EthApi; diff --git a/crates/anvil/src/server/beacon_handler.rs b/crates/anvil/src/server/beacon_handler.rs new file mode 100644 index 0000000000000..289b678020449 --- /dev/null +++ b/crates/anvil/src/server/beacon_handler.rs @@ -0,0 +1,55 @@ +use crate::eth::{ + EthApi, + beacon::{BeaconError, BeaconResponse}, +}; +use alloy_eips::BlockId; +use alloy_rpc_types_beacon::{header::Header, sidecar::BlobData}; +use axum::{ + extract::{Path, Query, State}, + response::{IntoResponse, Response}, +}; +use std::{collections::HashMap, str::FromStr as _}; + +/// Handles incoming Beacon API requests for blob sidecars +/// +/// GET /eth/v1/beacon/blob_sidecars/{block_id} +pub async fn handle_get_blob_sidecars( + State(api): State, + Path(block_id): Path, + Query(params): Query>, +) -> Response { + // Parse block_id from path parameter + let Ok(block_id) = BlockId::from_str(&block_id) else { + return BeaconError::invalid_block_id(block_id).into_response(); + }; + + // Parse indices from query parameters + // Supports both comma-separated (?indices=1,2,3) and repeated parameters (?indices=1&indices=2) + let indices: Vec = params + .get("indices") + .map(|s| s.split(',').filter_map(|idx| idx.trim().parse::().ok()).collect()) + .unwrap_or_default(); + + // Get the blob sidecars using existing EthApi logic + match api.anvil_get_blob_sidecars_by_block_id(block_id) { + Ok(Some(sidecar)) => BeaconResponse::with_flags( + sidecar + .into_iter() + .filter(|blob_item| indices.is_empty() || indices.contains(&blob_item.index)) + .map(|blob_item| BlobData { + index: blob_item.index, + blob: blob_item.blob, + kzg_commitment: blob_item.kzg_commitment, + kzg_proof: blob_item.kzg_proof, + signed_block_header: Header::default(), // Not available in Anvil + kzg_commitment_inclusion_proof: vec![], // Not available in Anvil + }) + .collect::>(), + false, // Not available in Anvil + false, // Not available in Anvil + ) + .into_response(), + Ok(None) => BeaconError::block_not_found().into_response(), + Err(_) => BeaconError::internal_error().into_response(), + } +} diff --git a/crates/anvil/src/server/mod.rs b/crates/anvil/src/server/mod.rs index 18e3edd8161ea..46e17acfb08df 100644 --- a/crates/anvil/src/server/mod.rs +++ b/crates/anvil/src/server/mod.rs @@ -2,12 +2,13 @@ use crate::{EthApi, IpcTask}; use anvil_server::{ServerConfig, ipc::IpcEndpoint}; -use axum::Router; +use axum::{Router, routing::get}; use futures::StreamExt; use handler::{HttpEthRpcHandler, PubSubEthRpcHandler}; use std::{io, net::SocketAddr, pin::pin}; use tokio::net::TcpListener; +mod beacon_handler; pub mod error; mod handler; @@ -33,11 +34,30 @@ pub async fn serve_on( axum::serve(tcp_listener, router(api, config).into_make_service()).await } -/// Configures an [`axum::Router`] that handles [`EthApi`] related JSON-RPC calls via HTTP and WS. +/// Configures an [`axum::Router`] that handles [`EthApi`] related JSON-RPC calls via HTTP and WS, +/// and Beacon REST API calls. pub fn router(api: EthApi, config: ServerConfig) -> Router { let http = HttpEthRpcHandler::new(api.clone()); - let ws = PubSubEthRpcHandler::new(api); - anvil_server::http_ws_router(config, http, ws) + let ws = PubSubEthRpcHandler::new(api.clone()); + + // JSON-RPC router + let rpc_router = anvil_server::http_ws_router(config, http, ws); + + // Beacon REST API router + let beacon_router = beacon_router(api); + + // Merge the routers + rpc_router.merge(beacon_router) +} + +/// Configures an [`axum::Router`] that handles Beacon REST API calls. +fn beacon_router(api: EthApi) -> Router { + Router::new() + .route( + "/eth/v1/beacon/blob_sidecars/{block_id}", + get(beacon_handler::handle_get_blob_sidecars), + ) + .with_state(api) } /// Launches an ipc server at the given path in a new task diff --git a/crates/anvil/tests/it/eip4844.rs b/crates/anvil/tests/it/eip4844.rs index f41771b8d32fb..2b1d11f4dbb40 100644 --- a/crates/anvil/tests/it/eip4844.rs +++ b/crates/anvil/tests/it/eip4844.rs @@ -449,3 +449,179 @@ async fn can_get_blobs_by_tx_hash() { let blobs = api.anvil_get_blob_by_tx_hash(hash).unwrap().unwrap(); assert_eq!(blobs, sidecar.blobs); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_beacon_api_get_blob_sidecars() { + let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Cancun.into())); + let (api, handle) = spawn(node_config).await; + + // Disable auto-mining so we can include multiple transactions in the same block + api.anvil_set_auto_mine(false).await.unwrap(); + + let wallets = handle.dev_wallets().collect::>(); + let from = wallets[0].address(); + let to = wallets[1].address(); + + let provider = http_provider(&handle.http_endpoint()); + + let eip1559_est = provider.estimate_eip1559_fees().await.unwrap(); + let gas_price = provider.get_gas_price().await.unwrap(); + + // Create multiple blob transactions to be included in the same block + let blob_data = vec![ + b"Hello Beacon API - Blob 1", + b"Hello Beacon API - Blob 2", + b"Hello Beacon API - Blob 3", + ]; + + let mut pending_txs = Vec::new(); + + // Send all transactions without waiting for receipts + for (i, data) in blob_data.iter().enumerate() { + let sidecar: SidecarBuilder = SidecarBuilder::from_slice(data.as_slice()); + let sidecar = sidecar.build().unwrap(); + + let tx = TransactionRequest::default() + .with_from(from) + .with_to(to) + .with_nonce(i as u64) + .with_max_fee_per_blob_gas(gas_price + 1) + .with_max_fee_per_gas(eip1559_est.max_fee_per_gas) + .with_max_priority_fee_per_gas(eip1559_est.max_priority_fee_per_gas) + .with_blob_sidecar(sidecar) + .value(U256::from(100)); + + let mut tx = WithOtherFields::new(tx); + tx.populate_blob_hashes(); + + let pending = provider.send_transaction(tx).await.unwrap(); + pending_txs.push(pending); + } + + // Mine a block to include all transactions + api.evm_mine(None).await.unwrap(); + + // Get receipts for all transactions + let mut receipts = Vec::new(); + for pending in pending_txs { + let receipt = pending.get_receipt().await.unwrap(); + receipts.push(receipt); + } + + // Verify all transactions were included in the same block + let block_number = receipts[0].block_number.unwrap(); + for (i, receipt) in receipts.iter().enumerate() { + assert_eq!( + receipt.block_number.unwrap(), + block_number, + "Transaction {} was not included in block {}", + i, + block_number + ); + } + + // Test Beacon API endpoint using HTTP client + let client = reqwest::Client::new(); + let url = format!("{}/eth/v1/beacon/blob_sidecars/{}", handle.http_endpoint(), block_number); + + let response = client.get(&url).send().await.unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::OK); + + let body: serde_json::Value = response.json().await.unwrap(); + + // Verify response structure + assert!(body["data"].is_array()); + assert!(body["execution_optimistic"].is_boolean()); + assert!(body["finalized"].is_boolean()); + + // Verify we have blob data from all transactions + let blobs = body["data"].as_array().unwrap(); + assert_eq!(blobs.len(), 3, "Expected 3 blob sidecars from 3 transactions"); + + // Verify blob structure for each blob + for (i, blob) in blobs.iter().enumerate() { + assert!(blob["index"].is_string(), "Blob {} missing index", i); + assert!(blob["blob"].is_string(), "Blob {} missing blob data", i); + assert!(blob["kzg_commitment"].is_string(), "Blob {} missing kzg_commitment", i); + assert!(blob["kzg_proof"].is_string(), "Blob {} missing kzg_proof", i); + } + + // Test filtering with indices query parameter - single index + let url = format!( + "{}/eth/v1/beacon/blob_sidecars/{}?indices=1", + handle.http_endpoint(), + block_number + ); + let response = client.get(&url).send().await.unwrap(); + let status = response.status(); + if status != reqwest::StatusCode::OK { + let error_body = response.text().await.unwrap(); + panic!("Expected OK status, got {}: {}", status, error_body); + } + let body: serde_json::Value = response.json().await.unwrap(); + let filtered_blobs = body["data"].as_array().unwrap(); + assert_eq!(filtered_blobs.len(), 1, "Expected 1 blob sidecar when filtering by indices=1"); + assert_eq!(filtered_blobs[0]["index"].as_str().unwrap(), "1"); + + // Test filtering with indices query parameter - multiple indices (comma-separated) + let url = format!( + "{}/eth/v1/beacon/blob_sidecars/{}?indices=0,2", + handle.http_endpoint(), + block_number + ); + let response = client.get(&url).send().await.unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::OK); + let body: serde_json::Value = response.json().await.unwrap(); + let filtered_blobs = body["data"].as_array().unwrap(); + assert_eq!(filtered_blobs.len(), 2, "Expected 2 blob sidecars when filtering by indices=0,2"); + let indices: Vec = + filtered_blobs.iter().map(|b| b["index"].as_str().unwrap().to_string()).collect(); + assert!(indices.contains(&"0".to_string()), "Expected index 0 in results"); + assert!(indices.contains(&"2".to_string()), "Expected index 2 in results"); + + // Test filtering with non-existent index + let url = format!( + "{}/eth/v1/beacon/blob_sidecars/{}?indices=99", + handle.http_endpoint(), + block_number + ); + let response = client.get(&url).send().await.unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::OK); + let body: serde_json::Value = response.json().await.unwrap(); + let filtered_blobs = body["data"].as_array().unwrap(); + assert_eq!( + filtered_blobs.len(), + 0, + "Expected 0 blob sidecars when filtering by non-existent index" + ); + + // Test with special block identifiers + let test_ids = vec!["latest", "finalized", "safe", "earliest"]; + for block_id in test_ids { + let url = format!("{}/eth/v1/beacon/blob_sidecars/{}", handle.http_endpoint(), block_id); + assert_eq!(client.get(&url).send().await.unwrap().status(), reqwest::StatusCode::OK); + } + assert_eq!( + client + .get(&format!("{}/eth/v1/beacon/blob_sidecars/pending", handle.http_endpoint())) + .send() + .await + .unwrap() + .status(), + reqwest::StatusCode::NOT_FOUND + ); + + // Test with hex block number + let url = format!( + "{}/eth/v1/beacon/blob_sidecars/0x{}", + handle.http_endpoint(), + format!("{:x}", block_number) + ); + let response = client.get(&url).send().await.unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::OK); + + // Test with non-existent block + let url = format!("{}/eth/v1/beacon/blob_sidecars/999999", handle.http_endpoint()); + let response = client.get(&url).send().await.unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND); +} From 8269c55a5d67e6d0ff1dfc30429409f415ab3014 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Sun, 5 Oct 2025 19:41:53 +0200 Subject: [PATCH 2/2] fix: clippy on test --- crates/anvil/tests/it/eip4844.rs | 38 +++++++++----------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/crates/anvil/tests/it/eip4844.rs b/crates/anvil/tests/it/eip4844.rs index 2b1d11f4dbb40..9f4ccf21fe962 100644 --- a/crates/anvil/tests/it/eip4844.rs +++ b/crates/anvil/tests/it/eip4844.rs @@ -468,11 +468,8 @@ async fn test_beacon_api_get_blob_sidecars() { let gas_price = provider.get_gas_price().await.unwrap(); // Create multiple blob transactions to be included in the same block - let blob_data = vec![ - b"Hello Beacon API - Blob 1", - b"Hello Beacon API - Blob 2", - b"Hello Beacon API - Blob 3", - ]; + let blob_data = + [b"Hello Beacon API - Blob 1", b"Hello Beacon API - Blob 2", b"Hello Beacon API - Blob 3"]; let mut pending_txs = Vec::new(); @@ -514,9 +511,7 @@ async fn test_beacon_api_get_blob_sidecars() { assert_eq!( receipt.block_number.unwrap(), block_number, - "Transaction {} was not included in block {}", - i, - block_number + "Transaction {i} was not included in block {block_number}" ); } @@ -540,10 +535,10 @@ async fn test_beacon_api_get_blob_sidecars() { // Verify blob structure for each blob for (i, blob) in blobs.iter().enumerate() { - assert!(blob["index"].is_string(), "Blob {} missing index", i); - assert!(blob["blob"].is_string(), "Blob {} missing blob data", i); - assert!(blob["kzg_commitment"].is_string(), "Blob {} missing kzg_commitment", i); - assert!(blob["kzg_proof"].is_string(), "Blob {} missing kzg_proof", i); + assert!(blob["index"].is_string(), "Blob {i} missing index"); + assert!(blob["blob"].is_string(), "Blob {i} missing blob data"); + assert!(blob["kzg_commitment"].is_string(), "Blob {i} missing kzg_commitment"); + assert!(blob["kzg_proof"].is_string(), "Blob {i} missing kzg_proof"); } // Test filtering with indices query parameter - single index @@ -556,7 +551,7 @@ async fn test_beacon_api_get_blob_sidecars() { let status = response.status(); if status != reqwest::StatusCode::OK { let error_body = response.text().await.unwrap(); - panic!("Expected OK status, got {}: {}", status, error_body); + panic!("Expected OK status, got {status}: {error_body}"); } let body: serde_json::Value = response.json().await.unwrap(); let filtered_blobs = body["data"].as_array().unwrap(); @@ -601,22 +596,11 @@ async fn test_beacon_api_get_blob_sidecars() { let url = format!("{}/eth/v1/beacon/blob_sidecars/{}", handle.http_endpoint(), block_id); assert_eq!(client.get(&url).send().await.unwrap().status(), reqwest::StatusCode::OK); } - assert_eq!( - client - .get(&format!("{}/eth/v1/beacon/blob_sidecars/pending", handle.http_endpoint())) - .send() - .await - .unwrap() - .status(), - reqwest::StatusCode::NOT_FOUND - ); + let url = format!("{}/eth/v1/beacon/blob_sidecars/pending", handle.http_endpoint()); + assert_eq!(client.get(&url).send().await.unwrap().status(), reqwest::StatusCode::NOT_FOUND); // Test with hex block number - let url = format!( - "{}/eth/v1/beacon/blob_sidecars/0x{}", - handle.http_endpoint(), - format!("{:x}", block_number) - ); + let url = format!("{}/eth/v1/beacon/blob_sidecars/0x{block_number:x}", handle.http_endpoint()); let response = client.get(&url).send().await.unwrap(); assert_eq!(response.status(), reqwest::StatusCode::OK);