mithril_client/cardano_database_client/
fetch.rs

1use std::sync::Arc;
2
3use anyhow::Context;
4use serde::de::DeserializeOwned;
5
6use crate::{
7    CardanoDatabaseSnapshot, CardanoDatabaseSnapshotListItem, MithrilResult,
8    aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest},
9    common::Epoch,
10};
11
12pub struct InternalArtifactRetriever {
13    pub(super) aggregator_client: Arc<dyn AggregatorClient>,
14}
15
16impl InternalArtifactRetriever {
17    /// Constructs a new `InternalArtifactRetriever`
18    pub fn new(aggregator_client: Arc<dyn AggregatorClient>) -> Self {
19        Self { aggregator_client }
20    }
21
22    /// Fetch a list of signed CardanoDatabase
23    pub async fn list(&self) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
24        self.fetch_list_with_aggregator_request(AggregatorRequest::ListCardanoDatabaseSnapshots)
25            .await
26    }
27
28    /// Fetch a list of signed CardanoDatabase for a given epoch
29    pub async fn list_by_epoch(
30        &self,
31        epoch: Epoch,
32    ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
33        self.fetch_list_with_aggregator_request(
34            AggregatorRequest::ListCardanoDatabaseSnapshotByEpoch { epoch },
35        )
36        .await
37    }
38
39    /// Fetch a list of signed CardanoDatabase for the latest epoch
40    pub async fn list_for_latest_epoch(
41        &self,
42    ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
43        self.fetch_list_with_aggregator_request(
44            AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch { offset: None },
45        )
46        .await
47    }
48
49    /// Fetch a list of signed CardanoDatabase for the latest epoch minus the given offset
50    pub async fn list_for_latest_epoch_with_offset(
51        &self,
52        offset: u64,
53    ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
54        self.fetch_list_with_aggregator_request(
55            AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch {
56                offset: Some(offset),
57            },
58        )
59        .await
60    }
61
62    /// Get the given Cardano database data by hash.
63    pub async fn get(&self, hash: &str) -> MithrilResult<Option<CardanoDatabaseSnapshot>> {
64        self.fetch_with_aggregator_request(AggregatorRequest::GetCardanoDatabaseSnapshot {
65            hash: hash.to_string(),
66        })
67        .await
68    }
69
70    /// Fetch the given Cardano database data with an aggregator request.
71    /// If it cannot be found, None is returned.
72    async fn fetch_with_aggregator_request<T: DeserializeOwned>(
73        &self,
74        request: AggregatorRequest,
75    ) -> MithrilResult<Option<T>> {
76        match self.aggregator_client.get_content(request).await {
77            Ok(content) => {
78                let result = serde_json::from_str(&content)
79                    .with_context(|| "CardanoDatabase client can not deserialize artifact")?;
80
81                Ok(Some(result))
82            }
83            Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
84            Err(e) => Err(e.into()),
85        }
86    }
87
88    async fn fetch_list_with_aggregator_request(
89        &self,
90        request: AggregatorRequest,
91    ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
92        let response = self
93            .aggregator_client
94            .get_content(request)
95            .await
96            .with_context(|| "CardanoDatabase client can not get the artifact list")?;
97
98        serde_json::from_str(&response)
99            .with_context(|| "CardanoDatabase client can not deserialize artifact list")
100    }
101}
102
103#[cfg(test)]
104mod tests {
105
106    use anyhow::anyhow;
107    use chrono::{DateTime, Utc};
108    use mockall::predicate::eq;
109
110    use mithril_common::entities::{CardanoDbBeacon, Epoch};
111    use mithril_common::test::double::Dummy;
112
113    use crate::aggregator_client;
114    use crate::cardano_database_client::CardanoDatabaseClientDependencyInjector;
115
116    use super::*;
117
118    fn config_aggregator_client_to_always_returns_invalid_json(
119        http_client: &mut aggregator_client::MockAggregatorClient,
120    ) {
121        http_client
122            .expect_get_content()
123            .returning(move |_| Ok("invalid json structure".to_string()));
124    }
125
126    fn fake_messages() -> Vec<CardanoDatabaseSnapshotListItem> {
127        vec![
128            CardanoDatabaseSnapshotListItem {
129                hash: "hash-123".to_string(),
130                merkle_root: "mkroot-123".to_string(),
131                beacon: CardanoDbBeacon {
132                    epoch: Epoch(1),
133                    immutable_file_number: 123,
134                },
135                certificate_hash: "cert-hash-123".to_string(),
136                total_db_size_uncompressed: 800796318,
137                created_at: DateTime::parse_from_rfc3339("2025-01-19T13:43:05.618857482Z")
138                    .unwrap()
139                    .with_timezone(&Utc),
140                cardano_node_version: "0.0.1".to_string(),
141            },
142            CardanoDatabaseSnapshotListItem {
143                hash: "hash-456".to_string(),
144                merkle_root: "mkroot-456".to_string(),
145                beacon: CardanoDbBeacon {
146                    epoch: Epoch(2),
147                    immutable_file_number: 456,
148                },
149                certificate_hash: "cert-hash-456".to_string(),
150                total_db_size_uncompressed: 2960713808,
151                created_at: DateTime::parse_from_rfc3339("2025-01-27T15:22:05.618857482Z")
152                    .unwrap()
153                    .with_timezone(&Utc),
154                cardano_node_version: "0.0.1".to_string(),
155            },
156        ]
157    }
158
159    mod list {
160
161        use super::*;
162
163        #[tokio::test]
164        async fn list_cardano_database_snapshots_returns_messages() {
165            let message = fake_messages();
166            let client = CardanoDatabaseClientDependencyInjector::new()
167                .with_aggregator_client_mock_config(|http_client| {
168                    http_client
169                        .expect_get_content()
170                        .with(eq(AggregatorRequest::ListCardanoDatabaseSnapshots))
171                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
172                })
173                .build_cardano_database_client();
174
175            let messages = client.list().await.unwrap();
176
177            assert_eq!(2, messages.len());
178            assert_eq!("hash-123".to_string(), messages[0].hash);
179            assert_eq!("hash-456".to_string(), messages[1].hash);
180        }
181
182        #[tokio::test]
183        async fn list_cardano_database_snapshots_returns_error_when_invalid_json_structure_in_response()
184         {
185            let client = CardanoDatabaseClientDependencyInjector::new()
186                .with_aggregator_client_mock_config(
187                    config_aggregator_client_to_always_returns_invalid_json,
188                )
189                .build_cardano_database_client();
190
191            client
192                .list()
193                .await
194                .expect_err("List Cardano databases should return an error");
195        }
196
197        #[tokio::test]
198        async fn list_cardano_database_snapshots_by_epoch_returns_messages() {
199            let message = fake_messages();
200            let client = CardanoDatabaseClientDependencyInjector::new()
201                .with_aggregator_client_mock_config(|http_client| {
202                    http_client
203                        .expect_get_content()
204                        .with(eq(AggregatorRequest::ListCardanoDatabaseSnapshotByEpoch {
205                            epoch: Epoch(4),
206                        }))
207                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
208                })
209                .build_cardano_database_client();
210
211            let messages = client.list_by_epoch(Epoch(4)).await.unwrap();
212
213            assert_eq!(2, messages.len());
214            assert_eq!("hash-123".to_string(), messages[0].hash);
215            assert_eq!("hash-456".to_string(), messages[1].hash);
216        }
217
218        #[tokio::test]
219        async fn list_cardano_database_snapshots_returns_by_epoch_error_when_invalid_json_structure_in_response()
220         {
221            let client = CardanoDatabaseClientDependencyInjector::new()
222                .with_aggregator_client_mock_config(
223                    config_aggregator_client_to_always_returns_invalid_json,
224                )
225                .build_cardano_database_client();
226
227            client
228                .list_by_epoch(Epoch(4))
229                .await
230                .expect_err("List Cardano databases should return an error");
231        }
232
233        #[tokio::test]
234        async fn list_cardano_database_snapshots_for_latest_epoch_returns_messages() {
235            let message = fake_messages();
236            let client = CardanoDatabaseClientDependencyInjector::new()
237                .with_aggregator_client_mock_config(|http_client| {
238                    http_client
239                        .expect_get_content()
240                        .with(eq(
241                            AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch {
242                                offset: None,
243                            },
244                        ))
245                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
246                })
247                .build_cardano_database_client();
248
249            let messages = client.list_for_latest_epoch().await.unwrap();
250
251            assert_eq!(2, messages.len());
252            assert_eq!("hash-123".to_string(), messages[0].hash);
253            assert_eq!("hash-456".to_string(), messages[1].hash);
254        }
255
256        #[tokio::test]
257        async fn list_cardano_database_snapshots_returns_for_latest_epoch_error_when_invalid_json_structure_in_response()
258         {
259            let client = CardanoDatabaseClientDependencyInjector::new()
260                .with_aggregator_client_mock_config(
261                    config_aggregator_client_to_always_returns_invalid_json,
262                )
263                .build_cardano_database_client();
264
265            client
266                .list_for_latest_epoch()
267                .await
268                .expect_err("List Cardano databases should return an error");
269        }
270
271        #[tokio::test]
272        async fn list_cardano_database_snapshots_for_latest_epoch_with_offset_returns_messages() {
273            let message = fake_messages();
274            let client = CardanoDatabaseClientDependencyInjector::new()
275                .with_aggregator_client_mock_config(|http_client| {
276                    http_client
277                        .expect_get_content()
278                        .with(eq(
279                            AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch {
280                                offset: Some(42),
281                            },
282                        ))
283                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
284                })
285                .build_cardano_database_client();
286
287            let messages = client.list_for_latest_epoch_with_offset(42).await.unwrap();
288
289            assert_eq!(2, messages.len());
290            assert_eq!("hash-123".to_string(), messages[0].hash);
291            assert_eq!("hash-456".to_string(), messages[1].hash);
292        }
293
294        #[tokio::test]
295        async fn list_cardano_database_snapshots_returns_for_latest_epoch_with_offset_error_when_invalid_json_structure_in_response()
296         {
297            let client = CardanoDatabaseClientDependencyInjector::new()
298                .with_aggregator_client_mock_config(
299                    config_aggregator_client_to_always_returns_invalid_json,
300                )
301                .build_cardano_database_client();
302
303            client
304                .list_for_latest_epoch_with_offset(42)
305                .await
306                .expect_err("List Cardano databases should return an error");
307        }
308    }
309
310    mod get {
311        use super::*;
312
313        #[tokio::test]
314        async fn get_cardano_database_snapshot_returns_message() {
315            let expected_cardano_database_snapshot = CardanoDatabaseSnapshot {
316                hash: "hash-123".to_string(),
317                ..CardanoDatabaseSnapshot::dummy()
318            };
319            let message = expected_cardano_database_snapshot.clone();
320            let client = CardanoDatabaseClientDependencyInjector::new()
321                .with_aggregator_client_mock_config(|http_client| {
322                    http_client
323                        .expect_get_content()
324                        .with(eq(AggregatorRequest::GetCardanoDatabaseSnapshot {
325                            hash: "hash-123".to_string(),
326                        }))
327                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
328                })
329                .build_cardano_database_client();
330
331            let cardano_database = client
332                .get("hash-123")
333                .await
334                .unwrap()
335                .expect("This test returns a Cardano database");
336
337            assert_eq!(expected_cardano_database_snapshot, cardano_database);
338        }
339
340        #[tokio::test]
341        async fn get_cardano_database_snapshot_returns_error_when_invalid_json_structure_in_response()
342         {
343            let client = CardanoDatabaseClientDependencyInjector::new()
344                .with_aggregator_client_mock_config(
345                    config_aggregator_client_to_always_returns_invalid_json,
346                )
347                .build_cardano_database_client();
348
349            client
350                .get("hash-123")
351                .await
352                .expect_err("Get Cardano database should return an error");
353        }
354
355        #[tokio::test]
356        async fn get_cardano_database_snapshot_returns_none_when_not_found_or_remote_server_logical_error()
357         {
358            let client = CardanoDatabaseClientDependencyInjector::new()
359                .with_aggregator_client_mock_config(|http_client| {
360                    http_client.expect_get_content().return_once(move |_| {
361                        Err(AggregatorClientError::RemoteServerLogical(anyhow!(
362                            "not found"
363                        )))
364                    });
365                })
366                .build_cardano_database_client();
367
368            let result = client.get("hash-123").await.unwrap();
369
370            assert!(result.is_none());
371        }
372
373        #[tokio::test]
374        async fn get_cardano_database_snapshot_returns_error() {
375            let client = CardanoDatabaseClientDependencyInjector::new()
376                .with_aggregator_client_mock_config(|http_client| {
377                    http_client.expect_get_content().return_once(move |_| {
378                        Err(AggregatorClientError::SubsystemError(anyhow!("error")))
379                    });
380                })
381                .build_cardano_database_client();
382
383            client
384                .get("hash-123")
385                .await
386                .expect_err("Get Cardano database should return an error");
387        }
388    }
389}