1use std::sync::Arc;
35
36use anyhow::Context;
37use async_trait::async_trait;
38use serde::{Deserialize, Serialize};
39
40use mithril_common::entities::SupportedEra;
41
42use crate::{
43 MithrilResult,
44 aggregator_client::{AggregatorClient, AggregatorRequest},
45};
46
47pub struct MithrilEraClient {
49 era_fetcher: Arc<dyn EraFetcher>,
50}
51
52impl MithrilEraClient {
53 pub fn new(era_fetcher: Arc<dyn EraFetcher>) -> Self {
55 Self { era_fetcher }
56 }
57
58 pub async fn fetch_current(&self) -> MithrilResult<FetchedEra> {
60 self.era_fetcher.fetch_current_era().await
61 }
62}
63
64#[derive(Debug, PartialEq, Serialize, Deserialize)]
66pub struct FetchedEra {
67 pub era: String,
69}
70
71impl FetchedEra {
72 pub fn to_supported_era(&self) -> MithrilResult<SupportedEra> {
74 self.era
75 .parse::<SupportedEra>()
76 .with_context(|| format!("Unknown supported era: {}", self.era))
77 }
78}
79
80#[cfg_attr(target_family = "wasm", async_trait(?Send))]
82#[cfg_attr(not(target_family = "wasm"), async_trait)]
83pub trait EraFetcher: Send + Sync {
84 async fn fetch_current_era(&self) -> MithrilResult<FetchedEra>;
86}
87
88pub struct AggregatorHttpEraFetcher {
91 aggregator_client: Arc<dyn AggregatorClient>,
92}
93
94impl AggregatorHttpEraFetcher {
95 pub fn new(aggregator_client: Arc<dyn AggregatorClient>) -> Self {
97 Self { aggregator_client }
98 }
99}
100
101#[cfg_attr(target_family = "wasm", async_trait(?Send))]
102#[cfg_attr(not(target_family = "wasm"), async_trait)]
103impl EraFetcher for AggregatorHttpEraFetcher {
104 async fn fetch_current_era(&self) -> MithrilResult<FetchedEra> {
105 #[derive(serde::Deserialize)]
106 struct Era {
107 mithril_era: String,
108 }
109
110 let response = self
111 .aggregator_client
112 .get_content(AggregatorRequest::Status)
113 .await
114 .with_context(|| "Failed to fetch the aggregator status route")?;
115
116 let era = serde_json::from_str::<Era>(&response)
117 .with_context(|| "Failed to parse aggregator status message as JSON value")?
118 .mithril_era;
119
120 Ok(FetchedEra { era })
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use mithril_common::{
127 entities::{Epoch, ProtocolParameters, SupportedEra},
128 messages::AggregatorStatusMessage,
129 };
130 use mockall::predicate::eq;
131
132 use crate::aggregator_client::MockAggregatorClient;
133
134 use super::*;
135
136 fn dummy_aggregator_status_message() -> AggregatorStatusMessage {
137 AggregatorStatusMessage {
138 epoch: Epoch(48),
139 cardano_era: "conway".to_string(),
140 cardano_network: "mainnet".to_string(),
141 mithril_era: SupportedEra::Pythagoras,
142 cardano_node_version: "1.2.3".to_string(),
143 aggregator_node_version: "4.5.6".to_string(),
144 protocol_parameters: ProtocolParameters {
145 k: 5,
146 m: 100,
147 phi_f: 0.65,
148 },
149 next_protocol_parameters: ProtocolParameters {
150 k: 50,
151 m: 1000,
152 phi_f: 0.65,
153 },
154 total_signers: 1234,
155 total_next_signers: 56789,
156 total_stakes_signers: 123456789,
157 total_next_stakes_signers: 987654321,
158 total_cardano_spo: 7777,
159 total_cardano_stake: 888888888,
160 }
161 }
162
163 #[tokio::test]
164 async fn fetch_current_era_should_return_mithril_era_from_aggregator_status() {
165 let aggregator_status_message = AggregatorStatusMessage {
166 mithril_era: SupportedEra::Pythagoras,
167 ..dummy_aggregator_status_message()
168 };
169 let mock_aggregator_client = {
170 let mut mock_client = MockAggregatorClient::new();
171 mock_client
172 .expect_get_content()
173 .with(eq(AggregatorRequest::Status))
174 .return_once(move |_| {
175 Ok(serde_json::to_string(&aggregator_status_message).unwrap())
176 });
177
178 Arc::new(mock_client)
179 };
180 let era_fetcher = AggregatorHttpEraFetcher::new(mock_aggregator_client);
181
182 let fetched_era = era_fetcher.fetch_current_era().await.unwrap();
183
184 assert_eq!(
185 {
186 FetchedEra {
187 era: SupportedEra::Pythagoras.to_string(),
188 }
189 },
190 fetched_era
191 );
192 }
193
194 #[tokio::test]
195 async fn fetch_current_era_returns_error_if_response_is_invalid_json() {
196 let mock_aggregator_client = {
197 let mut mock_client = MockAggregatorClient::new();
198 mock_client
199 .expect_get_content()
200 .with(eq(AggregatorRequest::Status))
201 .return_once(|_| Ok("invalid_json".to_string()));
202
203 Arc::new(mock_client)
204 };
205 let era_fetcher = AggregatorHttpEraFetcher::new(mock_aggregator_client);
206
207 era_fetcher
208 .fetch_current_era()
209 .await
210 .expect_err("Expected an error due to invalid JSON response");
211 }
212
213 #[tokio::test]
214 async fn fetch_current_era_returns_error_if_mithril_era_field_is_missing() {
215 let mut response_json = serde_json::json!(dummy_aggregator_status_message());
216 response_json.as_object_mut().unwrap().remove("mithril_era");
217 let mock_aggregator_client = {
218 let mut mock_client = MockAggregatorClient::new();
219 mock_client
220 .expect_get_content()
221 .with(eq(AggregatorRequest::Status))
222 .return_once(move |_| Ok(response_json.to_string()));
223
224 Arc::new(mock_client)
225 };
226 let era_fetcher = AggregatorHttpEraFetcher::new(mock_aggregator_client);
227
228 era_fetcher.fetch_current_era().await.expect_err(
229 "Expected an error due to missing 'mithril_era' field in aggregator status",
230 );
231 }
232
233 #[test]
234 fn to_supported_era_should_return_enum_variant_for_known_era() {
235 let fetched_era = FetchedEra {
236 era: SupportedEra::Pythagoras.to_string(),
237 };
238
239 let supported_era = fetched_era.to_supported_era().unwrap();
240
241 assert_eq!(SupportedEra::Pythagoras, supported_era);
242 }
243
244 #[test]
245 fn to_supported_era_returns_error_for_unsupported_era() {
246 let fetched_era = FetchedEra {
247 era: "unsupported_era".to_string(),
248 };
249
250 fetched_era
251 .to_supported_era()
252 .expect_err("Expected an error when converting an unsupported era to 'SupportedEra'");
253 }
254}