1use anyhow::{anyhow, Context};
2use reqwest::Url;
3use serde::{Deserialize, Serialize};
4use slog::{o, Logger};
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use mithril_common::api_version::APIVersionProvider;
9use mithril_common::MITHRIL_ORIGIN_TAG_HEADER;
10
11use crate::aggregator_client::{AggregatorClient, AggregatorHTTPClient};
12#[cfg(feature = "unstable")]
13use crate::cardano_database_client::CardanoDatabaseClient;
14use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
15use crate::cardano_transaction_client::CardanoTransactionClient;
16#[cfg(feature = "unstable")]
17use crate::certificate_client::CertificateVerifierCache;
18use crate::certificate_client::{
19 CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
20};
21use crate::feedback::{FeedbackReceiver, FeedbackSender};
22#[cfg(feature = "fs")]
23use crate::file_downloader::{
24 FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
25};
26use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
27use crate::snapshot_client::SnapshotClient;
28#[cfg(feature = "fs")]
29use crate::utils::AncillaryVerifier;
30use crate::MithrilResult;
31
32#[cfg(target_family = "wasm")]
33const fn one_week_in_seconds() -> u32 {
34 604800
35}
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct ClientOptions {
40 pub http_headers: Option<HashMap<String, String>>,
42
43 #[cfg(target_family = "wasm")]
45 #[cfg_attr(target_family = "wasm", serde(default))]
46 pub origin_tag: Option<String>,
47
48 #[cfg(target_family = "wasm")]
50 #[cfg_attr(target_family = "wasm", serde(default))]
51 pub unstable: bool,
52
53 #[cfg(target_family = "wasm")]
59 #[cfg_attr(target_family = "wasm", serde(default))]
60 pub enable_certificate_chain_verification_cache: bool,
61
62 #[cfg(target_family = "wasm")]
69 #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
70 pub certificate_chain_verification_cache_duration_in_seconds: u32,
71}
72
73impl ClientOptions {
74 pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
76 Self {
77 http_headers,
78 #[cfg(target_family = "wasm")]
79 origin_tag: None,
80 #[cfg(target_family = "wasm")]
81 unstable: false,
82 #[cfg(target_family = "wasm")]
83 enable_certificate_chain_verification_cache: false,
84 #[cfg(target_family = "wasm")]
85 certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
86 }
87 }
88
89 #[cfg(target_family = "wasm")]
91 pub fn with_unstable_features(self, unstable: bool) -> Self {
92 Self { unstable, ..self }
93 }
94}
95
96#[derive(Clone)]
100pub struct Client {
101 certificate_client: Arc<CertificateClient>,
102 mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
103 snapshot_client: Arc<SnapshotClient>,
104 #[cfg(feature = "unstable")]
105 cardano_database_client: Arc<CardanoDatabaseClient>,
106 cardano_transaction_client: Arc<CardanoTransactionClient>,
107 cardano_stake_distribution_client: Arc<CardanoStakeDistributionClient>,
108}
109
110impl Client {
111 pub fn certificate(&self) -> Arc<CertificateClient> {
113 self.certificate_client.clone()
114 }
115
116 pub fn mithril_stake_distribution(&self) -> Arc<MithrilStakeDistributionClient> {
118 self.mithril_stake_distribution_client.clone()
119 }
120
121 #[deprecated(since = "0.11.9", note = "supersede by `cardano_database`")]
122 pub fn snapshot(&self) -> Arc<SnapshotClient> {
124 self.cardano_database()
125 }
126
127 pub fn cardano_database(&self) -> Arc<SnapshotClient> {
129 self.snapshot_client.clone()
130 }
131
132 #[cfg(feature = "unstable")]
134 pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
135 self.cardano_database_client.clone()
136 }
137
138 pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
140 self.cardano_transaction_client.clone()
141 }
142
143 pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
145 self.cardano_stake_distribution_client.clone()
146 }
147}
148
149pub struct ClientBuilder {
151 aggregator_endpoint: Option<String>,
152 genesis_verification_key: String,
153 origin_tag: Option<String>,
154 #[cfg(feature = "fs")]
155 ancillary_verification_key: Option<String>,
156 aggregator_client: Option<Arc<dyn AggregatorClient>>,
157 certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
158 #[cfg(feature = "fs")]
159 http_file_downloader: Option<Arc<dyn FileDownloader>>,
160 #[cfg(feature = "unstable")]
161 certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
162 logger: Option<Logger>,
163 feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
164 options: ClientOptions,
165}
166
167impl ClientBuilder {
168 pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder {
171 Self {
172 aggregator_endpoint: Some(endpoint.to_string()),
173 genesis_verification_key: genesis_verification_key.to_string(),
174 origin_tag: None,
175 #[cfg(feature = "fs")]
176 ancillary_verification_key: None,
177 aggregator_client: None,
178 certificate_verifier: None,
179 #[cfg(feature = "fs")]
180 http_file_downloader: None,
181 #[cfg(feature = "unstable")]
182 certificate_verifier_cache: None,
183 logger: None,
184 feedback_receivers: vec![],
185 options: ClientOptions::default(),
186 }
187 }
188
189 pub fn new(genesis_verification_key: &str) -> ClientBuilder {
194 Self {
195 aggregator_endpoint: None,
196 genesis_verification_key: genesis_verification_key.to_string(),
197 origin_tag: None,
198 #[cfg(feature = "fs")]
199 ancillary_verification_key: None,
200 aggregator_client: None,
201 certificate_verifier: None,
202 #[cfg(feature = "fs")]
203 http_file_downloader: None,
204 #[cfg(feature = "unstable")]
205 certificate_verifier_cache: None,
206 logger: None,
207 feedback_receivers: vec![],
208 options: ClientOptions::default(),
209 }
210 }
211
212 pub fn build(self) -> MithrilResult<Client> {
217 let logger = self
218 .logger
219 .clone()
220 .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
221
222 let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
223
224 let aggregator_client = match self.aggregator_client {
225 None => Arc::new(self.build_aggregator_client(logger.clone())?),
226 Some(client) => client,
227 };
228
229 let certificate_verifier = match self.certificate_verifier {
230 None => Arc::new(
231 MithrilCertificateVerifier::new(
232 aggregator_client.clone(),
233 &self.genesis_verification_key,
234 feedback_sender.clone(),
235 #[cfg(feature = "unstable")]
236 self.certificate_verifier_cache,
237 logger.clone(),
238 )
239 .with_context(|| "Building certificate verifier failed")?,
240 ),
241 Some(verifier) => verifier,
242 };
243 let certificate_client = Arc::new(CertificateClient::new(
244 aggregator_client.clone(),
245 certificate_verifier,
246 logger.clone(),
247 ));
248
249 let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
250 aggregator_client.clone(),
251 ));
252
253 #[cfg(feature = "fs")]
254 let http_file_downloader = match self.http_file_downloader {
255 None => Arc::new(RetryDownloader::new(
256 Arc::new(
257 HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
258 .with_context(|| "Building http file downloader failed")?,
259 ),
260 FileDownloadRetryPolicy::default(),
261 )),
262 Some(http_file_downloader) => http_file_downloader,
263 };
264
265 #[cfg(feature = "fs")]
266 let ancillary_verifier = match self.ancillary_verification_key {
267 None => None,
268 Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
269 verification_key
270 .try_into()
271 .with_context(|| "Building ancillary verifier failed")?,
272 ))),
273 };
274
275 let snapshot_client = Arc::new(SnapshotClient::new(
276 aggregator_client.clone(),
277 #[cfg(feature = "fs")]
278 http_file_downloader.clone(),
279 #[cfg(feature = "fs")]
280 ancillary_verifier.clone(),
281 #[cfg(feature = "fs")]
282 feedback_sender.clone(),
283 #[cfg(feature = "fs")]
284 logger.clone(),
285 ));
286
287 #[cfg(feature = "unstable")]
288 let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
289 aggregator_client.clone(),
290 #[cfg(feature = "fs")]
291 http_file_downloader,
292 #[cfg(feature = "fs")]
293 ancillary_verifier,
294 #[cfg(feature = "fs")]
295 feedback_sender,
296 #[cfg(feature = "fs")]
297 logger,
298 ));
299
300 let cardano_transaction_client =
301 Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
302
303 let cardano_stake_distribution_client =
304 Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
305
306 Ok(Client {
307 certificate_client,
308 mithril_stake_distribution_client,
309 snapshot_client,
310 #[cfg(feature = "unstable")]
311 cardano_database_client,
312 cardano_transaction_client,
313 cardano_stake_distribution_client,
314 })
315 }
316
317 fn build_aggregator_client(
318 &self,
319 logger: Logger,
320 ) -> Result<AggregatorHTTPClient, anyhow::Error> {
321 let endpoint = self
322 .aggregator_endpoint.as_ref()
323 .ok_or(anyhow!("No aggregator endpoint set: \
324 You must either provide an aggregator endpoint or your own AggregatorClient implementation"))?;
325 let endpoint_url = Url::parse(endpoint).with_context(|| {
326 format!("Invalid aggregator endpoint, it must be a correctly formed url: '{endpoint}'")
327 })?;
328
329 let headers = self.compute_http_headers();
330
331 AggregatorHTTPClient::new(
332 endpoint_url,
333 APIVersionProvider::compute_all_versions_sorted()
334 .with_context(|| "Could not compute aggregator api versions")?,
335 logger,
336 Some(headers),
337 )
338 .with_context(|| "Building aggregator client failed")
339 }
340
341 fn compute_http_headers(&self) -> HashMap<String, String> {
342 let mut headers = self.options.http_headers.clone().unwrap_or_default();
343 if let Some(origin_tag) = self.origin_tag.clone() {
344 headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
345 }
346
347 headers
348 }
349
350 pub fn with_aggregator_client(
352 mut self,
353 aggregator_client: Arc<dyn AggregatorClient>,
354 ) -> ClientBuilder {
355 self.aggregator_client = Some(aggregator_client);
356 self
357 }
358
359 pub fn with_certificate_verifier(
361 mut self,
362 certificate_verifier: Arc<dyn CertificateVerifier>,
363 ) -> ClientBuilder {
364 self.certificate_verifier = Some(certificate_verifier);
365 self
366 }
367
368 cfg_unstable! {
369 pub fn with_certificate_verifier_cache(
373 mut self,
374 certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
375 ) -> ClientBuilder {
376 self.certificate_verifier_cache = certificate_verifier_cache;
377 self
378 }
379 }
380
381 cfg_fs! {
382 pub fn with_http_file_downloader(
384 mut self,
385 http_file_downloader: Arc<dyn FileDownloader>,
386 ) -> ClientBuilder {
387 self.http_file_downloader = Some(http_file_downloader);
388 self
389 }
390
391 pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
393 mut self,
394 ancillary_verification_key: T,
395 ) -> ClientBuilder {
396 self.ancillary_verification_key = ancillary_verification_key.into();
397 self
398 }
399 }
400
401 pub fn with_logger(mut self, logger: Logger) -> Self {
403 self.logger = Some(logger);
404 self
405 }
406
407 pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
409 self.origin_tag = origin_tag;
410 self
411 }
412
413 pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
417 self.feedback_receivers.push(receiver);
418 self
419 }
420
421 pub fn with_options(mut self, options: ClientOptions) -> Self {
423 self.options = options;
424 self
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[tokio::test]
433 async fn compute_http_headers_returns_options_http_headers() {
434 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
435 let client_builder = ClientBuilder::new("").with_options(ClientOptions {
436 http_headers: Some(http_headers.clone()),
437 });
438
439 let computed_headers = client_builder.compute_http_headers();
440
441 assert_eq!(computed_headers, http_headers);
442 }
443
444 #[tokio::test]
445 async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
446 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
447 let client_builder = ClientBuilder::new("")
448 .with_options(ClientOptions {
449 http_headers: Some(http_headers.clone()),
450 })
451 .with_origin_tag(Some("CLIENT_TAG".to_string()));
452
453 let computed_headers = client_builder.compute_http_headers();
454
455 assert_eq!(
456 computed_headers,
457 HashMap::from([
458 ("Key".to_string(), "Value".to_string()),
459 (
460 MITHRIL_ORIGIN_TAG_HEADER.to_string(),
461 "CLIENT_TAG".to_string()
462 )
463 ])
464 );
465 }
466
467 #[tokio::test]
468 async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
469 let builder = ClientBuilder::new("")
470 .with_options(ClientOptions { http_headers: None })
471 .with_origin_tag(Some("TEST".to_string()));
472 assert_eq!(None, builder.options.http_headers);
473 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
474
475 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
476 let builder = ClientBuilder::new("")
477 .with_options(ClientOptions {
478 http_headers: Some(http_headers.clone()),
479 })
480 .with_origin_tag(Some("TEST".to_string()));
481 assert_eq!(Some(http_headers), builder.options.http_headers);
482 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
483 }
484
485 #[tokio::test]
486 async fn test_with_origin_tag_can_be_unset() {
487 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
488 let client_options = ClientOptions {
489 http_headers: Some(http_headers.clone()),
490 };
491 let builder = ClientBuilder::new("")
492 .with_options(client_options)
493 .with_origin_tag(None);
494
495 assert_eq!(Some(http_headers), builder.options.http_headers);
496 assert_eq!(None, builder.origin_tag);
497 }
498}