diff --git a/README.md b/README.md index 2bada5fc9..b2f61193c 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,9 @@ To add support for a new network, you can: 1. add a new module under `ethereum_consensus::configs` using an existing network as a template 2. add the network's `genesis_time` and support for a `Clock` for that network in `ethereum_consensus::clock` 3. there are convenience methods on `ethereum_consensus::state_transition::Context` for each networkd and these should also be updated for the new network + +# beacon-api-client + +A client for the Ethereum beacon node APIs: + +https://ethereum.github.io/beacon-APIs diff --git a/examples/post.rs b/examples/post.rs new file mode 100644 index 000000000..585c55073 --- /dev/null +++ b/examples/post.rs @@ -0,0 +1,13 @@ +use beacon_api_client::mainnet::Client; +use ethereum_consensus::builder::SignedValidatorRegistration; +use url::Url; + +#[tokio::main] +async fn main() { + let client = Client::new(Url::parse("http://localhost:8080").unwrap()); + let data = SignedValidatorRegistration::default(); + let response = client.http_post("/eth/v1/builder/validators", &data).await.unwrap(); + dbg!(&response); + dbg!(&response.status()); + dbg!(&response.text().await); +} diff --git a/examples/sketch.rs b/examples/sketch.rs new file mode 100644 index 000000000..4a0f8b654 --- /dev/null +++ b/examples/sketch.rs @@ -0,0 +1,66 @@ +use beacon_api_client::{ApiError, ApiResult, Value, VersionedValue}; +use ethereum_consensus::{bellatrix::mainnet as bellatrix, capella::mainnet as capella}; +use std::collections::HashMap; + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "version", content = "data")] +#[serde(rename_all = "lowercase")] +enum BlindedBeaconBlock { + Bellatrix(bellatrix::BlindedBeaconBlock), + Capella(capella::BlindedBeaconBlock), +} + +fn main() { + let block = Value { meta: HashMap::new(), data: bellatrix::BlindedBeaconBlock::default() }; + let block_repr = serde_json::to_string(&block).unwrap(); + println!("{block_repr}"); + + let version = serde_json::to_value("bellatrix").unwrap(); + let block_with_version = Value { + meta: HashMap::from_iter([("version".to_string(), version)]), + data: bellatrix::BlindedBeaconBlock::default(), + }; + let block_with_version_repr = serde_json::to_string(&block_with_version).unwrap(); + println!("{block_with_version_repr}"); + + let block = BlindedBeaconBlock::Bellatrix(Default::default()); + let block_with_version_repr = serde_json::to_string(&block).unwrap(); + println!("{block_with_version_repr}"); + let recovered_block: BlindedBeaconBlock = + serde_json::from_str(&block_with_version_repr).unwrap(); + println!("{recovered_block:#?}"); + + let block = BlindedBeaconBlock::Capella(Default::default()); + let block_with_version_repr = serde_json::to_string(&block).unwrap(); + println!("{block_with_version_repr}"); + + let full_success_response = ApiResult::Ok(block.clone()); + let str_repr = serde_json::to_string(&full_success_response).unwrap(); + println!("{str_repr}"); + + let recovered_success: ApiResult> = + serde_json::from_str(&str_repr).unwrap(); + println!("{recovered_success:#?}"); + + let full_success_response = ApiResult::Ok(VersionedValue { + payload: block, + meta: HashMap::from_iter([( + String::from("finalized_root"), + serde_json::Value::String("0xdeadbeefcafe".to_string()), + )]), + }); + let str_repr = serde_json::to_string(&full_success_response).unwrap(); + println!("{str_repr}"); + + let recovered_success: ApiResult> = + serde_json::from_str(&str_repr).unwrap(); + println!("{recovered_success:#?}"); + + let full_error_response: ApiResult> = + ApiResult::Err(ApiError::try_from((404, "some failure")).unwrap()); + let str_repr = serde_json::to_string(&full_error_response).unwrap(); + println!("{str_repr}"); + + let recovered_error: ApiResult = serde_json::from_str(&str_repr).unwrap(); + println!("{recovered_error:#?}"); +} diff --git a/src/api_client.rs b/src/api_client.rs new file mode 100644 index 000000000..a05e38518 --- /dev/null +++ b/src/api_client.rs @@ -0,0 +1,864 @@ +use crate::{ + types::{ + ApiResult, AttestationDuty, BalanceSummary, BeaconHeaderSummary, + BeaconProposerRegistration, BlockId, BroadcastValidation, CommitteeDescriptor, + CommitteeFilter, CommitteeSummary, ConnectionOrientation, CoordinateWithMetadata, + DepositContract, DepositSnapshot, EventTopic, FinalityCheckpoints, GenesisDetails, + HealthStatus, NetworkIdentity, PeerDescription, PeerState, PeerSummary, ProposerDuty, + PublicKeyOrIndex, RootData, StateId, SyncCommitteeDescriptor, SyncCommitteeDuty, + SyncCommitteeSummary, SyncStatus, ValidatorLiveness, ValidatorStatus, ValidatorSummary, + Value, VersionData, + }, + ApiError, Error, +}; +use ethereum_consensus::{ + altair::SyncCommitteeMessage, + builder::SignedValidatorRegistration, + capella::{SignedBlsToExecutionChange, Withdrawal}, + networking::PeerId, + phase0::{AttestationData, Fork, ProposerSlashing, SignedVoluntaryExit}, + primitives::{ + BlobIndex, Bytes32, CommitteeIndex, Epoch, RandaoReveal, Root, Slot, ValidatorIndex, + }, +}; +use http::StatusCode; +use itertools::Itertools; +use std::collections::HashMap; +use url::Url; + +pub async fn api_error_or_ok(response: reqwest::Response) -> Result<(), Error> { + match response.status() { + reqwest::StatusCode::OK | reqwest::StatusCode::ACCEPTED => Ok(()), + _ => { + let api_err = response.json::().await?; + Err(Error::Api(api_err)) + } + } +} + +async fn api_error_or_value( + response: reqwest::Response, +) -> Result { + match response.status() { + reqwest::StatusCode::OK | reqwest::StatusCode::ACCEPTED => { + let value = response.json().await?; + Ok(value) + } + _ => { + let api_err = response.json::().await?; + Err(Error::Api(api_err)) + } + } +} + +#[allow(clippy::type_complexity)] +#[derive(Clone)] +pub struct Client { + pub http: reqwest::Client, + pub endpoint: Url, + _phantom: std::marker::PhantomData<(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O)>, +} + +impl< + SignedContributionAndProof: serde::Serialize, + SyncCommitteeContribution: serde::Serialize + serde::de::DeserializeOwned, + BlindedBeaconBlock: serde::Serialize + serde::de::DeserializeOwned, + SignedBlindedBeaconBlock: serde::Serialize + serde::de::DeserializeOwned, + Attestation: serde::Serialize + serde::de::DeserializeOwned, + AttesterSlashing: serde::Serialize + serde::de::DeserializeOwned, + BeaconBlock: serde::Serialize + serde::de::DeserializeOwned, + BeaconState: serde::Serialize + serde::de::DeserializeOwned, + SignedAggregateAndProof: serde::Serialize, + SignedBeaconBlock: serde::Serialize + serde::de::DeserializeOwned, + BlobSidecar: serde::Serialize + serde::de::DeserializeOwned, + LightClientBootstrap: serde::Serialize + serde::de::DeserializeOwned, + LightClientUpdate: serde::Serialize + serde::de::DeserializeOwned, + LightClientFinalityUpdate: serde::Serialize + serde::de::DeserializeOwned, + LightClientOptimisticUpdate: serde::Serialize + serde::de::DeserializeOwned, + > + Client< + SignedContributionAndProof, + SyncCommitteeContribution, + BlindedBeaconBlock, + SignedBlindedBeaconBlock, + Attestation, + AttesterSlashing, + BeaconBlock, + BeaconState, + SignedAggregateAndProof, + SignedBeaconBlock, + BlobSidecar, + LightClientBootstrap, + LightClientUpdate, + LightClientFinalityUpdate, + LightClientOptimisticUpdate, + > +{ + pub fn new_with_client>(client: reqwest::Client, endpoint: U) -> Self { + Self { http: client, endpoint: endpoint.into(), _phantom: std::marker::PhantomData } + } + + pub fn new>(endpoint: U) -> Self { + let client = reqwest::Client::new(); + Self::new_with_client(client, endpoint) + } + + pub async fn get( + &self, + path: &str, + ) -> Result { + let result: ApiResult = self.http_get(path).await?.json().await?; + match result { + ApiResult::Ok(result) => Ok(result), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn http_get(&self, path: &str) -> Result { + let target = self.endpoint.join(path)?; + let response = self.http.get(target).send().await?; + Ok(response) + } + + pub async fn post( + &self, + path: &str, + argument: &T, + ) -> Result<(), Error> { + let response = self.http_post(path, argument).await?; + api_error_or_ok(response).await + } + + pub async fn http_post( + &self, + path: &str, + argument: &T, + ) -> Result { + let target = self.endpoint.join(path)?; + let response = self.http.post(target).json(argument).send().await?; + Ok(response) + } + + /* beacon namespace */ + pub async fn get_genesis_details(&self) -> Result { + let details: Value = self.get("eth/v1/beacon/genesis").await?; + Ok(details.data) + } + + pub async fn get_state_root(&self, state_id: StateId) -> Result { + let path = format!("eth/v1/beacon/states/{state_id}/root"); + let root: Value = self.get(&path).await?; + Ok(root.data.root) + } + + pub async fn get_fork(&self, state_id: StateId) -> Result { + let path = format!("eth/v1/beacon/states/{state_id}/fork"); + let result: Value = self.get(&path).await?; + Ok(result.data) + } + + pub async fn get_finality_checkpoints( + &self, + id: StateId, + ) -> Result { + let path = format!("eth/v1/beacon/states/{id}/finality_checkpoints"); + let result: Value = self.get(&path).await?; + Ok(result.data) + } + + pub async fn get_validators( + &self, + state_id: StateId, + validator_ids: &[PublicKeyOrIndex], + filters: &[ValidatorStatus], + ) -> Result, Error> { + let path = format!("eth/v1/beacon/states/{state_id}/validators"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + if !validator_ids.is_empty() { + let validator_ids = validator_ids.iter().join(","); + request = request.query(&[("id", validator_ids)]); + } + if !filters.is_empty() { + let filters = filters.iter().join(","); + request = request.query(&[("status", filters)]); + } + let response = request.send().await?; + + let result: ApiResult>> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_validator( + &self, + state_id: StateId, + validator_id: PublicKeyOrIndex, + ) -> Result { + let path = format!("eth/v1/beacon/states/{state_id}/validators/{validator_id}"); + let result: Value = self.get(&path).await?; + Ok(result.data) + } + + pub async fn get_balances( + &self, + id: StateId, + filters: &[PublicKeyOrIndex], + ) -> Result, Error> { + let path = format!("eth/v1/beacon/states/{id}/validator_balances"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + + if !filters.is_empty() { + let filters = filters.iter().join(","); + request = request.query(&[("id", filters)]); + } + let response = request.send().await?; + + let result: ApiResult>> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_all_committees(&self, id: StateId) -> Result, Error> { + self.get_committees(id, CommitteeFilter::default()).await + } + + pub async fn get_committees( + &self, + id: StateId, + filter: CommitteeFilter, + ) -> Result, Error> { + let path = format!("eth/v1/beacon/states/{id}/committees"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + if let Some(epoch) = filter.epoch { + request = request.query(&[("epoch", epoch)]); + } + if let Some(index) = filter.index { + request = request.query(&[("index", index)]); + } + if let Some(slot) = filter.slot { + request = request.query(&[("slot", slot)]); + } + let response = request.send().await?; + let result: ApiResult>> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_sync_committees( + &self, + id: StateId, + epoch: Option, + ) -> Result { + let path = format!("eth/v1/beacon/states/{id}/sync_committees"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + if let Some(epoch) = epoch { + request = request.query(&[("epoch", epoch)]); + } + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_randao(&self, id: StateId, epoch: Option) -> Result { + let path = format!("eth/v1/beacon/states/{id}/randao"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + if let Some(epoch) = epoch { + request = request.query(&[("epoch", epoch)]); + } + let response = request.send().await?; + + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_beacon_header_at_head(&self) -> Result { + let result: Value = self.get("eth/v1/beacon/headers").await?; + Ok(result.data) + } + + pub async fn get_beacon_header_for_slot( + &self, + slot: Slot, + ) -> Result { + let target = self.endpoint.join("eth/v1/beacon/headers")?; + let mut request = self.http.get(target); + request = request.query(&[("slot", slot)]); + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_beacon_header_for_parent_root( + &self, + parent_root: Root, + ) -> Result { + let target = self.endpoint.join("eth/v1/beacon/headers")?; + let mut request = self.http.get(target); + request = request.query(&[("parent_root", parent_root)]); + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_beacon_header(&self, id: BlockId) -> Result { + let path = format!("eth/v1/beacon/headers/{id}"); + let result: Value = self.get(&path).await?; + Ok(result.data) + } + + pub async fn post_signed_blinded_beacon_block( + &self, + block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + self.post("eth/v1/beacon/blinded_blocks", block).await + } + + pub async fn post_signed_blinded_beacon_block_v2( + &self, + block: &SignedBlindedBeaconBlock, + broadcast_validation: Option, + ) -> Result<(), Error> { + let target = self.endpoint.join("eth/v2/beacon/blinded_blocks")?; + let mut request = self.http.post(target).json(block); + if let Some(validation) = broadcast_validation { + request = request.query(&[("broadcast_validation", validation)]); + } + let response = request.send().await?; + api_error_or_ok(response).await + } + + pub async fn post_signed_beacon_block(&self, block: &SignedBeaconBlock) -> Result<(), Error> { + self.post("eth/v1/beacon/blocks", block).await + } + + pub async fn post_signed_beacon_block_v2( + &self, + block: &SignedBeaconBlock, + broadcast_validation: Option, + ) -> Result<(), Error> { + let target = self.endpoint.join("eth/v2/beacon/blocks")?; + let mut request = self.http.post(target).json(block); + if let Some(validation) = broadcast_validation { + request = request.query(&[("broadcast_validation", validation)]); + } + let response = request.send().await?; + api_error_or_ok(response).await + } + + // v2 endpoint + pub async fn get_beacon_block(&self, id: BlockId) -> Result { + let result: Value = + self.get(&format!("eth/v2/beacon/blocks/{id}")).await?; + Ok(result.data) + } + + pub async fn get_beacon_block_root(&self, id: BlockId) -> Result { + let result: Value = self.get(&format!("eth/v1/beacon/blocks/{id}/root")).await?; + Ok(result.data.root) + } + + pub async fn get_attestations_from_beacon_block( + &self, + id: BlockId, + ) -> Result, Error> { + let result: Value> = + self.get(&format!("eth/v1/beacon/blocks/{id}/attestations")).await?; + Ok(result.data) + } + + pub async fn get_blob_sidecars( + &self, + id: BlockId, + indices: &[BlobIndex], + ) -> Result, Error> { + let path = format!("eth/v1/beacon/blob_sidecars/{id}"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + if !indices.is_empty() { + request = request.query(&[("indices", indices)]); + } + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_deposit_snapshot(&self) -> Result { + let result: Value = self.get("eth/v1/beacon/deposit_snapshot").await?; + Ok(result.data) + } + + pub async fn get_blinded_block(&self, id: BlockId) -> Result { + let result: Value = + self.get(&format!("eth/v1/beacon/blinded_blocks/{id}")).await?; + Ok(result.data) + } + + pub async fn get_light_client_bootstrap( + &self, + block: Root, + ) -> Result { + let result: Value<_> = + self.get(&format!("eth/v1/beacon/light_client/bootstrap/{block}")).await?; + Ok(result.data) + } + + pub async fn get_light_client_updates( + &self, + start: u64, + count: u64, + ) -> Result, Error> { + let target = self.endpoint.join("eth/v1/beacon/light_client/updates")?; + let mut request = self.http.get(target); + request = request.query(&[("start_period", start), ("count", count)]); + + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_light_client_finality_update( + &self, + ) -> Result { + let result: Value<_> = self.get("eth/v1/beacon/light_client/finality_update").await?; + Ok(result.data) + } + + pub async fn get_light_client_optimistic_update( + &self, + ) -> Result { + let result: Value<_> = self.get("eth/v1/beacon/light_client/optimistic_update").await?; + Ok(result.data) + } + + pub async fn get_attestations_from_pool( + &self, + slot: Option, + committee_index: Option, + ) -> Result, Error> { + let path = "eth/v1/beacon/pool/attestations"; + let target = self.endpoint.join(path)?; + let mut request = self.http.get(target); + if let Some(slot) = slot { + request = request.query(&[("slot", slot)]); + } + if let Some(committee_index) = committee_index { + request = request.query(&[("committee_index", committee_index)]); + } + let response = request.send().await?; + let result: ApiResult>> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn post_attestations(&self, attestations: &[Attestation]) -> Result<(), Error> { + self.post("eth/v1/beacon/pool/attestations", attestations).await + } + + pub async fn get_attester_slashings_from_pool(&self) -> Result, Error> { + let result: Value> = + self.get("eth/v1/beacon/pool/attester_slashings").await?; + Ok(result.data) + } + + pub async fn post_attester_slashing( + &self, + attester_slashing: &AttesterSlashing, + ) -> Result<(), Error> { + self.post("eth/v1/beacon/pool/attester_slashings", attester_slashing).await + } + + pub async fn get_proposer_slashings_from_pool(&self) -> Result, Error> { + let result: Value> = + self.get("eth/v1/beacon/pool/proposer_slashings").await?; + Ok(result.data) + } + + pub async fn post_proposer_slashing( + &self, + proposer_slashing: &ProposerSlashing, + ) -> Result<(), Error> { + self.post("eth/v1/beacon/pool/proposer_slashings", proposer_slashing).await + } + + pub async fn post_sync_committee_messages( + &self, + messages: &[SyncCommitteeMessage], + ) -> Result<(), Error> { + self.post("eth/v1/beacon/pool/sync_committees", messages).await + } + + pub async fn get_voluntary_exits_from_pool(&self) -> Result, Error> { + let result: Value> = + self.get("eth/v1/beacon/pool/voluntary_exits").await?; + Ok(result.data) + } + + pub async fn post_signed_voluntary_exit( + &self, + exit: &SignedVoluntaryExit, + ) -> Result<(), Error> { + self.post("eth/v1/beacon/pool/voluntary_exits", exit).await + } + + pub async fn get_bls_to_execution_changes( + &self, + ) -> Result, Error> { + let result: Value> = + self.get("eth/v1/beacon/pool/bls_to_execution_changes").await?; + Ok(result.data) + } + + pub async fn post_bls_to_execution_changes( + &self, + changes: &[SignedBlsToExecutionChange], + ) -> Result<(), Error> { + self.post("eth/v1/beacon/pool/bls_to_execution_changes", changes).await + } + + /* builder namespace */ + pub async fn get_expected_withdrawals( + &self, + id: StateId, + slot: Option, + ) -> Result, Error> { + let path = format!("eth/v1/builder/states/{id}/expected_withdrawals"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + if let Some(slot) = slot { + request = request.query(&[("proposal_slot", slot)]); + } + + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + /* config namespace */ + pub async fn get_fork_schedule(&self) -> Result, Error> { + let result: Value> = self.get("eth/v1/config/fork_schedule").await?; + Ok(result.data) + } + + pub async fn get_spec(&self) -> Result, Error> { + let result: Value> = self.get("eth/v1/config/spec").await?; + Ok(result.data) + } + + pub async fn get_deposit_contract_address(&self) -> Result { + let result: Value = self.get("eth/v1/config/deposit_contract").await?; + Ok(result.data) + } + + /* debug namespace */ + // v2 endpoint + pub async fn get_state(&self, id: StateId) -> Result { + let result: Value = + self.get(&format!("eth/v2/debug/beacon/states/{id}")).await?; + Ok(result.data) + } + + // v2 endpoint + pub async fn get_heads(&self) -> Result, Error> { + let result: Value> = + self.get("eth/v2/debug/beacon/heads").await?; + Ok(result.data) + } + + /* events namespace */ + pub async fn get_events(_topics: &[EventTopic]) -> Result { + unimplemented!("") + } + + /* node namespace */ + pub async fn get_node_identity(&self) -> Result { + let result: Value = self.get("eth/v1/node/identity").await?; + Ok(result.data) + } + + pub async fn get_node_peers( + &self, + peer_states: &[PeerState], + connection_orientations: &[ConnectionOrientation], + ) -> Result, Error> { + let path = "eth/v1/node/peers"; + let target = self.endpoint.join(path)?; + let mut request = self.http.get(target); + if !peer_states.is_empty() { + request = request.query(&[("state", peer_states.iter().join(","))]); + } + if !connection_orientations.is_empty() { + request = request.query(&[("direction", connection_orientations.iter().join(","))]); + } + let response = request.send().await?; + let result: ApiResult>> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_peer(&self, peer_id: PeerId) -> Result { + let result: Value = + self.get(&format!("/eth/v1/node/peers/{peer_id}")).await?; + Ok(result.data) + } + + pub async fn get_peer_summary(&self) -> Result { + let result: Value = self.get("eth/v1/node/peer_count").await?; + Ok(result.data) + } + + pub async fn get_node_version(&self) -> Result { + let result: Value = self.get("eth/v1/node/version").await?; + Ok(result.data.version) + } + + pub async fn get_sync_status(&self) -> Result { + let result: Value = self.get("eth/v1/node/syncing").await?; + Ok(result.data) + } + + pub async fn get_health(&self) -> Result { + let path = "eth/v1/node/health"; + let target = self.endpoint.join(path)?; + let request = self.http.get(target); + let response = request.send().await?; + let result = match response.status() { + StatusCode::OK => HealthStatus::Ready, + StatusCode::PARTIAL_CONTENT => HealthStatus::Syncing, + StatusCode::SERVICE_UNAVAILABLE => HealthStatus::NotInitialized, + _ => HealthStatus::Unknown, + }; + Ok(result) + } + + /* validator namespace */ + pub async fn get_attester_duties( + &self, + epoch: Epoch, + indices: &[ValidatorIndex], + ) -> Result<(Root, Vec), Error> { + let endpoint = format!("eth/v1/validator/duties/attester/{epoch}"); + let indices = indices.iter().map(|index| index.to_string()).collect::>(); + let response = self.http_post(&endpoint, &indices).await?; + let mut result: Value> = api_error_or_value(response).await?; + let dependent_root_value = result + .meta + .remove("dependent_root") + .ok_or_else(|| Error::MissingExpectedData("`dependent_root`".to_string()))?; + let dependent_root: Root = serde_json::from_value(dependent_root_value)?; + Ok((dependent_root, result.data)) + } + + pub async fn get_proposer_duties( + &self, + epoch: Epoch, + ) -> Result<(Root, Vec), Error> { + let endpoint = format!("eth/v1/validator/duties/proposer/{epoch}"); + let mut result: Value> = self.get(&endpoint).await?; + let dependent_root_value = result.meta.remove("dependent_root").ok_or_else(|| { + Error::MissingExpectedData("missing `dependent_root` in response".to_string()) + })?; + let dependent_root: Root = serde_json::from_value(dependent_root_value)?; + Ok((dependent_root, result.data)) + } + + pub async fn get_sync_committee_duties( + &self, + epoch: Epoch, + indices: &[ValidatorIndex], + ) -> Result, Error> { + let endpoint = format!("eth/v1/validator/duties/sync/{epoch}"); + let indices = indices.iter().map(|index| index.to_string()).collect::>(); + let response = self.http_post(&endpoint, &indices).await?; + let result: Value> = api_error_or_value(response).await?; + Ok(result.data) + } + + // v2 endpoint + pub async fn get_block_proposal( + &self, + slot: Slot, + randao_reveal: RandaoReveal, + graffiti: Option, + ) -> Result { + let path = format!("eth/v2/validator/blocks/{slot}"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + request = request.query(&[("randao_reveal", randao_reveal)]); + if let Some(graffiti) = graffiti { + request = request.query(&[("graffiti", graffiti)]); + } + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_blinded_block_proposal( + &self, + slot: Slot, + randao_reveal: RandaoReveal, + graffiti: Option, + ) -> Result { + let path = format!("eth/v1/validator/blinded_blocks/{slot}"); + let target = self.endpoint.join(&path)?; + let mut request = self.http.get(target); + request = request.query(&[("randao_reveal", randao_reveal)]); + if let Some(graffiti) = graffiti { + request = request.query(&[("graffiti", graffiti)]); + } + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_attestation_data( + &self, + slot: Slot, + committee_index: CommitteeIndex, + ) -> Result { + let target = self.endpoint.join("eth/v1/validator/attestation_data")?; + let mut request = self.http.get(target); + request = request.query(&[("slot", slot)]); + request = request.query(&[("committee_index", committee_index)]); + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn get_attestation_aggregate( + &self, + attestation_data_root: Root, + slot: Slot, + ) -> Result { + let target = self.endpoint.join("eth/v1/validator/aggregate_attestation")?; + let mut request = self.http.get(target); + request = request.query(&[("attestation_data_root", attestation_data_root)]); + request = request.query(&[("slot", slot)]); + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn post_aggregates_with_proofs( + &self, + aggregates_with_proofs: &[SignedAggregateAndProof], + ) -> Result<(), Error> { + self.post("eth/v1/validator/aggregate_and_proofs", aggregates_with_proofs).await + } + + pub async fn subscribe_subnets_for_attestation_committees( + &self, + committee_descriptors: &[CommitteeDescriptor], + ) -> Result<(), Error> { + self.post("eth/v1/validator/beacon_committee_subscriptions", committee_descriptors).await + } + + pub async fn subscribe_subnets_for_sync_committees( + &self, + sync_committee_descriptors: &[SyncCommitteeDescriptor], + ) -> Result<(), Error> { + self.post("eth/v1/validator/sync_committee_subscriptions", sync_committee_descriptors).await + } + + pub async fn get_sync_committee_contribution( + &self, + slot: Slot, + subcommittee_index: usize, + beacon_block_root: Root, + ) -> Result { + let target = self.endpoint.join("eth/v1/validator/sync_committee_contribution")?; + let mut request = self.http.get(target); + request = request.query(&[("slot", slot)]); + request = request.query(&[("subcommittee_index", subcommittee_index)]); + request = request.query(&[("beacon_block_root", beacon_block_root)]); + let response = request.send().await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } + + pub async fn post_sync_committee_contributions_with_proofs( + &self, + contributions_with_proofs: &[SignedContributionAndProof], + ) -> Result<(), Error> { + self.post("eth/v1/validator/contribution_and_proofs", contributions_with_proofs).await + } + + pub async fn prepare_proposers( + &self, + registrations: &[BeaconProposerRegistration], + ) -> Result<(), Error> { + self.post("eth/v1/validator/prepare_beacon_proposer", registrations).await + } + + // endpoint for builder registrations + pub async fn register_validators_with_builders( + &self, + registrations: &[SignedValidatorRegistration], + ) -> Result<(), Error> { + self.post("eth/v1/validator/register_validator", registrations).await + } + + pub async fn post_liveness( + &self, + epoch: Epoch, + indices: &[ValidatorIndex], + ) -> Result, Error> { + let response = + self.http_post(&format!("eth/v1/validator/liveness/{epoch}"), indices).await?; + let result: ApiResult> = response.json().await?; + match result { + ApiResult::Ok(result) => Ok(result.data), + ApiResult::Err(err) => Err(err.into()), + } + } +} diff --git a/src/api_error.rs b/src/api_error.rs new file mode 100644 index 000000000..3efc6e5e9 --- /dev/null +++ b/src/api_error.rs @@ -0,0 +1,55 @@ +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use std::{error::Error, fmt}; + +// NOTE: `IndexedError` must come before `ErrorMessage` so +// the `serde(untagged)` machinery does not greedily match it first. +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +pub enum ApiError { + IndexedError { + #[serde(with = "crate::serde::as_u16")] + code: StatusCode, + message: String, + failures: Vec, + }, + ErrorMessage { + #[serde(with = "crate::serde::as_u16")] + code: StatusCode, + message: String, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct IndexedError { + index: usize, + message: String, +} + +impl fmt::Display for ApiError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ErrorMessage { message, .. } => { + write!(f, "{message}") + } + Self::IndexedError { message, failures, .. } => { + write!(f, "{message}: ")?; + for failure in failures { + write!(f, "{failure:?}, ")?; + } + Ok(()) + } + } + } +} + +impl Error for ApiError {} + +impl<'a> TryFrom<(u16, &'a str)> for ApiError { + type Error = http::status::InvalidStatusCode; + + fn try_from((code, message): (u16, &'a str)) -> Result { + let code = StatusCode::from_u16(code)?; + Ok(Self::ErrorMessage { code, message: message.to_string() }) + } +} diff --git a/src/cli/config.rs b/src/cli/config.rs new file mode 100644 index 000000000..a12ce2bb3 --- /dev/null +++ b/src/cli/config.rs @@ -0,0 +1,39 @@ +use crate::types::StateId; +use clap::{Args, Parser, Subcommand}; + +#[derive(Debug, Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct CliConfig { + #[arg(long)] + pub endpoint: String, + #[command(subcommand)] + pub namespace: Namespace, +} + +#[derive(Debug, Subcommand)] +pub enum Namespace { + #[clap(subcommand)] + Beacon(BeaconMethod), +} + +#[derive(Debug, Subcommand)] +pub enum BeaconMethod { + Genesis, + Root(StateIdArg), +} + +#[derive(Args, Debug)] +pub struct StateIdArg { + #[arg( + value_parser = clap::value_parser!(StateId), + long_help = "Identifier for the state under consideration. Possible values are: + head + genesis + finalized + justified + + ", + )] + pub state_id: StateId, +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 000000000..681371370 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,15 @@ +mod config; +use crate::mainnet::Client; +pub use config::CliConfig; +use config::{BeaconMethod, Namespace::Beacon}; + +pub async fn run_cli(client: &Client, args: &CliConfig) { + match &args.namespace { + Beacon(BeaconMethod::Genesis) => { + println!("{:?}", &client.get_genesis_details().await.unwrap()); + } + Beacon(BeaconMethod::Root(arg)) => { + println!("{}", &client.get_state_root(arg.state_id.clone()).await.unwrap()) + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..11b7c3bbc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,83 @@ +mod api_client; +mod api_error; +mod cli; +mod serde; +mod types; + +pub use api_client::*; +pub use api_error::*; +pub use cli::*; +pub use error::*; +pub use presets::*; +pub use types::*; + +mod error { + use crate::ApiError; + use thiserror::Error; + use url::ParseError; + + #[derive(Debug, Error)] + pub enum Error { + #[error("could not parse URL: {0}")] + Url(#[from] ParseError), + #[error("could not send request: {0}")] + Http(#[from] reqwest::Error), + #[error("error from API: {0}")] + Api(#[from] ApiError), + #[error("missing expected data in response: {0}")] + MissingExpectedData(String), + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + } +} + +pub mod presets { + pub mod mainnet { + use ethereum_consensus::{ + altair::mainnet as altair, bellatrix::mainnet as bellatrix, deneb::mainnet as deneb, + phase0::mainnet as phase0, + }; + + pub type Client = crate::Client< + altair::SignedContributionAndProof, + altair::SyncCommitteeContribution, + bellatrix::BlindedBeaconBlock, + bellatrix::SignedBlindedBeaconBlock, + phase0::Attestation, + phase0::AttesterSlashing, + phase0::BeaconBlock, + phase0::BeaconState, + phase0::SignedAggregateAndProof, + phase0::SignedBeaconBlock, + deneb::BlobSidecar, + altair::LightClientBootstrap, + altair::LightClientUpdate, + altair::LightClientFinalityUpdate, + altair::LightClientOptimisticUpdate, + >; + } + pub mod minimal { + use ethereum_consensus::{ + altair::minimal as altair, bellatrix::minimal as bellatrix, deneb::minimal as deneb, + phase0::minimal as phase0, + }; + + pub type Client = crate::Client< + altair::SignedContributionAndProof, + altair::SyncCommitteeContribution, + bellatrix::BlindedBeaconBlock, + bellatrix::SignedBlindedBeaconBlock, + phase0::Attestation, + phase0::AttesterSlashing, + phase0::BeaconBlock, + phase0::BeaconState, + phase0::SignedAggregateAndProof, + phase0::SignedBeaconBlock, + deneb::BlobSidecar, + altair::LightClientBootstrap, + altair::LightClientUpdate, + altair::LightClientFinalityUpdate, + altair::LightClientOptimisticUpdate, + >; + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 000000000..3462009fa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,11 @@ +use beacon_api_client::{mainnet::Client, run_cli, CliConfig}; +use clap::Parser; +use url::Url; + +#[tokio::main] +async fn main() { + let args = CliConfig::parse(); + let url = Url::parse(&args.endpoint).unwrap(); + let client = Client::new(url); + run_cli(&client, &args).await; +} diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 000000000..87403fa79 --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,21 @@ +pub(crate) use ethereum_consensus::serde::{as_hex, as_string, collection_over_string}; + +pub(crate) mod as_u16 { + use http::StatusCode; + use serde::{de::Deserializer, Deserialize, Serializer}; + + pub fn serialize(x: &StatusCode, s: S) -> Result + where + S: Serializer, + { + s.serialize_u16(x.as_u16()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: u16 = Deserialize::deserialize(deserializer)?; + StatusCode::from_u16(value).map_err(serde::de::Error::custom) + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 000000000..02fc3b7ad --- /dev/null +++ b/src/types.rs @@ -0,0 +1,494 @@ +use crate::ApiError; +use ethereum_consensus::{ + altair::networking::MetaData, + networking::{Enr, Multiaddr, PeerId}, + phase0::{Checkpoint, SignedBeaconBlockHeader, Validator}, + primitives::{ + BlsPublicKey, ChainId, CommitteeIndex, Coordinate, Epoch, ExecutionAddress, Gwei, Hash32, + Root, Slot, ValidatorIndex, Version, + }, + serde::try_bytes_from_hex_str, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{collections::HashMap, fmt, str::FromStr}; + +#[derive(Serialize, Deserialize)] +pub struct VersionData { + pub version: String, +} + +#[derive(Serialize, Deserialize)] +pub struct CoordinateWithMetadata { + #[serde(flatten)] + pub coordinate: Coordinate, + #[serde(flatten)] + pub meta: HashMap, +} + +#[derive(Serialize, Deserialize)] +pub struct DepositContract { + #[serde(with = "crate::serde::as_string")] + pub chain_id: ChainId, + pub address: ExecutionAddress, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DepositSnapshot { + pub finalized: Vec, + pub deposit_root: Hash32, + #[serde(with = "crate::serde::as_string")] + pub deposit_count: u64, + pub execution_block_hash: Hash32, + #[serde(with = "crate::serde::as_string")] + pub execution_block_height: u64, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct GenesisDetails { + #[serde(with = "crate::serde::as_string")] + pub genesis_time: u64, + pub genesis_validators_root: Root, + #[serde(with = "crate::serde::as_hex")] + pub genesis_fork_version: Version, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum StateId { + Head, + Genesis, + Finalized, + Justified, + Slot(Slot), + Root(Root), +} + +impl fmt::Display for StateId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match *self { + StateId::Finalized => "finalized", + StateId::Justified => "justified", + StateId::Head => "head", + StateId::Genesis => "genesis", + StateId::Slot(slot) => return write!(f, "{slot}"), + StateId::Root(root) => return write!(f, "{root}"), + }; + write!(f, "{printable}") + } +} + +impl FromStr for StateId { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "finalized" => Ok(StateId::Finalized), + "justified" => Ok(StateId::Justified), + "head" => Ok(StateId::Head), + "genesis" => Ok(StateId::Genesis), + _ => match s.parse::() { + Ok(slot) => Ok(Self::Slot(slot)), + Err(_) => match try_bytes_from_hex_str(s) { + Ok(root_data) => { + let root = Root::try_from(root_data.as_ref()).map_err(|err| format!("could not parse state identifier by root from the provided argument {s}: {err}"))?; + Ok(Self::Root(root)) + } + Err(err) => { + let err = format!("could not parse state identifier by root from the provided argument {s}: {err}"); + Err(err) + } + }, + }, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct RootData { + pub root: Root, +} + +#[derive(Serialize, Deserialize)] +pub enum BlockId { + Head, + Genesis, + Finalized, + Slot(Slot), + Root(Root), +} + +impl fmt::Display for BlockId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match *self { + BlockId::Finalized => "finalized", + BlockId::Head => "head", + BlockId::Genesis => "genesis", + BlockId::Slot(slot) => return write!(f, "{slot}"), + BlockId::Root(root) => return write!(f, "{root}"), + }; + write!(f, "{printable}") + } +} + +#[derive(Serialize, Deserialize)] +enum ExecutionStatus { + Default, + Optimistic, +} + +#[derive(Serialize, Deserialize)] +pub struct FinalityCheckpoints { + pub previous_justified: Checkpoint, + pub current_justified: Checkpoint, + pub finalized: Checkpoint, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ValidatorStatus { + PendingInitialized, + PendingQueued, + ActiveOngoing, + ActiveExiting, + ActiveSlashed, + ExitedUnslashed, + ExitedSlashed, + WithdrawalPossible, + WithdrawalDone, + Active, + Pending, + Exited, + Withdrawal, +} + +impl fmt::Display for ValidatorStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match *self { + Self::PendingInitialized => "pending_initialized", + Self::PendingQueued => "pending_queued", + Self::ActiveOngoing => "active_ongoing", + Self::ActiveExiting => "active_exiting", + Self::ActiveSlashed => "active_slashed", + Self::ExitedUnslashed => "exited_unslashed", + Self::ExitedSlashed => "exited_slashed", + Self::WithdrawalPossible => "withdrawal_possible", + Self::WithdrawalDone => "withdrawal_done", + Self::Active => "active", + Self::Pending => "pending", + Self::Exited => "exited", + Self::Withdrawal => "withdrawal", + }; + write!(f, "{printable}") + } +} + +#[derive(Debug)] +pub enum PublicKeyOrIndex { + PublicKey(BlsPublicKey), + Index(ValidatorIndex), +} + +impl fmt::Display for PublicKeyOrIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match *self { + Self::PublicKey(ref pk) => pk.to_string(), + Self::Index(i) => i.to_string(), + }; + write!(f, "{printable}") + } +} + +impl From for PublicKeyOrIndex { + fn from(index: ValidatorIndex) -> Self { + Self::Index(index) + } +} + +impl From for PublicKeyOrIndex { + fn from(public_key: BlsPublicKey) -> Self { + Self::PublicKey(public_key) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ValidatorSummary { + #[serde(with = "crate::serde::as_string")] + pub index: ValidatorIndex, + #[serde(with = "crate::serde::as_string")] + pub balance: Gwei, + pub status: ValidatorStatus, + pub validator: Validator, +} + +#[derive(Serialize, Deserialize)] +pub struct BalanceSummary { + #[serde(with = "crate::serde::as_string")] + pub index: ValidatorIndex, + #[serde(with = "crate::serde::as_string")] + pub balance: Gwei, +} + +#[derive(Default)] +pub struct CommitteeFilter { + pub epoch: Option, + pub index: Option, + pub slot: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Committee( + #[serde(with = "crate::serde::collection_over_string")] pub Vec, +); + +#[derive(Serialize, Deserialize, Debug)] +pub struct CommitteeSummary { + #[serde(with = "crate::serde::as_string")] + pub index: CommitteeIndex, + #[serde(with = "crate::serde::as_string")] + pub slot: Slot, + pub validators: Committee, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SyncCommitteeSummary { + #[serde(with = "crate::serde::collection_over_string")] + pub validators: Vec, + pub validator_aggregates: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct BeaconHeaderSummary { + pub root: Root, + pub canonical: bool, + pub signed_header: SignedBeaconBlockHeader, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum BroadcastValidation { + Gossip, + Consensus, + ConsensusAndEquivocation, +} + +impl fmt::Display for BroadcastValidation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match self { + Self::Gossip => "gossip", + Self::Consensus => "consensus", + Self::ConsensusAndEquivocation => "consensus_and_equivocation", + }; + write!(f, "{printable}") + } +} + +pub enum EventTopic { + Head, + Block, + Attestation, + VoluntaryExit, + FinalizedCheckpoint, + ChainReorg, + ContributionAndProof, +} + +#[derive(Serialize, Deserialize)] +pub struct NetworkIdentity { + pub peer_id: PeerId, + pub enr: Enr, + pub p2p_addresses: Vec, + pub discovery_addresses: Vec, + pub metadata: MetaData, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum PeerState { + Disconnected, + Connecting, + Connected, + Disconnecting, +} + +impl fmt::Display for PeerState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match *self { + Self::Disconnected => "disconnected", + Self::Connecting => "connecting", + Self::Connected => "connected", + Self::Disconnecting => "disconnecting", + }; + write!(f, "{printable}") + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ConnectionOrientation { + Inbound, + Outbound, +} + +impl fmt::Display for ConnectionOrientation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match *self { + Self::Inbound => "inbound", + Self::Outbound => "outbound", + }; + write!(f, "{printable}") + } +} + +#[derive(Serialize, Deserialize)] +pub struct PeerDescriptor { + pub state: PeerState, + pub direction: ConnectionOrientation, +} + +#[derive(Serialize, Deserialize)] +pub struct PeerDescription { + pub peer_id: PeerId, + pub enr: Enr, + pub last_seen_p2p_address: Multiaddr, + pub state: PeerState, + pub direction: ConnectionOrientation, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct PeerSummary { + #[serde(with = "crate::serde::as_string")] + pub disconnected: usize, + #[serde(with = "crate::serde::as_string")] + pub connecting: usize, + #[serde(with = "crate::serde::as_string")] + pub connected: usize, + #[serde(with = "crate::serde::as_string")] + pub disconnecting: usize, +} + +#[derive(Serialize, Deserialize)] +pub struct SyncStatus { + #[serde(with = "crate::serde::as_string")] + pub head_slot: Slot, + #[serde(with = "crate::serde::as_string")] + pub sync_distance: usize, + pub is_syncing: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum HealthStatus { + Ready, + Syncing, + NotInitialized, + Unknown, +} + +#[derive(Serialize, Deserialize)] +pub struct AttestationDuty { + #[serde(rename = "pubkey")] + pub public_key: BlsPublicKey, + #[serde(with = "crate::serde::as_string")] + pub validator_index: ValidatorIndex, + #[serde(with = "crate::serde::as_string")] + pub committee_index: CommitteeIndex, + #[serde(with = "crate::serde::as_string")] + pub committee_length: usize, + #[serde(with = "crate::serde::as_string")] + pub committees_at_slot: usize, + #[serde(with = "crate::serde::as_string")] + pub validator_committee_index: usize, + #[serde(with = "crate::serde::as_string")] + pub slot: Slot, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ProposerDuty { + #[serde(rename = "pubkey")] + pub public_key: BlsPublicKey, + #[serde(with = "crate::serde::as_string")] + pub validator_index: ValidatorIndex, + #[serde(with = "crate::serde::as_string")] + pub slot: Slot, +} + +#[derive(Serialize, Deserialize)] +pub struct SyncCommitteeDuty { + #[serde(rename = "pubkey")] + pub public_key: BlsPublicKey, + #[serde(with = "crate::serde::as_string")] + pub validator_index: ValidatorIndex, + #[serde(with = "crate::serde::collection_over_string")] + pub validator_sync_committee_indices: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct CommitteeDescriptor { + #[serde(with = "crate::serde::as_string")] + pub validator_index: ValidatorIndex, + #[serde(with = "crate::serde::as_string")] + pub committee_index: CommitteeIndex, + #[serde(with = "crate::serde::as_string")] + pub committees_at_slot: usize, + #[serde(with = "crate::serde::as_string")] + pub slot: Slot, + pub is_aggregator: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct SyncCommitteeDescriptor { + #[serde(with = "crate::serde::as_string")] + pub validator_index: ValidatorIndex, + #[serde(with = "crate::serde::collection_over_string")] + pub sync_committee_indices: Vec, + #[serde(with = "crate::serde::as_string")] + pub until_epoch: Epoch, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BeaconProposerRegistration { + #[serde(with = "crate::serde::as_string")] + pub validator_index: ValidatorIndex, + pub fee_recipient: ExecutionAddress, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ValidatorLiveness { + #[serde(with = "crate::serde::as_string")] + index: ValidatorIndex, + is_live: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")] +pub struct Value { + pub data: T, + #[serde(flatten)] + pub meta: HashMap, +} + +/* +`VersionedValue` captures: +```json +{ + "version": "fork-version", + "data": { ... }, + < optional additional metadata >, +} + +And can be combined with Rust `enum`s to handle polymorphic {de,}serialization. +``` + */ +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(bound = "T: serde::Serialize + serde::de::DeserializeOwned")] +pub struct VersionedValue { + #[serde(flatten)] + pub payload: T, + #[serde(flatten)] + pub meta: HashMap, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")] +#[serde(untagged)] +pub enum ApiResult { + Ok(T), + Err(ApiError), +}