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