checkpointq_lib/
client.rs

1use crate::errors::AppError;
2use crate::processor::{process_to_displayable_format, DisplayableResult};
3use async_trait::async_trait;
4use futures::future::join_all;
5
6use reqwest::Response;
7
8use crate::args::Network;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fmt;
12use std::fmt::{Debug, Formatter};
13
14#[derive(Debug)]
15pub struct ResponsePayloadWithEndpointInfo {
16    pub payload: Result<SuccessEndpointPayload, AppError>,
17    pub endpoint: String,
18}
19
20#[derive(Serialize, Deserialize, Debug, Clone)]
21pub struct SuccessEndpointPayload {
22    pub data: Data,
23}
24
25#[derive(Serialize, Deserialize, Debug, Clone)]
26pub struct BlockInfo {
27    pub epoch: String,
28    pub root: String,
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone)]
32pub struct Data {
33    pub finalized: BlockInfo,
34    pub current_justified: BlockInfo,
35    pub previous_justified: BlockInfo,
36}
37
38#[derive(Debug, Clone)]
39pub struct CheckpointClient<C: HttpClient> {
40    client: C,
41    endpoints_config: EndpointsConfig,
42    state_id: StateId,
43}
44
45#[derive(Debug, Clone)]
46pub enum StateId {
47    Finalized,
48    Slot(u128), // TODO is u128 to big?
49}
50
51impl fmt::Display for StateId {
52    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53        match self {
54            StateId::Finalized => write!(f, "finalized"),
55            StateId::Slot(slot) => write!(f, "{:?}", slot.to_string()),
56        }
57    }
58}
59
60#[derive(Debug, Deserialize, Clone)]
61pub struct EndpointsConfig {
62    pub endpoints: HashMap<String, Vec<String>>,
63}
64
65#[async_trait]
66pub trait HttpClient {
67    async fn send_request(&self, path: String) -> Result<Response, AppError>;
68}
69
70#[async_trait]
71impl HttpClient for reqwest::Client {
72    async fn send_request(&self, path: String) -> Result<Response, AppError> {
73        self.get(path)
74            .send()
75            .await
76            .map_err(|e| AppError::EndpointResponseError(e.to_string()))
77    }
78}
79
80impl<C: HttpClient> CheckpointClient<C> {
81    pub fn new(client: C, state_id: StateId, endpoints: EndpointsConfig) -> Self {
82        Self {
83            client,
84            endpoints_config: endpoints,
85            state_id,
86        }
87    }
88    pub async fn fetch_finality_checkpoints(
89        &self,
90        network: Network,
91    ) -> Result<DisplayableResult, AppError> {
92        let endpoints_config = &self.endpoints_config;
93        let endpoints: &Vec<String> = endpoints_config.endpoints.get(&network.to_string().to_lowercase()).ok_or(
94            AppError::EndpointsNotFound(format!(r#"Endpoint not found for {network} network. Ensure it is present in the config file and the network name is specified in lowercase."#)),
95        )?;
96
97        let results = join_all(endpoints.iter().map(|endpoint| async {
98            let raw_response = async {
99                let path = format!(
100                    "{}/eth/v1/beacon/states/{}/finality_checkpoints",
101                    endpoint.clone(),
102                    self.state_id
103                );
104                let result = self.client.send_request(path);
105                match result.await {
106                    Ok(res) => {
107                        if res.status().is_success() {
108                            ResponsePayloadWithEndpointInfo {
109                                payload: res
110                                    .json::<SuccessEndpointPayload>()
111                                    .await
112                                    .map_err(|e| AppError::EndpointResponseError(e.to_string())),
113                                endpoint: endpoint.clone(),
114                            }
115                        } else {
116                            ResponsePayloadWithEndpointInfo {
117                                payload: Err(AppError::EndpointResponseError(format!(
118                                    "Error with calling {} status code {}",
119                                    endpoint.clone(),
120                                    res.status().to_string()
121                                ))),
122                                endpoint: endpoint.clone(),
123                            }
124                        }
125                    }
126                    Err(e) => ResponsePayloadWithEndpointInfo {
127                        payload: Err(e),
128                        endpoint: endpoint.clone(),
129                    },
130                }
131            }
132            .await;
133            raw_response
134        }))
135        .await;
136
137        Ok(process_to_displayable_format(results))
138    }
139}