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::Connector;
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
20pub mod api;
21pub mod query;
23
24impl Connector {
25 pub async fn download_from_invite_code(
29 &self,
30 invite: &InviteCode,
31 iroh_enable_dht: bool,
32 iroh_enable_next: bool,
33 ) -> anyhow::Result<(ClientConfig, DynGlobalApi)> {
34 debug!(
35 target: LOG_CLIENT,
36 %invite,
37 peers = ?invite.peers(),
38 "Downloading client config via invite code"
39 );
40
41 let federation_id = invite.federation_id();
42 let api_from_invite = DynGlobalApi::from_endpoints(
43 invite.peers(),
44 &invite.api_secret(),
45 iroh_enable_dht,
46 iroh_enable_next,
47 )
48 .await?;
49 let api_secret = invite.api_secret();
50
51 fedimint_core::util::retry(
52 "Downloading client config",
53 backoff_util::aggressive_backoff(),
54 || {
55 self.try_download_client_config(
56 &api_from_invite,
57 federation_id,
58 api_secret.clone(),
59 iroh_enable_dht,
60 iroh_enable_next,
61 )
62 },
63 )
64 .await
65 .context("Failed to download client config")
66 }
67
68 pub async fn try_download_client_config(
70 &self,
71 api_from_invite: &DynGlobalApi,
72 federation_id: FederationId,
73 api_secret: Option<String>,
74 iroh_enable_dht: bool,
75 iroh_enable_next: bool,
76 ) -> anyhow::Result<(ClientConfig, DynGlobalApi)> {
77 debug!(target: LOG_CLIENT, "Downloading client config from peer");
78 let query_strategy = FilterMap::new(move |cfg: ClientConfig| {
80 if federation_id != cfg.global.calculate_federation_id() {
81 return Err(PeerError::ConditionFailed(anyhow::anyhow!(
82 "FederationId in invite code does not match client config"
83 )));
84 }
85
86 Ok(cfg.global.api_endpoints)
87 });
88
89 let api_endpoints = api_from_invite
90 .request_with_strategy(
91 query_strategy,
92 CLIENT_CONFIG_ENDPOINT.to_owned(),
93 ApiRequestErased::default(),
94 )
95 .await?;
96
97 let api_endpoints = api_endpoints.into_iter().map(|(peer, url)| (peer, url.url));
99
100 debug!(target: LOG_CLIENT, "Verifying client config with all peers");
101
102 let api_full = DynGlobalApi::from_endpoints(
103 api_endpoints,
104 &api_secret,
105 iroh_enable_dht,
106 iroh_enable_next,
107 )
108 .await?;
109 let client_config = api_full
110 .request_current_consensus::<ClientConfig>(
111 CLIENT_CONFIG_ENDPOINT.to_owned(),
112 ApiRequestErased::default(),
113 )
114 .await?;
115
116 if client_config.calculate_federation_id() != federation_id {
117 bail!("Obtained client config has different federation id");
118 }
119
120 Ok((client_config, api_full))
121 }
122}