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