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    ) -> anyhow::Result<ClientConfig> {
32        debug!(target: LOG_CLIENT, %invite, "Downloading client config via invite code");
33
34        let federation_id = invite.federation_id();
35        let api = DynGlobalApi::from_endpoints(invite.peers(), &invite.api_secret()).await?;
36        let api_secret = invite.api_secret();
37
38        fedimint_core::util::retry(
39            "Downloading client config",
40            backoff_util::aggressive_backoff(),
41            || self.try_download_client_config(&api, federation_id, api_secret.clone()),
42        )
43        .await
44        .context("Failed to download client config")
45    }
46
47    /// Tries to download the [`ClientConfig`] only once.
48    pub async fn try_download_client_config(
49        &self,
50        api: &DynGlobalApi,
51        federation_id: FederationId,
52        api_secret: Option<String>,
53    ) -> anyhow::Result<ClientConfig> {
54        debug!(target: LOG_CLIENT, "Downloading client config from peer");
55        // TODO: use new download approach based on guardian PKs
56        let query_strategy = FilterMap::new(move |cfg: ClientConfig| {
57            if federation_id != cfg.global.calculate_federation_id() {
58                return Err(PeerError::ConditionFailed(anyhow::anyhow!(
59                    "FederationId in invite code does not match client config"
60                )));
61            }
62
63            Ok(cfg.global.api_endpoints)
64        });
65
66        let api_endpoints = api
67            .request_with_strategy(
68                query_strategy,
69                CLIENT_CONFIG_ENDPOINT.to_owned(),
70                ApiRequestErased::default(),
71            )
72            .await?;
73
74        // now we can build an api for all guardians and download the client config
75        let api_endpoints = api_endpoints.into_iter().map(|(peer, url)| (peer, url.url));
76
77        debug!(target: LOG_CLIENT, "Verifying client config with all peers");
78
79        let client_config = DynGlobalApi::from_endpoints(api_endpoints, &api_secret)
80            .await?
81            .request_current_consensus::<ClientConfig>(
82                CLIENT_CONFIG_ENDPOINT.to_owned(),
83                ApiRequestErased::default(),
84            )
85            .await?;
86
87        if client_config.calculate_federation_id() != federation_id {
88            bail!("Obtained client config has different federation id");
89        }
90
91        Ok(client_config)
92    }
93}