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