Skip to main content

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 api::{DynGlobalApi, FederationApiExt as _};
9use fedimint_connectors::ConnectorRegistry;
10use fedimint_connectors::error::ServerError;
11use fedimint_core::config::{ClientConfig, FederationId};
12use fedimint_core::endpoint_constants::CLIENT_CONFIG_ENDPOINT;
13use fedimint_core::envs::is_running_in_test_env;
14use fedimint_core::invite_code::InviteCode;
15use fedimint_core::module::ApiRequestErased;
16use fedimint_core::util::backoff_util;
17use fedimint_logging::LOG_CLIENT_NET;
18use query::FilterMap;
19use tracing::debug;
20
21use crate::api::ClientConfigDownloadError;
22
23pub mod api;
24pub mod metrics;
25/// Client query system
26pub mod query;
27
28/// Retry config downloads without making test fixtures wait through production
29/// backoffs. Only delays change: both policies allow fourteen retries.
30fn client_config_download_backoff() -> backoff_util::FibonacciBackoff {
31    if is_running_in_test_env() {
32        backoff_util::custom_backoff(
33            std::time::Duration::from_millis(20),
34            std::time::Duration::from_millis(100),
35            Some(14),
36        )
37    } else {
38        backoff_util::aggressive_backoff()
39    }
40}
41
42/// Tries to download the [`ClientConfig`], retrying up to fourteen times after
43/// the initial attempt before giving up.
44pub async fn download_from_invite_code(
45    endpoints: &ConnectorRegistry,
46    invite: &InviteCode,
47) -> Result<(ClientConfig, DynGlobalApi), ClientConfigDownloadError> {
48    debug!(
49        target: LOG_CLIENT_NET,
50        %invite,
51        peers = ?invite.peers(),
52        "Downloading client config via invite code"
53    );
54
55    let federation_id = invite.federation_id();
56    let api_from_invite = DynGlobalApi::new(
57        endpoints.clone(),
58        invite.peers(),
59        invite.api_secret().as_deref(),
60    );
61    let api_secret = invite.api_secret();
62
63    fedimint_core::util::retry(
64        "Downloading client config",
65        client_config_download_backoff(),
66        || {
67            try_download_client_config(
68                endpoints,
69                &api_from_invite,
70                federation_id,
71                api_secret.clone(),
72            )
73        },
74    )
75    .await
76}
77
78/// Tries to download the [`ClientConfig`] only once.
79pub async fn try_download_client_config(
80    endpoints: &ConnectorRegistry,
81    api_from_invite: &DynGlobalApi,
82    federation_id: FederationId,
83    api_secret: Option<String>,
84) -> Result<(ClientConfig, DynGlobalApi), ClientConfigDownloadError> {
85    debug!(target: LOG_CLIENT_NET, "Downloading client config from peer");
86    // TODO: use new download approach based on guardian PKs
87    let query_strategy = FilterMap::new(move |cfg: ClientConfig| {
88        if federation_id != cfg.global.calculate_federation_id() {
89            return Err(ServerError::ConditionFailed(
90                "FederationId in invite code does not match client config".to_string(),
91            ));
92        }
93
94        Ok(cfg.global.api_endpoints)
95    });
96
97    let api_endpoints = api_from_invite
98        .request_with_strategy(
99            query_strategy,
100            CLIENT_CONFIG_ENDPOINT.to_owned(),
101            ApiRequestErased::default(),
102        )
103        .await?;
104
105    // now we can build an api for all guardians and download the client config
106    let api_endpoints = api_endpoints
107        .into_iter()
108        .map(|(peer, url)| (peer, url.url))
109        .collect();
110
111    debug!(target: LOG_CLIENT_NET, "Verifying client config with all peers");
112
113    let api_full = DynGlobalApi::new(endpoints.clone(), api_endpoints, api_secret.as_deref());
114    let client_config = api_full
115        .request_current_consensus::<ClientConfig>(
116            CLIENT_CONFIG_ENDPOINT.to_owned(),
117            ApiRequestErased::default(),
118        )
119        .await?;
120
121    let found = client_config.calculate_federation_id();
122    if found != federation_id {
123        return Err(ClientConfigDownloadError::FederationIdMismatch {
124            expected: federation_id,
125            found,
126        });
127    }
128
129    Ok((client_config, api_full))
130}