mithril_client/
cardano_stake_distribution_client.rs

1//! A client to retrieve Cardano stake distributions data from an Aggregator.
2//!
3//! In order to do so it defines a [CardanoStakeDistributionClient] which exposes the following features:
4//!  - [get][CardanoStakeDistributionClient::get]: get a Cardano stake distribution data from its hash
5//!  - [get_by_epoch][CardanoStakeDistributionClient::get_by_epoch]: get a Cardano stake distribution data from its epoch
6//!  - [list][CardanoStakeDistributionClient::list]: get the list of available Cardano stake distribution
7//!
8//! # Get a Cardano stake distribution
9//!
10//! To get a Cardano stake distribution using the [ClientBuilder][crate::client::ClientBuilder].
11//!
12//! ```no_run
13//! # async fn run() -> mithril_client::MithrilResult<()> {
14//! use mithril_client::ClientBuilder;
15//!
16//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
17//! let cardano_stake_distribution = client.cardano_stake_distribution().get("CARDANO_STAKE_DISTRIBUTION_HASH").await?.unwrap();
18//!
19//! println!(
20//!     "Cardano stake distribution hash={}, epoch={}, stake_distribution={:?}",
21//!     cardano_stake_distribution.hash,
22//!     cardano_stake_distribution.epoch,
23//!     cardano_stake_distribution.stake_distribution
24//! );
25//! #    Ok(())
26//! # }
27//! ```
28//!
29//! # List available Cardano stake distributions
30//!
31//! To list available Cardano stake distributions using the [ClientBuilder][crate::client::ClientBuilder].
32//!
33//! ```no_run
34//! # async fn run() -> mithril_client::MithrilResult<()> {
35//! use mithril_client::ClientBuilder;
36//!
37//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
38//! let cardano_stake_distributions = client.cardano_stake_distribution().list().await?;
39//!
40//! for cardano_stake_distribution in cardano_stake_distributions {
41//!     println!("Cardano stake distribution hash={}, epoch={}", cardano_stake_distribution.hash, cardano_stake_distribution.epoch);
42//! }
43//! #    Ok(())
44//! # }
45//! ```
46//!
47//! # Get a Cardano stake distribution by epoch
48//!
49//! To get a Cardano stake distribution by epoch using the [ClientBuilder][crate::client::ClientBuilder].
50//!
51//! **Note:** The epoch represents the epoch at the end of which the Cardano stake distribution is computed by the Cardano node
52//!
53//! ```no_run
54//! # async fn run() -> mithril_client::MithrilResult<()> {
55//! use mithril_client::{ClientBuilder, common::Epoch};
56//!
57//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
58//! // For a specific epoch
59//! let cardano_stake_distribution = client.cardano_stake_distribution().get_by_epoch(Epoch(500)).await?.unwrap();
60//! // For the latest epoch known by the Mithril aggregator
61//! let cardano_stake_distribution = client.cardano_stake_distribution().get_for_latest_epoch().await?.unwrap();
62//! // For the latest epoch known by the Mithril aggregator with an offset
63//! let cardano_stake_distribution = client.cardano_stake_distribution().get_for_latest_epoch_with_offset(4).await?.unwrap();
64//!
65//! println!(
66//!     "Cardano stake distribution hash={}, epoch={}, stake_distribution={:?}",
67//!     cardano_stake_distribution.hash,
68//!     cardano_stake_distribution.epoch,
69//!     cardano_stake_distribution.stake_distribution
70//! );
71//! #    Ok(())
72//! # }
73//! ```
74
75use anyhow::Context;
76use std::sync::Arc;
77
78use crate::aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest};
79use crate::common::Epoch;
80use crate::{CardanoStakeDistribution, CardanoStakeDistributionListItem, MithrilResult};
81
82/// HTTP client for CardanoStakeDistribution API from the Aggregator
83pub struct CardanoStakeDistributionClient {
84    aggregator_client: Arc<dyn AggregatorClient>,
85}
86
87impl CardanoStakeDistributionClient {
88    /// Constructs a new `CardanoStakeDistribution`.
89    pub fn new(aggregator_client: Arc<dyn AggregatorClient>) -> Self {
90        Self { aggregator_client }
91    }
92
93    /// Fetch a list of signed CardanoStakeDistribution
94    pub async fn list(&self) -> MithrilResult<Vec<CardanoStakeDistributionListItem>> {
95        let response = self
96            .aggregator_client
97            .get_content(AggregatorRequest::ListCardanoStakeDistributions)
98            .await
99            .with_context(|| "CardanoStakeDistribution client can not get the artifact list")?;
100        let items = serde_json::from_str::<Vec<CardanoStakeDistributionListItem>>(&response)
101            .with_context(|| "CardanoStakeDistribution client can not deserialize artifact list")?;
102
103        Ok(items)
104    }
105
106    /// Get the given Cardano stake distribution data by hash.
107    pub async fn get(&self, hash: &str) -> MithrilResult<Option<CardanoStakeDistribution>> {
108        self.fetch_with_aggregator_request(AggregatorRequest::GetCardanoStakeDistribution {
109            hash: hash.to_string(),
110        })
111        .await
112    }
113
114    /// Get the given Cardano stake distribution data by epoch.
115    pub async fn get_by_epoch(
116        &self,
117        epoch: Epoch,
118    ) -> MithrilResult<Option<CardanoStakeDistribution>> {
119        self.fetch_with_aggregator_request(AggregatorRequest::GetCardanoStakeDistributionByEpoch {
120            epoch,
121        })
122        .await
123    }
124
125    /// Get the given Cardano stake distribution data by epoch.
126    pub async fn get_for_latest_epoch(&self) -> MithrilResult<Option<CardanoStakeDistribution>> {
127        self.fetch_with_aggregator_request(
128            AggregatorRequest::GetCardanoStakeDistributionForLatestEpoch { offset: None },
129        )
130        .await
131    }
132
133    /// Get the given Cardano stake distribution data by epoch.
134    pub async fn get_for_latest_epoch_with_offset(
135        &self,
136        offset: u64,
137    ) -> MithrilResult<Option<CardanoStakeDistribution>> {
138        self.fetch_with_aggregator_request(
139            AggregatorRequest::GetCardanoStakeDistributionForLatestEpoch {
140                offset: Some(offset),
141            },
142        )
143        .await
144    }
145
146    /// Fetch the given Cardano stake distribution data with an aggregator request.
147    /// If it cannot be found, a None is returned.
148    async fn fetch_with_aggregator_request(
149        &self,
150        request: AggregatorRequest,
151    ) -> MithrilResult<Option<CardanoStakeDistribution>> {
152        match self.aggregator_client.get_content(request).await {
153            Ok(content) => {
154                let cardano_stake_distribution: CardanoStakeDistribution = serde_json::from_str(
155                    &content,
156                )
157                .with_context(|| "CardanoStakeDistribution client can not deserialize artifact")?;
158
159                Ok(Some(cardano_stake_distribution))
160            }
161            Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
162            Err(e) => Err(e.into()),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use anyhow::anyhow;
170    use chrono::{DateTime, Utc};
171    use mockall::predicate::eq;
172
173    use crate::aggregator_client::MockAggregatorClient;
174    use crate::common::test::Dummy;
175
176    use super::*;
177
178    fn fake_messages() -> Vec<CardanoStakeDistributionListItem> {
179        vec![
180            CardanoStakeDistributionListItem {
181                epoch: Epoch(1),
182                hash: "hash-123".to_string(),
183                certificate_hash: "cert-hash-123".to_string(),
184                created_at: DateTime::parse_from_rfc3339("2024-08-06T12:13:05.618857482Z")
185                    .unwrap()
186                    .with_timezone(&Utc),
187            },
188            CardanoStakeDistributionListItem {
189                epoch: Epoch(2),
190                hash: "hash-456".to_string(),
191                certificate_hash: "cert-hash-456".to_string(),
192                created_at: DateTime::parse_from_rfc3339("2024-08-06T12:13:05.618857482Z")
193                    .unwrap()
194                    .with_timezone(&Utc),
195            },
196        ]
197    }
198
199    #[tokio::test]
200    async fn fetching_cardano_stake_distribution_from_aggregator_client_returns_error_when_invalid_json_structure_in_response()
201     {
202        let mut http_client = MockAggregatorClient::new();
203        http_client
204            .expect_get_content()
205            .return_once(move |_| Ok("invalid json structure".to_string()));
206        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
207
208        client
209            .fetch_with_aggregator_request(AggregatorRequest::ListCardanoStakeDistributions)
210            .await
211            .expect_err("Get Cardano stake distribution should return an error");
212    }
213
214    #[tokio::test]
215    async fn fetching_cardano_stake_distribution_from_aggregator_client_returns_none_when_not_found_or_remote_server_logical_error()
216     {
217        let mut http_client = MockAggregatorClient::new();
218        http_client.expect_get_content().return_once(move |_| {
219            Err(AggregatorClientError::RemoteServerLogical(anyhow!(
220                "not found"
221            )))
222        });
223        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
224
225        let result = client
226            .fetch_with_aggregator_request(AggregatorRequest::ListCardanoStakeDistributions)
227            .await
228            .unwrap();
229
230        assert!(result.is_none());
231    }
232
233    #[tokio::test]
234    async fn fetching_cardano_stake_distribution_from_aggregator_client_returns_error() {
235        let mut http_client = MockAggregatorClient::new();
236        http_client
237            .expect_get_content()
238            .return_once(move |_| Err(AggregatorClientError::SubsystemError(anyhow!("error"))));
239        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
240
241        client
242            .fetch_with_aggregator_request(AggregatorRequest::ListCardanoStakeDistributions)
243            .await
244            .expect_err("Get Cardano stake distribution should return an error");
245    }
246
247    #[tokio::test]
248    async fn list_cardano_stake_distributions_returns_messages() {
249        let message = fake_messages();
250        let mut http_client = MockAggregatorClient::new();
251        http_client
252            .expect_get_content()
253            .with(eq(AggregatorRequest::ListCardanoStakeDistributions))
254            .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
255        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
256
257        let messages = client.list().await.unwrap();
258
259        assert_eq!(2, messages.len());
260        assert_eq!("hash-123".to_string(), messages[0].hash);
261        assert_eq!("hash-456".to_string(), messages[1].hash);
262    }
263
264    #[tokio::test]
265    async fn get_cardano_stake_distribution_returns_message() {
266        let expected_message = CardanoStakeDistribution::dummy();
267        let mut http_client = MockAggregatorClient::new();
268        http_client
269            .expect_get_content()
270            .with(eq(AggregatorRequest::GetCardanoStakeDistribution {
271                hash: expected_message.hash.clone(),
272            }))
273            .return_once(move |_| {
274                Ok(serde_json::to_string(&CardanoStakeDistribution::dummy()).unwrap())
275            });
276        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
277
278        let cardano_stake_distribution = client
279            .get(&expected_message.hash)
280            .await
281            .unwrap()
282            .expect("This test returns a Cardano stake distribution");
283
284        assert_eq!(expected_message, cardano_stake_distribution);
285    }
286
287    #[tokio::test]
288    async fn get_cardano_stake_distribution_by_epoch_returns_message() {
289        let expected_message = CardanoStakeDistribution::dummy();
290        let mut http_client = MockAggregatorClient::new();
291        http_client
292            .expect_get_content()
293            .with(eq(AggregatorRequest::GetCardanoStakeDistributionByEpoch {
294                epoch: expected_message.epoch,
295            }))
296            .return_once(move |_| {
297                Ok(serde_json::to_string(&CardanoStakeDistribution::dummy()).unwrap())
298            });
299        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
300
301        let cardano_stake_distribution = client
302            .get_by_epoch(expected_message.epoch)
303            .await
304            .unwrap()
305            .expect("This test returns a Cardano stake distribution");
306
307        assert_eq!(expected_message, cardano_stake_distribution);
308    }
309
310    #[tokio::test]
311    async fn get_cardano_stake_distribution_for_latest_epoch_returns_message() {
312        let expected_message = CardanoStakeDistribution::dummy();
313        let mut http_client = MockAggregatorClient::new();
314        http_client
315            .expect_get_content()
316            .with(eq(
317                AggregatorRequest::GetCardanoStakeDistributionForLatestEpoch { offset: None },
318            ))
319            .return_once(move |_| {
320                Ok(serde_json::to_string(&CardanoStakeDistribution::dummy()).unwrap())
321            });
322        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
323
324        let cardano_stake_distribution = client
325            .get_for_latest_epoch()
326            .await
327            .unwrap()
328            .expect("This test returns a Cardano stake distribution");
329
330        assert_eq!(expected_message, cardano_stake_distribution);
331    }
332
333    #[tokio::test]
334    async fn get_cardano_stake_distribution_for_latest_with_offset_epoch_returns_message() {
335        let expected_message = CardanoStakeDistribution::dummy();
336        let mut http_client = MockAggregatorClient::new();
337        http_client
338            .expect_get_content()
339            .with(eq(
340                AggregatorRequest::GetCardanoStakeDistributionForLatestEpoch { offset: Some(4) },
341            ))
342            .return_once(move |_| {
343                Ok(serde_json::to_string(&CardanoStakeDistribution::dummy()).unwrap())
344            });
345        let client = CardanoStakeDistributionClient::new(Arc::new(http_client));
346
347        let cardano_stake_distribution = client
348            .get_for_latest_epoch_with_offset(4)
349            .await
350            .unwrap()
351            .expect("This test returns a Cardano stake distribution");
352
353        assert_eq!(expected_message, cardano_stake_distribution);
354    }
355}