fedimint_api_client/
lib.rs1#![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;
22pub mod query;
24
25pub 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
62pub 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 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 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}