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::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;
23pub mod query;
25
26impl ConnectorType {
27 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 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 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 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}