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