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 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;
24pub mod query;
26
27pub 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
63pub 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 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 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}