1use anyhow::{Context, anyhow};
2#[cfg(feature = "fs")]
3use chrono::Utc;
4use reqwest::Url;
5use serde::{Deserialize, Serialize};
6use slog::{Logger, o};
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use mithril_common::api_version::APIVersionProvider;
11use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
12
13use crate::MithrilResult;
14use crate::aggregator_client::{AggregatorClient, AggregatorHTTPClient};
15use crate::cardano_database_client::CardanoDatabaseClient;
16use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
17use crate::cardano_transaction_client::CardanoTransactionClient;
18#[cfg(feature = "unstable")]
19use crate::certificate_client::CertificateVerifierCache;
20use crate::certificate_client::{
21 CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
22};
23use crate::era::{AggregatorHttpEraFetcher, EraFetcher, MithrilEraClient};
24use crate::feedback::{FeedbackReceiver, FeedbackSender};
25#[cfg(feature = "fs")]
26use crate::file_downloader::{
27 FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
28};
29use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
30use crate::snapshot_client::SnapshotClient;
31#[cfg(feature = "fs")]
32use crate::utils::AncillaryVerifier;
33#[cfg(feature = "fs")]
34use crate::utils::TimestampTempDirectoryProvider;
35
36const DEFAULT_CLIENT_TYPE: &str = "LIBRARY";
37
38#[cfg(target_family = "wasm")]
39const fn one_week_in_seconds() -> u32 {
40 604800
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct ClientOptions {
46 pub http_headers: Option<HashMap<String, String>>,
48
49 #[cfg(target_family = "wasm")]
51 #[cfg_attr(target_family = "wasm", serde(default))]
52 pub origin_tag: Option<String>,
53
54 #[cfg(target_family = "wasm")]
56 #[cfg_attr(target_family = "wasm", serde(default))]
57 pub unstable: bool,
58
59 #[cfg(target_family = "wasm")]
65 #[cfg_attr(target_family = "wasm", serde(default))]
66 pub enable_certificate_chain_verification_cache: bool,
67
68 #[cfg(target_family = "wasm")]
75 #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
76 pub certificate_chain_verification_cache_duration_in_seconds: u32,
77}
78
79impl ClientOptions {
80 pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
82 Self {
83 http_headers,
84 #[cfg(target_family = "wasm")]
85 origin_tag: None,
86 #[cfg(target_family = "wasm")]
87 unstable: false,
88 #[cfg(target_family = "wasm")]
89 enable_certificate_chain_verification_cache: false,
90 #[cfg(target_family = "wasm")]
91 certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
92 }
93 }
94
95 #[cfg(target_family = "wasm")]
97 pub fn with_unstable_features(self, unstable: bool) -> Self {
98 Self { unstable, ..self }
99 }
100}
101
102#[derive(Clone)]
106pub struct Client {
107 certificate_client: Arc<CertificateClient>,
108 mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
109 snapshot_client: Arc<SnapshotClient>,
110 cardano_database_client: Arc<CardanoDatabaseClient>,
111 cardano_transaction_client: Arc<CardanoTransactionClient>,
112 cardano_stake_distribution_client: Arc<CardanoStakeDistributionClient>,
113 mithril_era_client: Arc<MithrilEraClient>,
114}
115
116impl Client {
117 pub fn certificate(&self) -> Arc<CertificateClient> {
119 self.certificate_client.clone()
120 }
121
122 pub fn mithril_stake_distribution(&self) -> Arc<MithrilStakeDistributionClient> {
124 self.mithril_stake_distribution_client.clone()
125 }
126
127 #[deprecated(since = "0.11.9", note = "supersede by `cardano_database`")]
128 pub fn snapshot(&self) -> Arc<SnapshotClient> {
130 self.cardano_database()
131 }
132
133 pub fn cardano_database(&self) -> Arc<SnapshotClient> {
135 self.snapshot_client.clone()
136 }
137
138 pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
140 self.cardano_database_client.clone()
141 }
142
143 pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
145 self.cardano_transaction_client.clone()
146 }
147
148 pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
150 self.cardano_stake_distribution_client.clone()
151 }
152
153 pub fn mithril_era_client(&self) -> Arc<MithrilEraClient> {
155 self.mithril_era_client.clone()
156 }
157}
158
159pub struct ClientBuilder {
161 aggregator_endpoint: Option<String>,
162 genesis_verification_key: String,
163 origin_tag: Option<String>,
164 client_type: Option<String>,
165 #[cfg(feature = "fs")]
166 ancillary_verification_key: Option<String>,
167 aggregator_client: Option<Arc<dyn AggregatorClient>>,
168 certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
169 #[cfg(feature = "fs")]
170 http_file_downloader: Option<Arc<dyn FileDownloader>>,
171 #[cfg(feature = "unstable")]
172 certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
173 era_fetcher: Option<Arc<dyn EraFetcher>>,
174 logger: Option<Logger>,
175 feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
176 options: ClientOptions,
177}
178
179impl ClientBuilder {
180 pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder {
183 Self {
184 aggregator_endpoint: Some(endpoint.to_string()),
185 genesis_verification_key: genesis_verification_key.to_string(),
186 origin_tag: None,
187 client_type: None,
188 #[cfg(feature = "fs")]
189 ancillary_verification_key: None,
190 aggregator_client: None,
191 certificate_verifier: None,
192 #[cfg(feature = "fs")]
193 http_file_downloader: None,
194 #[cfg(feature = "unstable")]
195 certificate_verifier_cache: None,
196 era_fetcher: None,
197 logger: None,
198 feedback_receivers: vec![],
199 options: ClientOptions::default(),
200 }
201 }
202
203 #[deprecated(since = "0.12.33", note = "Will be removed in 0.13.0")]
208 pub fn new(genesis_verification_key: &str) -> ClientBuilder {
209 Self {
210 aggregator_endpoint: None,
211 genesis_verification_key: genesis_verification_key.to_string(),
212 origin_tag: None,
213 client_type: None,
214 #[cfg(feature = "fs")]
215 ancillary_verification_key: None,
216 aggregator_client: None,
217 certificate_verifier: None,
218 #[cfg(feature = "fs")]
219 http_file_downloader: None,
220 #[cfg(feature = "unstable")]
221 certificate_verifier_cache: None,
222 era_fetcher: None,
223 logger: None,
224 feedback_receivers: vec![],
225 options: ClientOptions::default(),
226 }
227 }
228
229 pub fn build(self) -> MithrilResult<Client> {
234 let logger = self
235 .logger
236 .clone()
237 .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
238
239 let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
240
241 let aggregator_client = match self.aggregator_client {
242 None => Arc::new(self.build_aggregator_client(logger.clone())?),
243 Some(client) => client,
244 };
245
246 let mithril_era_client = match self.era_fetcher {
247 None => Arc::new(MithrilEraClient::new(Arc::new(
248 AggregatorHttpEraFetcher::new(aggregator_client.clone()),
249 ))),
250 Some(era_fetcher) => Arc::new(MithrilEraClient::new(era_fetcher)),
251 };
252
253 let certificate_verifier = match self.certificate_verifier {
254 None => Arc::new(
255 MithrilCertificateVerifier::new(
256 aggregator_client.clone(),
257 &self.genesis_verification_key,
258 feedback_sender.clone(),
259 #[cfg(feature = "unstable")]
260 self.certificate_verifier_cache,
261 logger.clone(),
262 )
263 .with_context(|| "Building certificate verifier failed")?,
264 ),
265 Some(verifier) => verifier,
266 };
267 let certificate_client = Arc::new(CertificateClient::new(
268 aggregator_client.clone(),
269 certificate_verifier,
270 logger.clone(),
271 ));
272
273 let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
274 aggregator_client.clone(),
275 ));
276
277 #[cfg(feature = "fs")]
278 let http_file_downloader = match self.http_file_downloader {
279 None => Arc::new(RetryDownloader::new(
280 Arc::new(
281 HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
282 .with_context(|| "Building http file downloader failed")?,
283 ),
284 FileDownloadRetryPolicy::default(),
285 )),
286 Some(http_file_downloader) => http_file_downloader,
287 };
288
289 #[cfg(feature = "fs")]
290 let ancillary_verifier = match self.ancillary_verification_key {
291 None => None,
292 Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
293 verification_key
294 .try_into()
295 .with_context(|| "Building ancillary verifier failed")?,
296 ))),
297 };
298
299 let snapshot_client = Arc::new(SnapshotClient::new(
300 aggregator_client.clone(),
301 #[cfg(feature = "fs")]
302 http_file_downloader.clone(),
303 #[cfg(feature = "fs")]
304 ancillary_verifier.clone(),
305 #[cfg(feature = "fs")]
306 feedback_sender.clone(),
307 #[cfg(feature = "fs")]
308 logger.clone(),
309 ));
310
311 let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
312 aggregator_client.clone(),
313 #[cfg(feature = "fs")]
314 http_file_downloader,
315 #[cfg(feature = "fs")]
316 ancillary_verifier,
317 #[cfg(feature = "fs")]
318 feedback_sender,
319 #[cfg(feature = "fs")]
320 Arc::new(TimestampTempDirectoryProvider::new(&format!(
321 "{}",
322 Utc::now().timestamp_micros()
323 ))),
324 #[cfg(feature = "fs")]
325 logger,
326 ));
327
328 let cardano_transaction_client =
329 Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
330
331 let cardano_stake_distribution_client =
332 Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
333
334 Ok(Client {
335 certificate_client,
336 mithril_stake_distribution_client,
337 snapshot_client,
338 cardano_database_client,
339 cardano_transaction_client,
340 cardano_stake_distribution_client,
341 mithril_era_client,
342 })
343 }
344
345 fn build_aggregator_client(
346 &self,
347 logger: Logger,
348 ) -> Result<AggregatorHTTPClient, anyhow::Error> {
349 let endpoint = self
350 .aggregator_endpoint.as_ref()
351 .ok_or(anyhow!("No aggregator endpoint set: \
352 You must either provide an aggregator endpoint or your own AggregatorClient implementation"))?;
353 let endpoint_url = Url::parse(endpoint).with_context(|| {
354 format!("Invalid aggregator endpoint, it must be a correctly formed url: '{endpoint}'")
355 })?;
356
357 let headers = self.compute_http_headers();
358
359 AggregatorHTTPClient::new(
360 endpoint_url,
361 APIVersionProvider::compute_all_versions_sorted(),
362 logger,
363 Some(headers),
364 )
365 .with_context(|| "Building aggregator client failed")
366 }
367
368 fn compute_http_headers(&self) -> HashMap<String, String> {
369 let mut headers = self.options.http_headers.clone().unwrap_or_default();
370 if let Some(origin_tag) = self.origin_tag.clone() {
371 headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
372 }
373 if let Some(client_type) = self.client_type.clone() {
374 headers.insert(MITHRIL_CLIENT_TYPE_HEADER.to_string(), client_type);
375 } else if !headers.contains_key(MITHRIL_CLIENT_TYPE_HEADER) {
376 headers.insert(
377 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
378 DEFAULT_CLIENT_TYPE.to_string(),
379 );
380 }
381
382 headers
383 }
384
385 #[deprecated(since = "0.12.33", note = "Will be removed in 0.13.0")]
387 pub fn with_aggregator_client(
388 mut self,
389 aggregator_client: Arc<dyn AggregatorClient>,
390 ) -> ClientBuilder {
391 self.aggregator_client = Some(aggregator_client);
392 self
393 }
394
395 pub fn with_era_fetcher(mut self, era_fetcher: Arc<dyn EraFetcher>) -> ClientBuilder {
397 self.era_fetcher = Some(era_fetcher);
398 self
399 }
400
401 pub fn with_certificate_verifier(
403 mut self,
404 certificate_verifier: Arc<dyn CertificateVerifier>,
405 ) -> ClientBuilder {
406 self.certificate_verifier = Some(certificate_verifier);
407 self
408 }
409
410 cfg_unstable! {
411 pub fn with_certificate_verifier_cache(
415 mut self,
416 certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
417 ) -> ClientBuilder {
418 self.certificate_verifier_cache = certificate_verifier_cache;
419 self
420 }
421 }
422
423 cfg_fs! {
424 pub fn with_http_file_downloader(
426 mut self,
427 http_file_downloader: Arc<dyn FileDownloader>,
428 ) -> ClientBuilder {
429 self.http_file_downloader = Some(http_file_downloader);
430 self
431 }
432
433 pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
435 mut self,
436 ancillary_verification_key: T,
437 ) -> ClientBuilder {
438 self.ancillary_verification_key = ancillary_verification_key.into();
439 self
440 }
441 }
442
443 pub fn with_logger(mut self, logger: Logger) -> Self {
445 self.logger = Some(logger);
446 self
447 }
448
449 pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
451 self.origin_tag = origin_tag;
452 self
453 }
454
455 pub fn with_client_type(mut self, client_type: Option<String>) -> Self {
457 self.client_type = client_type;
458 self
459 }
460
461 pub fn with_options(mut self, options: ClientOptions) -> Self {
463 self.options = options;
464 self
465 }
466
467 pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
471 self.feedback_receivers.push(receiver);
472 self
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 fn default_headers() -> HashMap<String, String> {
481 HashMap::from([(
482 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
483 DEFAULT_CLIENT_TYPE.to_string(),
484 )])
485 }
486
487 #[tokio::test]
488 async fn compute_http_headers_returns_options_http_headers() {
489 let http_headers = default_headers();
490 let client_builder = ClientBuilder::aggregator("", "").with_options(ClientOptions {
491 http_headers: Some(http_headers.clone()),
492 });
493
494 let computed_headers = client_builder.compute_http_headers();
495
496 assert_eq!(computed_headers, http_headers);
497 }
498
499 #[tokio::test]
500 async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
501 let http_headers = default_headers();
502 let client_builder = ClientBuilder::aggregator("", "")
503 .with_options(ClientOptions {
504 http_headers: Some(http_headers.clone()),
505 })
506 .with_origin_tag(Some("CLIENT_TAG".to_string()));
507 let mut expected_headers = http_headers.clone();
508 expected_headers.insert(
509 MITHRIL_ORIGIN_TAG_HEADER.to_string(),
510 "CLIENT_TAG".to_string(),
511 );
512
513 let computed_headers = client_builder.compute_http_headers();
514 assert_eq!(computed_headers, expected_headers);
515 }
516
517 #[tokio::test]
518 async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
519 let builder = ClientBuilder::aggregator("", "")
520 .with_options(ClientOptions { http_headers: None })
521 .with_origin_tag(Some("TEST".to_string()));
522 assert_eq!(None, builder.options.http_headers);
523 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
524
525 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
526 let builder = ClientBuilder::aggregator("", "")
527 .with_options(ClientOptions {
528 http_headers: Some(http_headers.clone()),
529 })
530 .with_origin_tag(Some("TEST".to_string()));
531 assert_eq!(Some(http_headers), builder.options.http_headers);
532 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
533 }
534
535 #[tokio::test]
536 async fn test_with_origin_tag_can_be_unset() {
537 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
538 let client_options = ClientOptions {
539 http_headers: Some(http_headers.clone()),
540 };
541 let builder = ClientBuilder::aggregator("", "")
542 .with_options(client_options)
543 .with_origin_tag(None);
544
545 assert_eq!(Some(http_headers), builder.options.http_headers);
546 assert_eq!(None, builder.origin_tag);
547 }
548
549 #[tokio::test]
550 async fn compute_http_headers_with_client_type_returns_options_http_headers_with_client_type() {
551 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
552 let client_builder = ClientBuilder::aggregator("", "")
553 .with_options(ClientOptions {
554 http_headers: Some(http_headers.clone()),
555 })
556 .with_client_type(Some("CLIENT_TYPE".to_string()));
557
558 let computed_headers = client_builder.compute_http_headers();
559
560 assert_eq!(
561 computed_headers,
562 HashMap::from([
563 ("Key".to_string(), "Value".to_string()),
564 (
565 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
566 "CLIENT_TYPE".to_string()
567 )
568 ])
569 );
570 }
571
572 #[tokio::test]
573 async fn compute_http_headers_with_options_containing_client_type_returns_client_type() {
574 let http_headers = HashMap::from([(
575 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
576 "client type from options".to_string(),
577 )]);
578 let client_builder = ClientBuilder::aggregator("", "").with_options(ClientOptions {
579 http_headers: Some(http_headers.clone()),
580 });
581
582 let computed_headers = client_builder.compute_http_headers();
583
584 assert_eq!(computed_headers, http_headers);
585 }
586
587 #[tokio::test]
588 async fn test_with_client_type_not_overwrite_other_client_options_attributes() {
589 let builder = ClientBuilder::aggregator("", "")
590 .with_options(ClientOptions { http_headers: None })
591 .with_client_type(Some("TEST".to_string()));
592 assert_eq!(None, builder.options.http_headers);
593 assert_eq!(Some("TEST".to_string()), builder.client_type);
594
595 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
596 let builder = ClientBuilder::aggregator("", "")
597 .with_options(ClientOptions {
598 http_headers: Some(http_headers.clone()),
599 })
600 .with_client_type(Some("TEST".to_string()));
601 assert_eq!(Some(http_headers), builder.options.http_headers);
602 assert_eq!(Some("TEST".to_string()), builder.client_type);
603 }
604
605 #[tokio::test]
606 async fn test_given_a_none_client_type_compute_http_headers_will_set_client_type_to_default_value()
607 {
608 let builder_without_client_type = ClientBuilder::aggregator("", "");
609 let computed_headers = builder_without_client_type.compute_http_headers();
610
611 assert_eq!(
612 computed_headers,
613 HashMap::from([(
614 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
615 DEFAULT_CLIENT_TYPE.to_string()
616 )])
617 );
618
619 let builder_with_none_client_type =
620 ClientBuilder::aggregator("", "").with_client_type(None);
621 let computed_headers = builder_with_none_client_type.compute_http_headers();
622
623 assert_eq!(
624 computed_headers,
625 HashMap::from([(
626 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
627 DEFAULT_CLIENT_TYPE.to_string()
628 )])
629 );
630 }
631
632 #[tokio::test]
633 async fn test_compute_http_headers_will_compute_client_type_header_from_struct_attribute_over_options()
634 {
635 let http_headers = HashMap::from([(
636 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
637 "client type from options".to_string(),
638 )]);
639 let client_builder = ClientBuilder::aggregator("", "")
640 .with_options(ClientOptions {
641 http_headers: Some(http_headers.clone()),
642 })
643 .with_client_type(Some("client type".to_string()));
644
645 let computed_headers = client_builder.compute_http_headers();
646
647 assert_eq!(
648 computed_headers,
649 HashMap::from([(
650 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
651 "client type".to_string()
652 )])
653 );
654 }
655}