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::envs::is_running_in_test_env;
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
21use crate::api::ClientConfigDownloadError;
22
23pub mod api;
24pub mod metrics;
25pub mod query;
27
28fn client_config_download_backoff() -> backoff_util::FibonacciBackoff {
31 if is_running_in_test_env() {
32 backoff_util::custom_backoff(
33 std::time::Duration::from_millis(20),
34 std::time::Duration::from_millis(100),
35 Some(14),
36 )
37 } else {
38 backoff_util::aggressive_backoff()
39 }
40}
41
42pub async fn download_from_invite_code(
45 endpoints: &ConnectorRegistry,
46 invite: &InviteCode,
47) -> Result<(ClientConfig, DynGlobalApi), ClientConfigDownloadError> {
48 debug!(
49 target: LOG_CLIENT_NET,
50 %invite,
51 peers = ?invite.peers(),
52 "Downloading client config via invite code"
53 );
54
55 let federation_id = invite.federation_id();
56 let api_from_invite = DynGlobalApi::new(
57 endpoints.clone(),
58 invite.peers(),
59 invite.api_secret().as_deref(),
60 );
61 let api_secret = invite.api_secret();
62
63 fedimint_core::util::retry(
64 "Downloading client config",
65 client_config_download_backoff(),
66 || {
67 try_download_client_config(
68 endpoints,
69 &api_from_invite,
70 federation_id,
71 api_secret.clone(),
72 )
73 },
74 )
75 .await
76}
77
78pub async fn try_download_client_config(
80 endpoints: &ConnectorRegistry,
81 api_from_invite: &DynGlobalApi,
82 federation_id: FederationId,
83 api_secret: Option<String>,
84) -> Result<(ClientConfig, DynGlobalApi), ClientConfigDownloadError> {
85 debug!(target: LOG_CLIENT_NET, "Downloading client config from peer");
86 let query_strategy = FilterMap::new(move |cfg: ClientConfig| {
88 if federation_id != cfg.global.calculate_federation_id() {
89 return Err(ServerError::ConditionFailed(
90 "FederationId in invite code does not match client config".to_string(),
91 ));
92 }
93
94 Ok(cfg.global.api_endpoints)
95 });
96
97 let api_endpoints = api_from_invite
98 .request_with_strategy(
99 query_strategy,
100 CLIENT_CONFIG_ENDPOINT.to_owned(),
101 ApiRequestErased::default(),
102 )
103 .await?;
104
105 let api_endpoints = api_endpoints
107 .into_iter()
108 .map(|(peer, url)| (peer, url.url))
109 .collect();
110
111 debug!(target: LOG_CLIENT_NET, "Verifying client config with all peers");
112
113 let api_full = DynGlobalApi::new(endpoints.clone(), api_endpoints, api_secret.as_deref());
114 let client_config = api_full
115 .request_current_consensus::<ClientConfig>(
116 CLIENT_CONFIG_ENDPOINT.to_owned(),
117 ApiRequestErased::default(),
118 )
119 .await?;
120
121 let found = client_config.calculate_federation_id();
122 if found != federation_id {
123 return Err(ClientConfigDownloadError::FederationIdMismatch {
124 expected: federation_id,
125 found,
126 });
127 }
128
129 Ok((client_config, api_full))
130}