fedimint_api_client/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::must_use_candidate)]
6#![allow(clippy::return_self_not_must_use)]
7
8use anyhow::{Context as _, bail};
9use api::net::Connector;
10use api::{DynGlobalApi, FederationApiExt as _, PeerError};
11use fedimint_core::config::{ClientConfig, FederationId};
12use fedimint_core::endpoint_constants::CLIENT_CONFIG_ENDPOINT;
13use fedimint_core::invite_code::InviteCode;
14use fedimint_core::module::ApiRequestErased;
15use fedimint_core::util::backoff_util;
16use fedimint_logging::LOG_CLIENT;
17use query::FilterMap;
18use tracing::debug;
19
20pub mod api;
21/// Client query system
22pub mod query;
23
24impl Connector {
25    /// Tries to download the [`ClientConfig`] from the federation with an
26    /// specified [`Connector`] variant, attempts to retry ten times before
27    /// giving up.
28    pub async fn download_from_invite_code(
29        &self,
30        invite: &InviteCode,
31        iroh_enable_dht: bool,
32        iroh_enable_next: bool,
33    ) -> anyhow::Result<(ClientConfig, DynGlobalApi)> {
34        debug!(
35            target: LOG_CLIENT,
36            %invite,
37            peers = ?invite.peers(),
38            "Downloading client config via invite code"
39        );
40
41        let federation_id = invite.federation_id();
42        let api_from_invite = DynGlobalApi::from_endpoints(
43            invite.peers(),
44            &invite.api_secret(),
45            iroh_enable_dht,
46            iroh_enable_next,
47        )
48        .await?;
49        let api_secret = invite.api_secret();
50
51        fedimint_core::util::retry(
52            "Downloading client config",
53            backoff_util::aggressive_backoff(),
54            || {
55                self.try_download_client_config(
56                    &api_from_invite,
57                    federation_id,
58                    api_secret.clone(),
59                    iroh_enable_dht,
60                    iroh_enable_next,
61                )
62            },
63        )
64        .await
65        .context("Failed to download client config")
66    }
67
68    /// Tries to download the [`ClientConfig`] only once.
69    pub async fn try_download_client_config(
70        &self,
71        api_from_invite: &DynGlobalApi,
72        federation_id: FederationId,
73        api_secret: Option<String>,
74        iroh_enable_dht: bool,
75        iroh_enable_next: bool,
76    ) -> anyhow::Result<(ClientConfig, DynGlobalApi)> {
77        debug!(target: LOG_CLIENT, "Downloading client config from peer");
78        // TODO: use new download approach based on guardian PKs
79        let query_strategy = FilterMap::new(move |cfg: ClientConfig| {
80            if federation_id != cfg.global.calculate_federation_id() {
81                return Err(PeerError::ConditionFailed(anyhow::anyhow!(
82                    "FederationId in invite code does not match client config"
83                )));
84            }
85
86            Ok(cfg.global.api_endpoints)
87        });
88
89        let api_endpoints = api_from_invite
90            .request_with_strategy(
91                query_strategy,
92                CLIENT_CONFIG_ENDPOINT.to_owned(),
93                ApiRequestErased::default(),
94            )
95            .await?;
96
97        // now we can build an api for all guardians and download the client config
98        let api_endpoints = api_endpoints.into_iter().map(|(peer, url)| (peer, url.url));
99
100        debug!(target: LOG_CLIENT, "Verifying client config with all peers");
101
102        let api_full = DynGlobalApi::from_endpoints(
103            api_endpoints,
104            &api_secret,
105            iroh_enable_dht,
106            iroh_enable_next,
107        )
108        .await?;
109        let client_config = api_full
110            .request_current_consensus::<ClientConfig>(
111                CLIENT_CONFIG_ENDPOINT.to_owned(),
112                ApiRequestErased::default(),
113            )
114            .await?;
115
116        if client_config.calculate_federation_id() != federation_id {
117            bail!("Obtained client config has different federation id");
118        }
119
120        Ok((client_config, api_full))
121    }
122}