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