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 pub fn new(genesis_verification_key: &str) -> ClientBuilder {
208 Self {
209 aggregator_endpoint: None,
210 genesis_verification_key: genesis_verification_key.to_string(),
211 origin_tag: None,
212 client_type: None,
213 #[cfg(feature = "fs")]
214 ancillary_verification_key: None,
215 aggregator_client: None,
216 certificate_verifier: None,
217 #[cfg(feature = "fs")]
218 http_file_downloader: None,
219 #[cfg(feature = "unstable")]
220 certificate_verifier_cache: None,
221 era_fetcher: None,
222 logger: None,
223 feedback_receivers: vec![],
224 options: ClientOptions::default(),
225 }
226 }
227
228 pub fn build(self) -> MithrilResult<Client> {
233 let logger = self
234 .logger
235 .clone()
236 .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
237
238 let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
239
240 let aggregator_client = match self.aggregator_client {
241 None => Arc::new(self.build_aggregator_client(logger.clone())?),
242 Some(client) => client,
243 };
244
245 let mithril_era_client = match self.era_fetcher {
246 None => Arc::new(MithrilEraClient::new(Arc::new(
247 AggregatorHttpEraFetcher::new(aggregator_client.clone()),
248 ))),
249 Some(era_fetcher) => Arc::new(MithrilEraClient::new(era_fetcher)),
250 };
251
252 let certificate_verifier = match self.certificate_verifier {
253 None => Arc::new(
254 MithrilCertificateVerifier::new(
255 aggregator_client.clone(),
256 &self.genesis_verification_key,
257 feedback_sender.clone(),
258 #[cfg(feature = "unstable")]
259 self.certificate_verifier_cache,
260 logger.clone(),
261 )
262 .with_context(|| "Building certificate verifier failed")?,
263 ),
264 Some(verifier) => verifier,
265 };
266 let certificate_client = Arc::new(CertificateClient::new(
267 aggregator_client.clone(),
268 certificate_verifier,
269 logger.clone(),
270 ));
271
272 let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
273 aggregator_client.clone(),
274 ));
275
276 #[cfg(feature = "fs")]
277 let http_file_downloader = match self.http_file_downloader {
278 None => Arc::new(RetryDownloader::new(
279 Arc::new(
280 HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
281 .with_context(|| "Building http file downloader failed")?,
282 ),
283 FileDownloadRetryPolicy::default(),
284 )),
285 Some(http_file_downloader) => http_file_downloader,
286 };
287
288 #[cfg(feature = "fs")]
289 let ancillary_verifier = match self.ancillary_verification_key {
290 None => None,
291 Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
292 verification_key
293 .try_into()
294 .with_context(|| "Building ancillary verifier failed")?,
295 ))),
296 };
297
298 let snapshot_client = Arc::new(SnapshotClient::new(
299 aggregator_client.clone(),
300 #[cfg(feature = "fs")]
301 http_file_downloader.clone(),
302 #[cfg(feature = "fs")]
303 ancillary_verifier.clone(),
304 #[cfg(feature = "fs")]
305 feedback_sender.clone(),
306 #[cfg(feature = "fs")]
307 logger.clone(),
308 ));
309
310 let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
311 aggregator_client.clone(),
312 #[cfg(feature = "fs")]
313 http_file_downloader,
314 #[cfg(feature = "fs")]
315 ancillary_verifier,
316 #[cfg(feature = "fs")]
317 feedback_sender,
318 #[cfg(feature = "fs")]
319 Arc::new(TimestampTempDirectoryProvider::new(&format!(
320 "{}",
321 Utc::now().timestamp_micros()
322 ))),
323 #[cfg(feature = "fs")]
324 logger,
325 ));
326
327 let cardano_transaction_client =
328 Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
329
330 let cardano_stake_distribution_client =
331 Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
332
333 Ok(Client {
334 certificate_client,
335 mithril_stake_distribution_client,
336 snapshot_client,
337 cardano_database_client,
338 cardano_transaction_client,
339 cardano_stake_distribution_client,
340 mithril_era_client,
341 })
342 }
343
344 fn build_aggregator_client(
345 &self,
346 logger: Logger,
347 ) -> Result<AggregatorHTTPClient, anyhow::Error> {
348 let endpoint = self
349 .aggregator_endpoint.as_ref()
350 .ok_or(anyhow!("No aggregator endpoint set: \
351 You must either provide an aggregator endpoint or your own AggregatorClient implementation"))?;
352 let endpoint_url = Url::parse(endpoint).with_context(|| {
353 format!("Invalid aggregator endpoint, it must be a correctly formed url: '{endpoint}'")
354 })?;
355
356 let headers = self.compute_http_headers();
357
358 AggregatorHTTPClient::new(
359 endpoint_url,
360 APIVersionProvider::compute_all_versions_sorted(),
361 logger,
362 Some(headers),
363 )
364 .with_context(|| "Building aggregator client failed")
365 }
366
367 fn compute_http_headers(&self) -> HashMap<String, String> {
368 let mut headers = self.options.http_headers.clone().unwrap_or_default();
369 if let Some(origin_tag) = self.origin_tag.clone() {
370 headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
371 }
372 if let Some(client_type) = self.client_type.clone() {
373 headers.insert(MITHRIL_CLIENT_TYPE_HEADER.to_string(), client_type);
374 } else if !headers.contains_key(MITHRIL_CLIENT_TYPE_HEADER) {
375 headers.insert(
376 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
377 DEFAULT_CLIENT_TYPE.to_string(),
378 );
379 }
380
381 headers
382 }
383
384 pub fn with_aggregator_client(
386 mut self,
387 aggregator_client: Arc<dyn AggregatorClient>,
388 ) -> ClientBuilder {
389 self.aggregator_client = Some(aggregator_client);
390 self
391 }
392
393 pub fn with_era_fetcher(mut self, era_fetcher: Arc<dyn EraFetcher>) -> ClientBuilder {
395 self.era_fetcher = Some(era_fetcher);
396 self
397 }
398
399 pub fn with_certificate_verifier(
401 mut self,
402 certificate_verifier: Arc<dyn CertificateVerifier>,
403 ) -> ClientBuilder {
404 self.certificate_verifier = Some(certificate_verifier);
405 self
406 }
407
408 cfg_unstable! {
409 pub fn with_certificate_verifier_cache(
413 mut self,
414 certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
415 ) -> ClientBuilder {
416 self.certificate_verifier_cache = certificate_verifier_cache;
417 self
418 }
419 }
420
421 cfg_fs! {
422 pub fn with_http_file_downloader(
424 mut self,
425 http_file_downloader: Arc<dyn FileDownloader>,
426 ) -> ClientBuilder {
427 self.http_file_downloader = Some(http_file_downloader);
428 self
429 }
430
431 pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
433 mut self,
434 ancillary_verification_key: T,
435 ) -> ClientBuilder {
436 self.ancillary_verification_key = ancillary_verification_key.into();
437 self
438 }
439 }
440
441 pub fn with_logger(mut self, logger: Logger) -> Self {
443 self.logger = Some(logger);
444 self
445 }
446
447 pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
449 self.origin_tag = origin_tag;
450 self
451 }
452
453 pub fn with_client_type(mut self, client_type: Option<String>) -> Self {
455 self.client_type = client_type;
456 self
457 }
458
459 pub fn with_options(mut self, options: ClientOptions) -> Self {
461 self.options = options;
462 self
463 }
464
465 pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
469 self.feedback_receivers.push(receiver);
470 self
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 fn default_headers() -> HashMap<String, String> {
479 HashMap::from([(
480 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
481 DEFAULT_CLIENT_TYPE.to_string(),
482 )])
483 }
484
485 #[tokio::test]
486 async fn compute_http_headers_returns_options_http_headers() {
487 let http_headers = default_headers();
488 let client_builder = ClientBuilder::new("").with_options(ClientOptions {
489 http_headers: Some(http_headers.clone()),
490 });
491
492 let computed_headers = client_builder.compute_http_headers();
493
494 assert_eq!(computed_headers, http_headers);
495 }
496
497 #[tokio::test]
498 async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
499 let http_headers = default_headers();
500 let client_builder = ClientBuilder::new("")
501 .with_options(ClientOptions {
502 http_headers: Some(http_headers.clone()),
503 })
504 .with_origin_tag(Some("CLIENT_TAG".to_string()));
505 let mut expected_headers = http_headers.clone();
506 expected_headers.insert(
507 MITHRIL_ORIGIN_TAG_HEADER.to_string(),
508 "CLIENT_TAG".to_string(),
509 );
510
511 let computed_headers = client_builder.compute_http_headers();
512 assert_eq!(computed_headers, expected_headers);
513 }
514
515 #[tokio::test]
516 async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
517 let builder = ClientBuilder::new("")
518 .with_options(ClientOptions { http_headers: None })
519 .with_origin_tag(Some("TEST".to_string()));
520 assert_eq!(None, builder.options.http_headers);
521 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
522
523 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
524 let builder = ClientBuilder::new("")
525 .with_options(ClientOptions {
526 http_headers: Some(http_headers.clone()),
527 })
528 .with_origin_tag(Some("TEST".to_string()));
529 assert_eq!(Some(http_headers), builder.options.http_headers);
530 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
531 }
532
533 #[tokio::test]
534 async fn test_with_origin_tag_can_be_unset() {
535 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
536 let client_options = ClientOptions {
537 http_headers: Some(http_headers.clone()),
538 };
539 let builder = ClientBuilder::new("")
540 .with_options(client_options)
541 .with_origin_tag(None);
542
543 assert_eq!(Some(http_headers), builder.options.http_headers);
544 assert_eq!(None, builder.origin_tag);
545 }
546
547 #[tokio::test]
548 async fn compute_http_headers_with_client_type_returns_options_http_headers_with_client_type() {
549 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
550 let client_builder = ClientBuilder::new("")
551 .with_options(ClientOptions {
552 http_headers: Some(http_headers.clone()),
553 })
554 .with_client_type(Some("CLIENT_TYPE".to_string()));
555
556 let computed_headers = client_builder.compute_http_headers();
557
558 assert_eq!(
559 computed_headers,
560 HashMap::from([
561 ("Key".to_string(), "Value".to_string()),
562 (
563 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
564 "CLIENT_TYPE".to_string()
565 )
566 ])
567 );
568 }
569
570 #[tokio::test]
571 async fn compute_http_headers_with_options_containing_client_type_returns_client_type() {
572 let http_headers = HashMap::from([(
573 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
574 "client type from options".to_string(),
575 )]);
576 let client_builder = ClientBuilder::new("").with_options(ClientOptions {
577 http_headers: Some(http_headers.clone()),
578 });
579
580 let computed_headers = client_builder.compute_http_headers();
581
582 assert_eq!(computed_headers, http_headers);
583 }
584
585 #[tokio::test]
586 async fn test_with_client_type_not_overwrite_other_client_options_attributes() {
587 let builder = ClientBuilder::new("")
588 .with_options(ClientOptions { http_headers: None })
589 .with_client_type(Some("TEST".to_string()));
590 assert_eq!(None, builder.options.http_headers);
591 assert_eq!(Some("TEST".to_string()), builder.client_type);
592
593 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
594 let builder = ClientBuilder::new("")
595 .with_options(ClientOptions {
596 http_headers: Some(http_headers.clone()),
597 })
598 .with_client_type(Some("TEST".to_string()));
599 assert_eq!(Some(http_headers), builder.options.http_headers);
600 assert_eq!(Some("TEST".to_string()), builder.client_type);
601 }
602
603 #[tokio::test]
604 async fn test_given_a_none_client_type_compute_http_headers_will_set_client_type_to_default_value()
605 {
606 let builder_without_client_type = ClientBuilder::new("");
607 let computed_headers = builder_without_client_type.compute_http_headers();
608
609 assert_eq!(
610 computed_headers,
611 HashMap::from([(
612 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
613 DEFAULT_CLIENT_TYPE.to_string()
614 )])
615 );
616
617 let builder_with_none_client_type = ClientBuilder::new("").with_client_type(None);
618 let computed_headers = builder_with_none_client_type.compute_http_headers();
619
620 assert_eq!(
621 computed_headers,
622 HashMap::from([(
623 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
624 DEFAULT_CLIENT_TYPE.to_string()
625 )])
626 );
627 }
628
629 #[tokio::test]
630 async fn test_compute_http_headers_will_compute_client_type_header_from_struct_attribute_over_options()
631 {
632 let http_headers = HashMap::from([(
633 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
634 "client type from options".to_string(),
635 )]);
636 let client_builder = ClientBuilder::new("")
637 .with_options(ClientOptions {
638 http_headers: Some(http_headers.clone()),
639 })
640 .with_client_type(Some("client type".to_string()));
641
642 let computed_headers = client_builder.compute_http_headers();
643
644 assert_eq!(
645 computed_headers,
646 HashMap::from([(
647 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
648 "client type".to_string()
649 )])
650 );
651 }
652}