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_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
10
11use crate::aggregator_client::{AggregatorClient, AggregatorHTTPClient};
12use crate::cardano_database_client::CardanoDatabaseClient;
13use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
14use crate::cardano_transaction_client::CardanoTransactionClient;
15#[cfg(feature = "unstable")]
16use crate::certificate_client::CertificateVerifierCache;
17use crate::certificate_client::{
18 CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
19};
20use crate::feedback::{FeedbackReceiver, FeedbackSender};
21#[cfg(feature = "fs")]
22use crate::file_downloader::{
23 FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
24};
25use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
26use crate::snapshot_client::SnapshotClient;
27#[cfg(feature = "fs")]
28use crate::utils::AncillaryVerifier;
29use crate::MithrilResult;
30
31const DEFAULT_CLIENT_TYPE: &str = "LIBRARY";
32
33#[cfg(target_family = "wasm")]
34const fn one_week_in_seconds() -> u32 {
35 604800
36}
37
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct ClientOptions {
41 pub http_headers: Option<HashMap<String, String>>,
43
44 #[cfg(target_family = "wasm")]
46 #[cfg_attr(target_family = "wasm", serde(default))]
47 pub origin_tag: Option<String>,
48
49 #[cfg(target_family = "wasm")]
51 #[cfg_attr(target_family = "wasm", serde(default))]
52 pub unstable: bool,
53
54 #[cfg(target_family = "wasm")]
60 #[cfg_attr(target_family = "wasm", serde(default))]
61 pub enable_certificate_chain_verification_cache: bool,
62
63 #[cfg(target_family = "wasm")]
70 #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
71 pub certificate_chain_verification_cache_duration_in_seconds: u32,
72}
73
74impl ClientOptions {
75 pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
77 Self {
78 http_headers,
79 #[cfg(target_family = "wasm")]
80 origin_tag: None,
81 #[cfg(target_family = "wasm")]
82 unstable: false,
83 #[cfg(target_family = "wasm")]
84 enable_certificate_chain_verification_cache: false,
85 #[cfg(target_family = "wasm")]
86 certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
87 }
88 }
89
90 #[cfg(target_family = "wasm")]
92 pub fn with_unstable_features(self, unstable: bool) -> Self {
93 Self { unstable, ..self }
94 }
95}
96
97#[derive(Clone)]
101pub struct Client {
102 certificate_client: Arc<CertificateClient>,
103 mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
104 snapshot_client: Arc<SnapshotClient>,
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 pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
134 self.cardano_database_client.clone()
135 }
136
137 pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
139 self.cardano_transaction_client.clone()
140 }
141
142 pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
144 self.cardano_stake_distribution_client.clone()
145 }
146}
147
148pub struct ClientBuilder {
150 aggregator_endpoint: Option<String>,
151 genesis_verification_key: String,
152 origin_tag: Option<String>,
153 client_type: 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 client_type: None,
176 #[cfg(feature = "fs")]
177 ancillary_verification_key: None,
178 aggregator_client: None,
179 certificate_verifier: None,
180 #[cfg(feature = "fs")]
181 http_file_downloader: None,
182 #[cfg(feature = "unstable")]
183 certificate_verifier_cache: None,
184 logger: None,
185 feedback_receivers: vec![],
186 options: ClientOptions::default(),
187 }
188 }
189
190 pub fn new(genesis_verification_key: &str) -> ClientBuilder {
195 Self {
196 aggregator_endpoint: None,
197 genesis_verification_key: genesis_verification_key.to_string(),
198 origin_tag: None,
199 client_type: None,
200 #[cfg(feature = "fs")]
201 ancillary_verification_key: None,
202 aggregator_client: None,
203 certificate_verifier: None,
204 #[cfg(feature = "fs")]
205 http_file_downloader: None,
206 #[cfg(feature = "unstable")]
207 certificate_verifier_cache: None,
208 logger: None,
209 feedback_receivers: vec![],
210 options: ClientOptions::default(),
211 }
212 }
213
214 pub fn build(self) -> MithrilResult<Client> {
219 let logger = self
220 .logger
221 .clone()
222 .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
223
224 let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
225
226 let aggregator_client = match self.aggregator_client {
227 None => Arc::new(self.build_aggregator_client(logger.clone())?),
228 Some(client) => client,
229 };
230
231 let certificate_verifier = match self.certificate_verifier {
232 None => Arc::new(
233 MithrilCertificateVerifier::new(
234 aggregator_client.clone(),
235 &self.genesis_verification_key,
236 feedback_sender.clone(),
237 #[cfg(feature = "unstable")]
238 self.certificate_verifier_cache,
239 logger.clone(),
240 )
241 .with_context(|| "Building certificate verifier failed")?,
242 ),
243 Some(verifier) => verifier,
244 };
245 let certificate_client = Arc::new(CertificateClient::new(
246 aggregator_client.clone(),
247 certificate_verifier,
248 logger.clone(),
249 ));
250
251 let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
252 aggregator_client.clone(),
253 ));
254
255 #[cfg(feature = "fs")]
256 let http_file_downloader = match self.http_file_downloader {
257 None => Arc::new(RetryDownloader::new(
258 Arc::new(
259 HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
260 .with_context(|| "Building http file downloader failed")?,
261 ),
262 FileDownloadRetryPolicy::default(),
263 )),
264 Some(http_file_downloader) => http_file_downloader,
265 };
266
267 #[cfg(feature = "fs")]
268 let ancillary_verifier = match self.ancillary_verification_key {
269 None => None,
270 Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
271 verification_key
272 .try_into()
273 .with_context(|| "Building ancillary verifier failed")?,
274 ))),
275 };
276
277 let snapshot_client = Arc::new(SnapshotClient::new(
278 aggregator_client.clone(),
279 #[cfg(feature = "fs")]
280 http_file_downloader.clone(),
281 #[cfg(feature = "fs")]
282 ancillary_verifier.clone(),
283 #[cfg(feature = "fs")]
284 feedback_sender.clone(),
285 #[cfg(feature = "fs")]
286 logger.clone(),
287 ));
288
289 let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
290 aggregator_client.clone(),
291 #[cfg(feature = "fs")]
292 http_file_downloader,
293 #[cfg(feature = "fs")]
294 ancillary_verifier,
295 #[cfg(feature = "fs")]
296 feedback_sender,
297 #[cfg(feature = "fs")]
298 logger,
299 ));
300
301 let cardano_transaction_client =
302 Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
303
304 let cardano_stake_distribution_client =
305 Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
306
307 Ok(Client {
308 certificate_client,
309 mithril_stake_distribution_client,
310 snapshot_client,
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 logger,
335 Some(headers),
336 )
337 .with_context(|| "Building aggregator client failed")
338 }
339
340 fn compute_http_headers(&self) -> HashMap<String, String> {
341 let mut headers = self.options.http_headers.clone().unwrap_or_default();
342 if let Some(origin_tag) = self.origin_tag.clone() {
343 headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
344 }
345 if let Some(client_type) = self.client_type.clone() {
346 headers.insert(MITHRIL_CLIENT_TYPE_HEADER.to_string(), client_type);
347 } else if !headers.contains_key(MITHRIL_CLIENT_TYPE_HEADER) {
348 headers.insert(
349 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
350 DEFAULT_CLIENT_TYPE.to_string(),
351 );
352 }
353
354 headers
355 }
356
357 pub fn with_aggregator_client(
359 mut self,
360 aggregator_client: Arc<dyn AggregatorClient>,
361 ) -> ClientBuilder {
362 self.aggregator_client = Some(aggregator_client);
363 self
364 }
365
366 pub fn with_certificate_verifier(
368 mut self,
369 certificate_verifier: Arc<dyn CertificateVerifier>,
370 ) -> ClientBuilder {
371 self.certificate_verifier = Some(certificate_verifier);
372 self
373 }
374
375 cfg_unstable! {
376 pub fn with_certificate_verifier_cache(
380 mut self,
381 certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
382 ) -> ClientBuilder {
383 self.certificate_verifier_cache = certificate_verifier_cache;
384 self
385 }
386 }
387
388 cfg_fs! {
389 pub fn with_http_file_downloader(
391 mut self,
392 http_file_downloader: Arc<dyn FileDownloader>,
393 ) -> ClientBuilder {
394 self.http_file_downloader = Some(http_file_downloader);
395 self
396 }
397
398 pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
400 mut self,
401 ancillary_verification_key: T,
402 ) -> ClientBuilder {
403 self.ancillary_verification_key = ancillary_verification_key.into();
404 self
405 }
406 }
407
408 pub fn with_logger(mut self, logger: Logger) -> Self {
410 self.logger = Some(logger);
411 self
412 }
413
414 pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
416 self.origin_tag = origin_tag;
417 self
418 }
419
420 pub fn with_client_type(mut self, client_type: Option<String>) -> Self {
422 self.client_type = client_type;
423 self
424 }
425
426 pub fn with_options(mut self, options: ClientOptions) -> Self {
428 self.options = options;
429 self
430 }
431
432 pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
436 self.feedback_receivers.push(receiver);
437 self
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 fn default_headers() -> HashMap<String, String> {
446 HashMap::from([(
447 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
448 DEFAULT_CLIENT_TYPE.to_string(),
449 )])
450 }
451
452 #[tokio::test]
453 async fn compute_http_headers_returns_options_http_headers() {
454 let http_headers = default_headers();
455 let client_builder = ClientBuilder::new("").with_options(ClientOptions {
456 http_headers: Some(http_headers.clone()),
457 });
458
459 let computed_headers = client_builder.compute_http_headers();
460
461 assert_eq!(computed_headers, http_headers);
462 }
463
464 #[tokio::test]
465 async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
466 let http_headers = default_headers();
467 let client_builder = ClientBuilder::new("")
468 .with_options(ClientOptions {
469 http_headers: Some(http_headers.clone()),
470 })
471 .with_origin_tag(Some("CLIENT_TAG".to_string()));
472 let mut expected_headers = http_headers.clone();
473 expected_headers.insert(
474 MITHRIL_ORIGIN_TAG_HEADER.to_string(),
475 "CLIENT_TAG".to_string(),
476 );
477
478 let computed_headers = client_builder.compute_http_headers();
479 assert_eq!(computed_headers, expected_headers);
480 }
481
482 #[tokio::test]
483 async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
484 let builder = ClientBuilder::new("")
485 .with_options(ClientOptions { http_headers: None })
486 .with_origin_tag(Some("TEST".to_string()));
487 assert_eq!(None, builder.options.http_headers);
488 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
489
490 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
491 let builder = ClientBuilder::new("")
492 .with_options(ClientOptions {
493 http_headers: Some(http_headers.clone()),
494 })
495 .with_origin_tag(Some("TEST".to_string()));
496 assert_eq!(Some(http_headers), builder.options.http_headers);
497 assert_eq!(Some("TEST".to_string()), builder.origin_tag);
498 }
499
500 #[tokio::test]
501 async fn test_with_origin_tag_can_be_unset() {
502 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
503 let client_options = ClientOptions {
504 http_headers: Some(http_headers.clone()),
505 };
506 let builder = ClientBuilder::new("")
507 .with_options(client_options)
508 .with_origin_tag(None);
509
510 assert_eq!(Some(http_headers), builder.options.http_headers);
511 assert_eq!(None, builder.origin_tag);
512 }
513
514 #[tokio::test]
515 async fn compute_http_headers_with_client_type_returns_options_http_headers_with_client_type() {
516 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
517 let client_builder = ClientBuilder::new("")
518 .with_options(ClientOptions {
519 http_headers: Some(http_headers.clone()),
520 })
521 .with_client_type(Some("CLIENT_TYPE".to_string()));
522
523 let computed_headers = client_builder.compute_http_headers();
524
525 assert_eq!(
526 computed_headers,
527 HashMap::from([
528 ("Key".to_string(), "Value".to_string()),
529 (
530 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
531 "CLIENT_TYPE".to_string()
532 )
533 ])
534 );
535 }
536
537 #[tokio::test]
538 async fn compute_http_headers_with_options_containing_client_type_returns_client_type() {
539 let http_headers = HashMap::from([(
540 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
541 "client type from options".to_string(),
542 )]);
543 let client_builder = ClientBuilder::new("").with_options(ClientOptions {
544 http_headers: Some(http_headers.clone()),
545 });
546
547 let computed_headers = client_builder.compute_http_headers();
548
549 assert_eq!(computed_headers, http_headers);
550 }
551
552 #[tokio::test]
553 async fn test_with_client_type_not_overwrite_other_client_options_attributes() {
554 let builder = ClientBuilder::new("")
555 .with_options(ClientOptions { http_headers: None })
556 .with_client_type(Some("TEST".to_string()));
557 assert_eq!(None, builder.options.http_headers);
558 assert_eq!(Some("TEST".to_string()), builder.client_type);
559
560 let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
561 let builder = ClientBuilder::new("")
562 .with_options(ClientOptions {
563 http_headers: Some(http_headers.clone()),
564 })
565 .with_client_type(Some("TEST".to_string()));
566 assert_eq!(Some(http_headers), builder.options.http_headers);
567 assert_eq!(Some("TEST".to_string()), builder.client_type);
568 }
569
570 #[tokio::test]
571 async fn test_given_a_none_client_type_compute_http_headers_will_set_client_type_to_default_value(
572 ) {
573 let builder_without_client_type = ClientBuilder::new("");
574 let computed_headers = builder_without_client_type.compute_http_headers();
575
576 assert_eq!(
577 computed_headers,
578 HashMap::from([(
579 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
580 DEFAULT_CLIENT_TYPE.to_string()
581 )])
582 );
583
584 let builder_with_none_client_type = ClientBuilder::new("").with_client_type(None);
585 let computed_headers = builder_with_none_client_type.compute_http_headers();
586
587 assert_eq!(
588 computed_headers,
589 HashMap::from([(
590 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
591 DEFAULT_CLIENT_TYPE.to_string()
592 )])
593 );
594 }
595
596 #[tokio::test]
597 async fn test_compute_http_headers_will_compute_client_type_header_from_struct_attribute_over_options(
598 ) {
599 let http_headers = HashMap::from([(
600 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
601 "client type from options".to_string(),
602 )]);
603 let client_builder = ClientBuilder::new("")
604 .with_options(ClientOptions {
605 http_headers: Some(http_headers.clone()),
606 })
607 .with_client_type(Some("client type".to_string()));
608
609 let computed_headers = client_builder.compute_http_headers();
610
611 assert_eq!(
612 computed_headers,
613 HashMap::from([(
614 MITHRIL_CLIENT_TYPE_HEADER.to_string(),
615 "client type".to_string()
616 )])
617 );
618 }
619}