use eth2::types::builder_bid::SignedBuilderBid; use eth2::types::{ AbstractExecPayload, BlindedPayload, EthSpec, ExecutionBlockHash, ExecutionPayload, ForkVersionedResponse, PublicKeyBytes, SignedBeaconBlock, SignedValidatorRegistrationData, Slot, }; pub use eth2::Error; use eth2::{ok_or_error, StatusCode}; use reqwest::{IntoUrl, Response}; use sensitive_url::SensitiveUrl; use serde::de::DeserializeOwned; use serde::Serialize; use std::time::Duration; pub const DEFAULT_TIMEOUT_MILLIS: u64 = 15000; /// This timeout is in accordance with v0.2.0 of the [builder specs](https://github.com/flashbots/mev-boost/pull/20). pub const DEFAULT_GET_HEADER_TIMEOUT_MILLIS: u64 = 1000; #[derive(Clone)] pub struct Timeouts { get_header: Duration, post_validators: Duration, post_blinded_blocks: Duration, get_builder_status: Duration, } impl Default for Timeouts { fn default() -> Self { Self { get_header: Duration::from_millis(DEFAULT_GET_HEADER_TIMEOUT_MILLIS), post_validators: Duration::from_millis(DEFAULT_TIMEOUT_MILLIS), post_blinded_blocks: Duration::from_millis(DEFAULT_TIMEOUT_MILLIS), get_builder_status: Duration::from_millis(DEFAULT_TIMEOUT_MILLIS), } } } #[derive(Clone)] pub struct BuilderHttpClient { client: reqwest::Client, server: SensitiveUrl, timeouts: Timeouts, } impl BuilderHttpClient { pub fn new(server: SensitiveUrl) -> Result { Ok(Self { client: reqwest::Client::new(), server, timeouts: Timeouts::default(), }) } pub fn new_with_timeouts(server: SensitiveUrl, timeouts: Timeouts) -> Result { Ok(Self { client: reqwest::Client::new(), server, timeouts, }) } async fn get_with_timeout( &self, url: U, timeout: Duration, ) -> Result { self.get_response_with_timeout(url, Some(timeout)) .await? .json() .await .map_err(Error::Reqwest) } /// Perform a HTTP GET request, returning the `Response` for further processing. async fn get_response_with_timeout( &self, url: U, timeout: Option, ) -> Result { let mut builder = self.client.get(url); if let Some(timeout) = timeout { builder = builder.timeout(timeout); } let response = builder.send().await.map_err(Error::Reqwest)?; ok_or_error(response).await } /// Generic POST function supporting arbitrary responses and timeouts. async fn post_generic( &self, url: U, body: &T, timeout: Option, ) -> Result { let mut builder = self.client.post(url); if let Some(timeout) = timeout { builder = builder.timeout(timeout); } let response = builder.json(body).send().await?; ok_or_error(response).await } async fn post_with_raw_response( &self, url: U, body: &T, timeout: Option, ) -> Result { let mut builder = self.client.post(url); if let Some(timeout) = timeout { builder = builder.timeout(timeout); } let response = builder.json(body).send().await.map_err(Error::Reqwest)?; ok_or_error(response).await } /// `POST /eth/v1/builder/validators` pub async fn post_builder_validators( &self, validator: &[SignedValidatorRegistrationData], ) -> Result<(), Error> { let mut path = self.server.full.clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("builder") .push("validators"); self.post_generic(path, &validator, Some(self.timeouts.post_validators)) .await?; Ok(()) } /// `POST /eth/v1/builder/blinded_blocks` pub async fn post_builder_blinded_blocks( &self, blinded_block: &SignedBeaconBlock>, ) -> Result>, Error> { let mut path = self.server.full.clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("builder") .push("blinded_blocks"); Ok(self .post_with_raw_response( path, &blinded_block, Some(self.timeouts.post_blinded_blocks), ) .await? .json() .await?) } /// `GET /eth/v1/builder/header` pub async fn get_builder_header>( &self, slot: Slot, parent_hash: ExecutionBlockHash, pubkey: &PublicKeyBytes, ) -> Result>>, Error> { let mut path = self.server.full.clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("builder") .push("header") .push(slot.to_string().as_str()) .push(format!("{parent_hash:?}").as_str()) .push(pubkey.as_hex_string().as_str()); let resp = self.get_with_timeout(path, self.timeouts.get_header).await; if matches!(resp, Err(Error::StatusCode(StatusCode::NO_CONTENT))) { Ok(None) } else { resp.map(Some) } } /// `GET /eth/v1/builder/status` pub async fn get_builder_status(&self) -> Result<(), Error> { let mut path = self.server.full.clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("builder") .push("status"); self.get_with_timeout(path, self.timeouts.get_builder_status) .await } }