mithril_client/
client.rs

1use std::collections::HashMap;
2#[cfg(not(target_family = "wasm"))]
3use std::str::FromStr;
4use std::sync::Arc;
5
6use anyhow::{Context, anyhow};
7#[cfg(any(feature = "fs", not(target_family = "wasm")))]
8use chrono::Utc;
9#[cfg(not(target_family = "wasm"))]
10use rand::SeedableRng;
11#[cfg(not(target_family = "wasm"))]
12use rand::rngs::StdRng;
13use reqwest::Url;
14use serde::{Deserialize, Serialize};
15use slog::{Logger, o};
16
17#[cfg(not(target_family = "wasm"))]
18use mithril_aggregator_discovery::{
19    AggregatorDiscoverer, AggregatorEndpoint, CapableAggregatorDiscoverer,
20    HttpConfigAggregatorDiscoverer, RequiredAggregatorCapabilities, ShuffleAggregatorDiscoverer,
21};
22use mithril_common::api_version::APIVersionProvider;
23use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
24
25use crate::MithrilResult;
26use crate::aggregator_client::{AggregatorClient, AggregatorHTTPClient};
27use crate::cardano_database_client::CardanoDatabaseClient;
28use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
29use crate::cardano_transaction_client::CardanoTransactionClient;
30#[cfg(feature = "unstable")]
31use crate::certificate_client::CertificateVerifierCache;
32use crate::certificate_client::{
33    CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
34};
35#[cfg(not(target_family = "wasm"))]
36use crate::common::MithrilNetwork;
37use crate::era::{AggregatorHttpEraFetcher, EraFetcher, MithrilEraClient};
38use crate::feedback::{FeedbackReceiver, FeedbackSender};
39#[cfg(feature = "fs")]
40use crate::file_downloader::{
41    FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
42};
43use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
44use crate::snapshot_client::SnapshotClient;
45#[cfg(feature = "fs")]
46use crate::utils::AncillaryVerifier;
47#[cfg(feature = "fs")]
48use crate::utils::TimestampTempDirectoryProvider;
49
50const DEFAULT_CLIENT_TYPE: &str = "LIBRARY";
51
52#[cfg(target_family = "wasm")]
53const fn one_week_in_seconds() -> u32 {
54    604800
55}
56
57/// The type of discovery to use to find the aggregator to connect to.
58pub enum AggregatorDiscoveryType {
59    /// Use a specific URL to connect to the aggregator.
60    Url(String),
61    /// Automatically discover the aggregator.
62    #[cfg(not(target_family = "wasm"))]
63    Automatic(MithrilNetwork),
64}
65
66#[cfg(not(target_family = "wasm"))]
67impl FromStr for AggregatorDiscoveryType {
68    type Err = anyhow::Error;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        if let Some(network) = s.strip_prefix("auto:") {
72            Ok(AggregatorDiscoveryType::Automatic(MithrilNetwork::new(
73                network.to_string(),
74            )))
75        } else {
76            Ok(AggregatorDiscoveryType::Url(s.to_string()))
77        }
78    }
79}
80
81/// The genesis verification key.
82pub enum GenesisVerificationKey {
83    /// The verification key is provided as a JSON Hex-encoded string.
84    JsonHex(String),
85}
86
87/// Options that can be used to configure the client.
88#[derive(Debug, Clone, Default, Serialize, Deserialize)]
89pub struct ClientOptions {
90    /// HTTP headers to include in the client requests.
91    pub http_headers: Option<HashMap<String, String>>,
92
93    /// Tag to retrieve the origin of the client requests.
94    #[cfg(target_family = "wasm")]
95    #[cfg_attr(target_family = "wasm", serde(default))]
96    pub origin_tag: Option<String>,
97
98    /// Whether to enable unstable features in the WASM client.
99    #[cfg(target_family = "wasm")]
100    #[cfg_attr(target_family = "wasm", serde(default))]
101    pub unstable: bool,
102
103    /// Whether to enable certificate chain verification caching in the WASM client.
104    ///
105    /// `unstable` must be set to `true` for this option to have any effect.
106    ///
107    /// DANGER: This feature is highly experimental and insecure, and it must not be used in production
108    #[cfg(target_family = "wasm")]
109    #[cfg_attr(target_family = "wasm", serde(default))]
110    pub enable_certificate_chain_verification_cache: bool,
111
112    /// Duration in seconds of certificate chain verification cache in the WASM client.
113    ///
114    /// Default to one week (604800 seconds).
115    ///
116    /// `enable_certificate_chain_verification_cache` and `unstable` must both be set to `true`
117    /// for this option to have any effect.
118    #[cfg(target_family = "wasm")]
119    #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
120    pub certificate_chain_verification_cache_duration_in_seconds: u32,
121}
122
123impl ClientOptions {
124    /// Instantiate a new [ClientOptions].
125    pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
126        Self {
127            http_headers,
128            #[cfg(target_family = "wasm")]
129            origin_tag: None,
130            #[cfg(target_family = "wasm")]
131            unstable: false,
132            #[cfg(target_family = "wasm")]
133            enable_certificate_chain_verification_cache: false,
134            #[cfg(target_family = "wasm")]
135            certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
136        }
137    }
138
139    /// Enable unstable features in the WASM client.
140    #[cfg(target_family = "wasm")]
141    pub fn with_unstable_features(self, unstable: bool) -> Self {
142        Self { unstable, ..self }
143    }
144}
145
146/// Structure that aggregates the available clients for each of the Mithril types of certified data.
147///
148/// Use the [ClientBuilder] to instantiate it easily.
149#[derive(Clone)]
150pub struct Client {
151    certificate_client: Arc<CertificateClient>,
152    mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
153    snapshot_client: Arc<SnapshotClient>,
154    cardano_database_client: Arc<CardanoDatabaseClient>,
155    cardano_transaction_client: Arc<CardanoTransactionClient>,
156    cardano_stake_distribution_client: Arc<CardanoStakeDistributionClient>,
157    mithril_era_client: Arc<MithrilEraClient>,
158}
159
160impl Client {
161    /// Get the client that fetches and verifies Mithril certificates.
162    pub fn certificate(&self) -> Arc<CertificateClient> {
163        self.certificate_client.clone()
164    }
165
166    /// Get the client that fetches Mithril stake distributions.
167    pub fn mithril_stake_distribution(&self) -> Arc<MithrilStakeDistributionClient> {
168        self.mithril_stake_distribution_client.clone()
169    }
170
171    /// Get the client that fetches and downloads Mithril snapshots.
172    #[deprecated(since = "0.12.35", note = "superseded by `cardano_database_v2`")]
173    pub fn cardano_database(&self) -> Arc<SnapshotClient> {
174        self.snapshot_client.clone()
175    }
176
177    /// Get the client that fetches and downloads Cardano database snapshots.
178    pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
179        self.cardano_database_client.clone()
180    }
181
182    /// Get the client that fetches and verifies Mithril Cardano transaction proof.
183    pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
184        self.cardano_transaction_client.clone()
185    }
186
187    /// Get the client that fetches Cardano stake distributions.
188    pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
189        self.cardano_stake_distribution_client.clone()
190    }
191
192    /// Get the client that fetches the current Mithril era.
193    pub fn mithril_era_client(&self) -> Arc<MithrilEraClient> {
194        self.mithril_era_client.clone()
195    }
196}
197
198/// Builder that can be used to create a [Client] easily or with custom dependencies.
199pub struct ClientBuilder {
200    aggregator_discovery: AggregatorDiscoveryType,
201    #[cfg(not(target_family = "wasm"))]
202    aggregator_capabilities: Option<RequiredAggregatorCapabilities>,
203    #[cfg(not(target_family = "wasm"))]
204    aggregator_discoverer: Option<Arc<dyn AggregatorDiscoverer>>,
205    genesis_verification_key: Option<GenesisVerificationKey>,
206    origin_tag: Option<String>,
207    client_type: Option<String>,
208    #[cfg(feature = "fs")]
209    ancillary_verification_key: Option<String>,
210    aggregator_client: Option<Arc<dyn AggregatorClient>>,
211    certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
212    #[cfg(feature = "fs")]
213    http_file_downloader: Option<Arc<dyn FileDownloader>>,
214    #[cfg(feature = "unstable")]
215    certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
216    era_fetcher: Option<Arc<dyn EraFetcher>>,
217    logger: Option<Logger>,
218    feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
219    options: ClientOptions,
220}
221
222impl ClientBuilder {
223    /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given
224    /// endpoint and with the given genesis verification key.
225    #[deprecated(
226        since = "0.12.36",
227        note = "Use `new` method instead and set the genesis verification key with `set_genesis_verification_key`"
228    )]
229    pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder {
230        Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())).set_genesis_verification_key(
231            GenesisVerificationKey::JsonHex(genesis_verification_key.to_string()),
232        )
233    }
234
235    /// Constructs a new `ClientBuilder` that automatically discovers the aggregator for the given
236    /// Mithril network and with the given genesis verification key.
237    #[cfg(not(target_family = "wasm"))]
238    pub fn automatic(network: &str, genesis_verification_key: &str) -> ClientBuilder {
239        Self::new(AggregatorDiscoveryType::Automatic(MithrilNetwork::new(
240            network.to_string(),
241        )))
242        .set_genesis_verification_key(GenesisVerificationKey::JsonHex(
243            genesis_verification_key.to_string(),
244        ))
245    }
246
247    /// Constructs a new `ClientBuilder` without any dependency set.
248    pub fn new(aggregator_discovery: AggregatorDiscoveryType) -> ClientBuilder {
249        Self {
250            aggregator_discovery,
251            #[cfg(not(target_family = "wasm"))]
252            aggregator_capabilities: None,
253            #[cfg(not(target_family = "wasm"))]
254            aggregator_discoverer: None,
255            genesis_verification_key: None,
256            origin_tag: None,
257            client_type: None,
258            #[cfg(feature = "fs")]
259            ancillary_verification_key: None,
260            aggregator_client: None,
261            certificate_verifier: None,
262            #[cfg(feature = "fs")]
263            http_file_downloader: None,
264            #[cfg(feature = "unstable")]
265            certificate_verifier_cache: None,
266            era_fetcher: None,
267            logger: None,
268            feedback_receivers: vec![],
269            options: ClientOptions::default(),
270        }
271    }
272
273    /// Sets the genesis verification key to use when verifying certificates.
274    pub fn set_genesis_verification_key(
275        mut self,
276        genesis_verification_key: GenesisVerificationKey,
277    ) -> ClientBuilder {
278        self.genesis_verification_key = Some(genesis_verification_key);
279
280        self
281    }
282
283    /// Sets the aggregator capabilities expected to be matched by the aggregator with which the client will interact.
284    #[cfg(not(target_family = "wasm"))]
285    pub fn with_capabilities(
286        mut self,
287        capabilities: RequiredAggregatorCapabilities,
288    ) -> ClientBuilder {
289        self.aggregator_capabilities = Some(capabilities);
290
291        self
292    }
293
294    /// Sets the aggregator discoverer to use to find the aggregator endpoint when in automatic discovery.
295    #[cfg(not(target_family = "wasm"))]
296    pub fn with_aggregator_discoverer(
297        mut self,
298        discoverer: Arc<dyn AggregatorDiscoverer>,
299    ) -> ClientBuilder {
300        self.aggregator_discoverer = Some(discoverer);
301
302        self
303    }
304
305    /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`.
306    ///
307    /// The builder will try to create the missing dependencies using default implementations
308    /// if possible.
309    pub fn build(self) -> MithrilResult<Client> {
310        let logger = self
311            .logger
312            .clone()
313            .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
314
315        let genesis_verification_key = match self.genesis_verification_key {
316            Some(GenesisVerificationKey::JsonHex(ref key)) => key,
317            None => {
318                return Err(anyhow!(
319                    "The genesis verification key must be provided to build the client with the 'set_genesis_verification_key' function"
320                ));
321            }
322        };
323
324        let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
325
326        let aggregator_client = match self.aggregator_client {
327            None => Arc::new(self.build_aggregator_client(logger.clone())?),
328            Some(client) => client,
329        };
330
331        let mithril_era_client = match self.era_fetcher {
332            None => Arc::new(MithrilEraClient::new(Arc::new(
333                AggregatorHttpEraFetcher::new(aggregator_client.clone()),
334            ))),
335            Some(era_fetcher) => Arc::new(MithrilEraClient::new(era_fetcher)),
336        };
337
338        let certificate_verifier = match self.certificate_verifier {
339            None => Arc::new(
340                MithrilCertificateVerifier::new(
341                    aggregator_client.clone(),
342                    genesis_verification_key,
343                    feedback_sender.clone(),
344                    #[cfg(feature = "unstable")]
345                    self.certificate_verifier_cache,
346                    logger.clone(),
347                )
348                .with_context(|| "Building certificate verifier failed")?,
349            ),
350            Some(verifier) => verifier,
351        };
352        let certificate_client = Arc::new(CertificateClient::new(
353            aggregator_client.clone(),
354            certificate_verifier,
355            logger.clone(),
356        ));
357
358        let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
359            aggregator_client.clone(),
360        ));
361
362        #[cfg(feature = "fs")]
363        let http_file_downloader = match self.http_file_downloader {
364            None => Arc::new(RetryDownloader::new(
365                Arc::new(
366                    HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
367                        .with_context(|| "Building http file downloader failed")?,
368                ),
369                FileDownloadRetryPolicy::default(),
370            )),
371            Some(http_file_downloader) => http_file_downloader,
372        };
373
374        #[cfg(feature = "fs")]
375        let ancillary_verifier = match self.ancillary_verification_key {
376            None => None,
377            Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
378                verification_key
379                    .try_into()
380                    .with_context(|| "Building ancillary verifier failed")?,
381            ))),
382        };
383
384        let snapshot_client = Arc::new(SnapshotClient::new(
385            aggregator_client.clone(),
386            #[cfg(feature = "fs")]
387            http_file_downloader.clone(),
388            #[cfg(feature = "fs")]
389            ancillary_verifier.clone(),
390            #[cfg(feature = "fs")]
391            feedback_sender.clone(),
392            #[cfg(feature = "fs")]
393            logger.clone(),
394        ));
395
396        let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
397            aggregator_client.clone(),
398            #[cfg(feature = "fs")]
399            http_file_downloader,
400            #[cfg(feature = "fs")]
401            ancillary_verifier,
402            #[cfg(feature = "fs")]
403            feedback_sender,
404            #[cfg(feature = "fs")]
405            Arc::new(TimestampTempDirectoryProvider::new(&format!(
406                "{}",
407                Utc::now().timestamp_micros()
408            ))),
409            #[cfg(feature = "fs")]
410            logger,
411        ));
412
413        let cardano_transaction_client =
414            Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
415
416        let cardano_stake_distribution_client =
417            Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
418
419        Ok(Client {
420            certificate_client,
421            mithril_stake_distribution_client,
422            snapshot_client,
423            cardano_database_client,
424            cardano_transaction_client,
425            cardano_stake_distribution_client,
426            mithril_era_client,
427        })
428    }
429
430    /// Discover available aggregator endpoints for the given Mithril network and required capabilities.
431    #[cfg(not(target_family = "wasm"))]
432    pub fn discover_aggregator(
433        &self,
434        network: &MithrilNetwork,
435    ) -> MithrilResult<impl Iterator<Item = AggregatorEndpoint>> {
436        let discoverer = self
437            .aggregator_discoverer
438            .clone()
439            .unwrap_or_else(|| Self::default_aggregator_discoverer());
440        let discoverer = if let Some(capabilities) = &self.aggregator_capabilities {
441            Arc::new(CapableAggregatorDiscoverer::new(
442                capabilities.to_owned(),
443                discoverer.clone(),
444            )) as Arc<dyn AggregatorDiscoverer>
445        } else {
446            discoverer as Arc<dyn AggregatorDiscoverer>
447        };
448
449        tokio::task::block_in_place(move || {
450            tokio::runtime::Handle::current().block_on(async move {
451                discoverer
452                    .get_available_aggregators(network.to_owned())
453                    .await
454                    .with_context(|| "Discovering aggregator endpoint failed")
455            })
456        })
457    }
458
459    /// Default aggregator discoverer to use to find the aggregator endpoint when in automatic discovery.
460    #[cfg(not(target_family = "wasm"))]
461    fn default_aggregator_discoverer() -> Arc<dyn AggregatorDiscoverer> {
462        Arc::new(ShuffleAggregatorDiscoverer::new(
463            Arc::new(HttpConfigAggregatorDiscoverer::default()),
464            {
465                let mut seed = [0u8; 32];
466                let timestamp = Utc::now().timestamp_nanos_opt().unwrap_or(0);
467                seed[..8].copy_from_slice(&timestamp.to_le_bytes());
468
469                StdRng::from_seed(seed)
470            },
471        ))
472    }
473
474    fn build_aggregator_client(&self, logger: Logger) -> MithrilResult<AggregatorHTTPClient> {
475        let aggregator_endpoint = match self.aggregator_discovery {
476            AggregatorDiscoveryType::Url(ref url) => url.clone(),
477            #[cfg(not(target_family = "wasm"))]
478            AggregatorDiscoveryType::Automatic(ref network) => self
479                .discover_aggregator(network)?
480                .next()
481                .ok_or_else(|| anyhow!("No aggregator was available through discovery"))?
482                .into(),
483        };
484        let endpoint_url = Url::parse(&aggregator_endpoint).with_context(|| {
485            format!("Invalid aggregator endpoint, it must be a correctly formed url: '{aggregator_endpoint}'")
486        })?;
487
488        let headers = self.compute_http_headers();
489
490        AggregatorHTTPClient::new(
491            endpoint_url,
492            APIVersionProvider::compute_all_versions_sorted(),
493            logger,
494            Some(headers),
495        )
496        .with_context(|| "Building aggregator client failed")
497    }
498
499    fn compute_http_headers(&self) -> HashMap<String, String> {
500        let mut headers = self.options.http_headers.clone().unwrap_or_default();
501        if let Some(origin_tag) = self.origin_tag.clone() {
502            headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
503        }
504        if let Some(client_type) = self.client_type.clone() {
505            headers.insert(MITHRIL_CLIENT_TYPE_HEADER.to_string(), client_type);
506        } else if !headers.contains_key(MITHRIL_CLIENT_TYPE_HEADER) {
507            headers.insert(
508                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
509                DEFAULT_CLIENT_TYPE.to_string(),
510            );
511        }
512
513        headers
514    }
515
516    /// Set the [AggregatorClient] that will be used to request data to the aggregator.
517    #[deprecated(since = "0.12.33", note = "Will be removed in 0.13.0")]
518    pub fn with_aggregator_client(
519        mut self,
520        aggregator_client: Arc<dyn AggregatorClient>,
521    ) -> ClientBuilder {
522        self.aggregator_client = Some(aggregator_client);
523        self
524    }
525
526    /// Sets the [EraFetcher] that will be used by the client to retrieve the current Mithril era.
527    pub fn with_era_fetcher(mut self, era_fetcher: Arc<dyn EraFetcher>) -> ClientBuilder {
528        self.era_fetcher = Some(era_fetcher);
529        self
530    }
531
532    /// Set the [CertificateVerifier] that will be used to validate certificates.
533    pub fn with_certificate_verifier(
534        mut self,
535        certificate_verifier: Arc<dyn CertificateVerifier>,
536    ) -> ClientBuilder {
537        self.certificate_verifier = Some(certificate_verifier);
538        self
539    }
540
541    cfg_unstable! {
542        /// Set the [CertificateVerifierCache] that will be used to cache certificate validation results.
543        ///
544        /// Passing a `None` value will disable the cache if any was previously set.
545        pub fn with_certificate_verifier_cache(
546            mut self,
547            certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
548        ) -> ClientBuilder {
549            self.certificate_verifier_cache = certificate_verifier_cache;
550            self
551        }
552    }
553
554    cfg_fs! {
555        /// Set the [FileDownloader] that will be used to download artifacts with HTTP.
556        pub fn with_http_file_downloader(
557            mut self,
558            http_file_downloader: Arc<dyn FileDownloader>,
559        ) -> ClientBuilder {
560            self.http_file_downloader = Some(http_file_downloader);
561            self
562        }
563
564        /// Set the ancillary verification key to use when verifying the downloaded ancillary files.
565        pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
566            mut self,
567            ancillary_verification_key: T,
568        ) -> ClientBuilder {
569            self.ancillary_verification_key = ancillary_verification_key.into();
570            self
571        }
572    }
573
574    /// Set the [Logger] to use.
575    pub fn with_logger(mut self, logger: Logger) -> Self {
576        self.logger = Some(logger);
577        self
578    }
579
580    /// Set the origin tag.
581    pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
582        self.origin_tag = origin_tag;
583        self
584    }
585
586    /// Set the client type.
587    pub fn with_client_type(mut self, client_type: Option<String>) -> Self {
588        self.client_type = client_type;
589        self
590    }
591
592    /// Sets the options to be used by the client.
593    pub fn with_options(mut self, options: ClientOptions) -> Self {
594        self.options = options;
595        self
596    }
597
598    /// Add a [feedback receiver][FeedbackReceiver] to receive [events][crate::feedback::MithrilEvent]
599    /// for tasks that can have a long duration (ie: snapshot download or a long certificate chain
600    /// validation).
601    pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
602        self.feedback_receivers.push(receiver);
603        self
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610
611    fn default_headers() -> HashMap<String, String> {
612        HashMap::from([(
613            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
614            DEFAULT_CLIENT_TYPE.to_string(),
615        )])
616    }
617
618    #[tokio::test]
619    async fn compute_http_headers_returns_options_http_headers() {
620        let http_headers = default_headers();
621        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
622            .with_options(ClientOptions {
623                http_headers: Some(http_headers.clone()),
624            });
625
626        let computed_headers = client_builder.compute_http_headers();
627
628        assert_eq!(computed_headers, http_headers);
629    }
630
631    #[tokio::test]
632    async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
633        let http_headers = default_headers();
634        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
635            .with_options(ClientOptions {
636                http_headers: Some(http_headers.clone()),
637            })
638            .with_origin_tag(Some("CLIENT_TAG".to_string()));
639        let mut expected_headers = http_headers.clone();
640        expected_headers.insert(
641            MITHRIL_ORIGIN_TAG_HEADER.to_string(),
642            "CLIENT_TAG".to_string(),
643        );
644
645        let computed_headers = client_builder.compute_http_headers();
646        assert_eq!(computed_headers, expected_headers);
647    }
648
649    #[tokio::test]
650    async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
651        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
652            .with_options(ClientOptions { http_headers: None })
653            .with_origin_tag(Some("TEST".to_string()));
654        assert_eq!(None, builder.options.http_headers);
655        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
656
657        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
658        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
659            .with_options(ClientOptions {
660                http_headers: Some(http_headers.clone()),
661            })
662            .with_origin_tag(Some("TEST".to_string()));
663        assert_eq!(Some(http_headers), builder.options.http_headers);
664        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
665    }
666
667    #[tokio::test]
668    async fn test_with_origin_tag_can_be_unset() {
669        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
670        let client_options = ClientOptions {
671            http_headers: Some(http_headers.clone()),
672        };
673        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
674            .with_options(client_options)
675            .with_origin_tag(None);
676
677        assert_eq!(Some(http_headers), builder.options.http_headers);
678        assert_eq!(None, builder.origin_tag);
679    }
680
681    #[tokio::test]
682    async fn compute_http_headers_with_client_type_returns_options_http_headers_with_client_type() {
683        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
684        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
685            .with_options(ClientOptions {
686                http_headers: Some(http_headers.clone()),
687            })
688            .with_client_type(Some("CLIENT_TYPE".to_string()));
689
690        let computed_headers = client_builder.compute_http_headers();
691
692        assert_eq!(
693            computed_headers,
694            HashMap::from([
695                ("Key".to_string(), "Value".to_string()),
696                (
697                    MITHRIL_CLIENT_TYPE_HEADER.to_string(),
698                    "CLIENT_TYPE".to_string()
699                )
700            ])
701        );
702    }
703
704    #[tokio::test]
705    async fn compute_http_headers_with_options_containing_client_type_returns_client_type() {
706        let http_headers = HashMap::from([(
707            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
708            "client type from options".to_string(),
709        )]);
710        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
711            .with_options(ClientOptions {
712                http_headers: Some(http_headers.clone()),
713            });
714
715        let computed_headers = client_builder.compute_http_headers();
716
717        assert_eq!(computed_headers, http_headers);
718    }
719
720    #[tokio::test]
721    async fn test_with_client_type_not_overwrite_other_client_options_attributes() {
722        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
723            .with_options(ClientOptions { http_headers: None })
724            .with_client_type(Some("TEST".to_string()));
725        assert_eq!(None, builder.options.http_headers);
726        assert_eq!(Some("TEST".to_string()), builder.client_type);
727
728        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
729        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
730            .with_options(ClientOptions {
731                http_headers: Some(http_headers.clone()),
732            })
733            .with_client_type(Some("TEST".to_string()));
734        assert_eq!(Some(http_headers), builder.options.http_headers);
735        assert_eq!(Some("TEST".to_string()), builder.client_type);
736    }
737
738    #[tokio::test]
739    async fn test_given_a_none_client_type_compute_http_headers_will_set_client_type_to_default_value()
740     {
741        let builder_without_client_type =
742            ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()));
743        let computed_headers = builder_without_client_type.compute_http_headers();
744
745        assert_eq!(
746            computed_headers,
747            HashMap::from([(
748                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
749                DEFAULT_CLIENT_TYPE.to_string()
750            )])
751        );
752
753        let builder_with_none_client_type =
754            ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string())).with_client_type(None);
755        let computed_headers = builder_with_none_client_type.compute_http_headers();
756
757        assert_eq!(
758            computed_headers,
759            HashMap::from([(
760                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
761                DEFAULT_CLIENT_TYPE.to_string()
762            )])
763        );
764    }
765
766    #[tokio::test]
767    async fn test_compute_http_headers_will_compute_client_type_header_from_struct_attribute_over_options()
768     {
769        let http_headers = HashMap::from([(
770            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
771            "client type from options".to_string(),
772        )]);
773        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
774            .with_options(ClientOptions {
775                http_headers: Some(http_headers.clone()),
776            })
777            .with_client_type(Some("client type".to_string()));
778
779        let computed_headers = client_builder.compute_http_headers();
780
781        assert_eq!(
782            computed_headers,
783            HashMap::from([(
784                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
785                "client type".to_string()
786            )])
787        );
788    }
789}