Skip to main content

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