//! This crate provides two major things: //! //! 1. The types served by the `http_api` crate. //! 2. A wrapper around `reqwest` that forms a HTTP client, able of consuming the endpoints served //! by the `http_api` crate. //! //! Eventually it would be ideal to publish this crate on crates.io, however we have some local //! dependencies preventing this presently. #[cfg(feature = "lighthouse")] pub mod lighthouse; pub mod types; use self::types::*; use reqwest::{IntoUrl, Response}; use serde::{de::DeserializeOwned, Serialize}; use std::convert::TryFrom; use std::fmt; pub use reqwest; pub use reqwest::{StatusCode, Url}; #[derive(Debug)] pub enum Error { /// The `reqwest` client raised an error. Reqwest(reqwest::Error), /// The server returned an error message where the body was able to be parsed. ServerMessage(ErrorMessage), /// The server returned an error message where the body was unable to be parsed. StatusCode(StatusCode), /// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`. InvalidUrl(Url), } impl Error { /// If the error has a HTTP status code, return it. pub fn status(&self) -> Option { match self { Error::Reqwest(error) => error.status(), Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), Error::InvalidUrl(_) => None, } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } } /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Beacon Node HTTP server (`http_api`). #[derive(Clone)] pub struct BeaconNodeHttpClient { client: reqwest::Client, server: Url, } impl BeaconNodeHttpClient { pub fn new(server: Url) -> Self { Self { client: reqwest::Client::new(), server, } } pub fn from_components(server: Url, client: reqwest::Client) -> Self { Self { client, server } } /// Return the path with the standard `/eth1/v1` prefix applied. fn eth_path(&self) -> Result { let mut path = self.server.clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1"); Ok(path) } /// Perform a HTTP GET request. async fn get(&self, url: U) -> Result { let response = self.client.get(url).send().await.map_err(Error::Reqwest)?; ok_or_error(response) .await? .json() .await .map_err(Error::Reqwest) } /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_opt(&self, url: U) -> Result, Error> { let response = self.client.get(url).send().await.map_err(Error::Reqwest)?; match ok_or_error(response).await { Ok(resp) => resp.json().await.map(Option::Some).map_err(Error::Reqwest), Err(err) => { if err.status() == Some(StatusCode::NOT_FOUND) { Ok(None) } else { Err(err) } } } } /// Perform a HTTP POST request. async fn post(&self, url: U, body: &T) -> Result<(), Error> { let response = self .client .post(url) .json(body) .send() .await .map_err(Error::Reqwest)?; ok_or_error(response).await?; Ok(()) } /// `GET beacon/genesis` /// /// ## Errors /// /// May return a `404` if beacon chain genesis has not yet occurred. pub async fn get_beacon_genesis(&self) -> Result, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("genesis"); self.get(path).await } /// `GET beacon/states/{state_id}/root` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_states_root( &self, state_id: StateId, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("states") .push(&state_id.to_string()) .push("root"); self.get_opt(path).await } /// `GET beacon/states/{state_id}/fork` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_states_fork( &self, state_id: StateId, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("states") .push(&state_id.to_string()) .push("fork"); self.get_opt(path).await } /// `GET beacon/states/{state_id}/finality_checkpoints` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_states_finality_checkpoints( &self, state_id: StateId, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("states") .push(&state_id.to_string()) .push("finality_checkpoints"); self.get_opt(path).await } /// `GET beacon/states/{state_id}/validators` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_states_validators( &self, state_id: StateId, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("states") .push(&state_id.to_string()) .push("validators"); self.get_opt(path).await } /// `GET beacon/states/{state_id}/committees?slot,index` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_states_committees( &self, state_id: StateId, epoch: Epoch, slot: Option, index: Option, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("states") .push(&state_id.to_string()) .push("committees") .push(&epoch.to_string()); if let Some(slot) = slot { path.query_pairs_mut() .append_pair("slot", &slot.to_string()); } if let Some(index) = index { path.query_pairs_mut() .append_pair("index", &index.to_string()); } self.get_opt(path).await } /// `GET beacon/states/{state_id}/validators/{validator_id}` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_states_validator_id( &self, state_id: StateId, validator_id: &ValidatorId, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("states") .push(&state_id.to_string()) .push("validators") .push(&validator_id.to_string()); self.get_opt(path).await } /// `GET beacon/headers?slot,parent_root` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_headers( &self, slot: Option, parent_root: Option, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("headers"); if let Some(slot) = slot { path.query_pairs_mut() .append_pair("slot", &slot.to_string()); } if let Some(root) = parent_root { path.query_pairs_mut() .append_pair("parent_root", &format!("{:?}", root)); } self.get_opt(path).await } /// `GET beacon/headers/{block_id}` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_headers_block_id( &self, block_id: BlockId, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("headers") .push(&block_id.to_string()); self.get_opt(path).await } /// `POST beacon/blocks` /// /// Returns `Ok(None)` on a 404 error. pub async fn post_beacon_blocks( &self, block: &SignedBeaconBlock, ) -> Result<(), Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("blocks"); self.post(path, block).await?; Ok(()) } /// `GET beacon/blocks` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_blocks( &self, block_id: BlockId, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("blocks") .push(&block_id.to_string()); self.get_opt(path).await } /// `GET beacon/blocks/{block_id}/root` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_blocks_root( &self, block_id: BlockId, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("blocks") .push(&block_id.to_string()) .push("root"); self.get_opt(path).await } /// `GET beacon/blocks/{block_id}/attestations` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_blocks_attestations( &self, block_id: BlockId, ) -> Result>>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("blocks") .push(&block_id.to_string()) .push("attestations"); self.get_opt(path).await } /// `POST beacon/pool/attestations` pub async fn post_beacon_pool_attestations( &self, attestation: &Attestation, ) -> Result<(), Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("attestations"); self.post(path, attestation).await?; Ok(()) } /// `GET beacon/pool/attestations` pub async fn get_beacon_pool_attestations( &self, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("attestations"); self.get(path).await } /// `POST beacon/pool/attester_slashings` pub async fn post_beacon_pool_attester_slashings( &self, slashing: &AttesterSlashing, ) -> Result<(), Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("attester_slashings"); self.post(path, slashing).await?; Ok(()) } /// `GET beacon/pool/attester_slashings` pub async fn get_beacon_pool_attester_slashings( &self, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("attester_slashings"); self.get(path).await } /// `POST beacon/pool/proposer_slashings` pub async fn post_beacon_pool_proposer_slashings( &self, slashing: &ProposerSlashing, ) -> Result<(), Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("proposer_slashings"); self.post(path, slashing).await?; Ok(()) } /// `GET beacon/pool/proposer_slashings` pub async fn get_beacon_pool_proposer_slashings( &self, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("proposer_slashings"); self.get(path).await } /// `POST beacon/pool/voluntary_exits` pub async fn post_beacon_pool_voluntary_exits( &self, exit: &SignedVoluntaryExit, ) -> Result<(), Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("voluntary_exits"); self.post(path, exit).await?; Ok(()) } /// `GET beacon/pool/voluntary_exits` pub async fn get_beacon_pool_voluntary_exits( &self, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("pool") .push("voluntary_exits"); self.get(path).await } /// `GET config/fork_schedule` pub async fn get_config_fork_schedule(&self) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("config") .push("fork_schedule"); self.get(path).await } /// `GET config/fork_schedule` pub async fn get_config_spec(&self) -> Result, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("config") .push("spec"); self.get(path).await } /// `GET config/deposit_contract` pub async fn get_config_deposit_contract( &self, ) -> Result, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("config") .push("deposit_contract"); self.get(path).await } /// `GET node/version` pub async fn get_node_version(&self) -> Result, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("node") .push("version"); self.get(path).await } /// `GET node/syncing` pub async fn get_node_syncing(&self) -> Result, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("node") .push("syncing"); self.get(path).await } /// `GET debug/beacon/states/{state_id}` pub async fn get_debug_beacon_states( &self, state_id: StateId, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("debug") .push("beacon") .push("states") .push(&state_id.to_string()); self.get_opt(path).await } /// `GET debug/beacon/heads` pub async fn get_debug_beacon_heads( &self, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("debug") .push("beacon") .push("heads"); self.get(path).await } /// `GET validator/duties/attester/{epoch}?index` /// /// ## Note /// /// The `index` query parameter accepts a list of validator indices. pub async fn get_validator_duties_attester( &self, epoch: Epoch, index: Option<&[u64]>, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("duties") .push("attester") .push(&epoch.to_string()); if let Some(index) = index { let string = index .iter() .map(|i| i.to_string()) .collect::>() .join(","); path.query_pairs_mut().append_pair("index", &string); } self.get(path).await } /// `GET validator/duties/proposer/{epoch}` pub async fn get_validator_duties_proposer( &self, epoch: Epoch, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("duties") .push("proposer") .push(&epoch.to_string()); self.get(path).await } /// `GET validator/duties/attester/{epoch}?index` /// /// ## Note /// /// The `index` query parameter accepts a list of validator indices. pub async fn get_validator_blocks( &self, slot: Slot, randao_reveal: SignatureBytes, graffiti: Option<&Graffiti>, ) -> Result>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("blocks") .push(&slot.to_string()); path.query_pairs_mut() .append_pair("randao_reveal", &randao_reveal.to_string()); if let Some(graffiti) = graffiti { path.query_pairs_mut() .append_pair("graffiti", &graffiti.to_string()); } self.get(path).await } /// `GET validator/attestation_data?slot,committee_index` pub async fn get_validator_attestation_data( &self, slot: Slot, committee_index: CommitteeIndex, ) -> Result, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("attestation_data"); path.query_pairs_mut() .append_pair("slot", &slot.to_string()) .append_pair("committee_index", &committee_index.to_string()); self.get(path).await } /// `GET validator/attestation_attestation?slot,attestation_data_root` pub async fn get_validator_aggregate_attestation( &self, slot: Slot, attestation_data_root: Hash256, ) -> Result>>, Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("aggregate_attestation"); path.query_pairs_mut() .append_pair("slot", &slot.to_string()) .append_pair( "attestation_data_root", &format!("{:?}", attestation_data_root), ); self.get_opt(path).await } /// `POST validator/aggregate_and_proofs` pub async fn post_validator_aggregate_and_proof( &self, aggregate: &SignedAggregateAndProof, ) -> Result<(), Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("aggregate_and_proofs"); self.post(path, aggregate).await?; Ok(()) } /// `POST validator/beacon_committee_subscriptions` pub async fn post_validator_beacon_committee_subscriptions( &self, subscriptions: &[BeaconCommitteeSubscription], ) -> Result<(), Error> { let mut path = self.eth_path()?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("beacon_committee_subscriptions"); self.post(path, &subscriptions).await?; Ok(()) } } /// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an /// appropriate error message. async fn ok_or_error(response: Response) -> Result { let status = response.status(); if status == StatusCode::OK { Ok(response) } else if let Ok(message) = response.json().await { Err(Error::ServerMessage(message)) } else { Err(Error::StatusCode(status)) } }