1mod error;
2pub mod global_api;
3
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt::Debug;
6use std::future::pending;
7use std::pin::Pin;
8use std::result;
9use std::sync::Arc;
10
11use bitcoin::secp256k1;
12pub use error::{
13 ClientConfigDownloadError, FederationError, FederationGeneralError, OutputOutcomeError,
14};
15pub use fedimint_connectors::ServerResult;
16pub use fedimint_connectors::error::ServerError;
17use fedimint_connectors::{
18 ConnectionPool, Connectivity, ConnectorRegistry, DynGuaridianConnection, IGuardianConnection,
19 PeerStatus,
20};
21use fedimint_core::admin_client::{GuardianConfigBackup, ServerStatusLegacy, SetupStatus};
22use fedimint_core::backup::{BackupStatistics, ClientBackupSnapshot};
23use fedimint_core::core::backup::SignedBackupRequest;
24use fedimint_core::core::{Decoder, DynOutputOutcome, ModuleInstanceId, ModuleKind, OutputOutcome};
25use fedimint_core::encoding::{Decodable, Encodable};
26use fedimint_core::invite_code::InviteCode;
27use fedimint_core::module::audit::AuditSummary;
28use fedimint_core::module::registry::ModuleDecoderRegistry;
29use fedimint_core::module::{
30 ApiAuth, ApiMethod, ApiRequestErased, ApiVersion, SerdeModuleEncoding,
31};
32use fedimint_core::net::api_announcement::SignedApiAnnouncement;
33use fedimint_core::net::guardian_metadata::SignedGuardianMetadata;
34use fedimint_core::session_outcome::{SessionOutcome, SessionStatus};
35use fedimint_core::task::{MaybeSend, MaybeSync};
36use fedimint_core::transaction::{Transaction, TransactionSubmissionOutcome};
37use fedimint_core::util::backoff_util::api_networking_backoff;
38use fedimint_core::util::{FmtCompact as _, SafeUrl};
39use fedimint_core::{
40 ChainId, NumPeersExt, PeerId, TransactionId, apply, async_trait_maybe_send, dyn_newtype_define,
41 util,
42};
43use fedimint_logging::LOG_CLIENT_NET_API;
44use fedimint_metrics::HistogramExt as _;
45use futures::stream::{BoxStream, FuturesUnordered};
46use futures::{Future, StreamExt};
47use global_api::with_cache::GlobalFederationApiWithCache;
48use jsonrpsee_core::DeserializeOwned;
49use serde::{Deserialize, Serialize};
50use serde_json::Value;
51use tokio::sync::watch;
52use tokio_stream::wrappers::WatchStream;
53use tracing::{debug, instrument, trace, warn};
54
55use crate::metrics::{CLIENT_API_REQUEST_DURATION_SECONDS, CLIENT_API_REQUESTS_TOTAL};
56use crate::query::{QueryStep, QueryStrategy, ThresholdConsensus};
57
58pub const VERSION_THAT_INTRODUCED_GET_SESSION_STATUS_V2: ApiVersion = ApiVersion::new(0, 5);
59
60pub const VERSION_THAT_INTRODUCED_GET_SESSION_STATUS: ApiVersion =
61 ApiVersion { major: 0, minor: 1 };
62
63pub const VERSION_THAT_INTRODUCED_AWAIT_OUTPUTS_OUTCOMES: ApiVersion = ApiVersion::new(0, 8);
64pub type FederationResult<T> = Result<T, FederationError>;
65pub type SerdeOutputOutcome = SerdeModuleEncoding<DynOutputOutcome>;
66
67pub type OutputOutcomeResult<O> = result::Result<O, OutputOutcomeError>;
68
69#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable)]
73pub struct ApiVersionSet {
74 pub core: ApiVersion,
75 pub modules: BTreeMap<ModuleInstanceId, ApiVersion>,
76}
77
78#[apply(async_trait_maybe_send!)]
80pub trait IRawFederationApi: Debug + MaybeSend + MaybeSync {
81 fn all_peers(&self) -> &BTreeSet<PeerId>;
89
90 fn self_peer(&self) -> Option<PeerId>;
95
96 fn with_module(&self, id: ModuleInstanceId) -> DynModuleApi;
97
98 async fn request_raw(
100 &self,
101 peer_id: PeerId,
102 method: &str,
103 params: &ApiRequestErased,
104 ) -> ServerResult<Value>;
105
106 fn connection_status_stream(&self) -> BoxStream<'static, BTreeMap<PeerId, PeerStatus>>;
121 async fn wait_for_initialized_connections(&self);
126
127 async fn get_peer_connection(&self, peer_id: PeerId) -> ServerResult<DynGuaridianConnection>;
133}
134
135#[apply(async_trait_maybe_send!)]
138pub trait FederationApiExt: IRawFederationApi {
139 async fn request_single_peer<Ret>(
140 &self,
141 method: String,
142 params: ApiRequestErased,
143 peer: PeerId,
144 ) -> ServerResult<Ret>
145 where
146 Ret: DeserializeOwned,
147 {
148 self.request_raw(peer, &method, ¶ms)
149 .await
150 .and_then(|v| {
151 serde_json::from_value(v)
152 .map_err(|e| ServerError::ResponseDeserialization(Box::new(e)))
153 })
154 }
155
156 async fn request_single_peer_federation<FedRet>(
157 &self,
158 method: String,
159 params: ApiRequestErased,
160 peer_id: PeerId,
161 ) -> FederationResult<FedRet>
162 where
163 FedRet: serde::de::DeserializeOwned + Eq + Debug + Clone + MaybeSend,
164 {
165 self.request_raw(peer_id, &method, ¶ms)
166 .await
167 .and_then(|v| {
168 serde_json::from_value(v)
169 .map_err(|e| ServerError::ResponseDeserialization(Box::new(e)))
170 })
171 .map_err(|e| error::FederationError::new_one_peer(peer_id, method, params, e))
172 }
173
174 #[instrument(target = LOG_CLIENT_NET_API, skip_all, fields(method=method))]
177 async fn request_with_strategy<PR: DeserializeOwned + MaybeSend, FR: Debug>(
178 &self,
179 mut strategy: impl QueryStrategy<PR, FR> + MaybeSend,
180 method: String,
181 params: ApiRequestErased,
182 ) -> FederationResult<FR> {
183 #[cfg(not(target_family = "wasm"))]
187 let mut futures = FuturesUnordered::<Pin<Box<dyn Future<Output = _> + Send>>>::new();
188 #[cfg(target_family = "wasm")]
189 let mut futures = FuturesUnordered::<Pin<Box<dyn Future<Output = _>>>>::new();
190
191 for peer in self.all_peers() {
192 futures.push(Box::pin({
193 let method = &method;
194 let params = ¶ms;
195 async move {
196 let result = self
197 .request_single_peer(method.clone(), params.clone(), *peer)
198 .await;
199
200 (*peer, result)
201 }
202 }));
203 }
204
205 let mut peer_errors = BTreeMap::new();
206 let peer_error_threshold = self.all_peers().to_num_peers().one_honest();
207
208 loop {
209 let (peer, result) = futures
210 .next()
211 .await
212 .expect("Query strategy ran out of peers to query without returning a result");
213
214 match result {
215 Ok(response) => match strategy.process(peer, response).await {
216 QueryStep::Retry(peers) => {
217 for peer in peers {
218 futures.push(Box::pin({
219 let method = &method;
220 let params = ¶ms;
221 async move {
222 let result = self
223 .request_single_peer(method.clone(), params.clone(), peer)
224 .await;
225
226 (peer, result)
227 }
228 }));
229 }
230 }
231 QueryStep::Success(response) => return Ok(response),
232 QueryStep::Failure(e) => {
233 peer_errors.insert(peer, e);
234 }
235 QueryStep::Continue => {}
236 },
237 Err(e) => {
238 e.report_if_unusual(peer, "RequestWithStrategy");
239
240 if let QueryStep::Success(response) = strategy.process_error(peer, &e) {
245 return Ok(response);
246 }
247
248 peer_errors.insert(peer, e);
249 }
250 }
251
252 if peer_errors.len() == peer_error_threshold {
253 return Err(FederationError::peer_errors(
254 method.clone(),
255 params.params.clone(),
256 peer_errors,
257 ));
258 }
259 }
260 }
261
262 #[instrument(target = LOG_CLIENT_NET_API, level = "debug", skip(self, strategy))]
263 async fn request_with_strategy_retry<PR: DeserializeOwned + MaybeSend, FR: Debug>(
264 &self,
265 mut strategy: impl QueryStrategy<PR, FR> + MaybeSend,
266 method: String,
267 params: ApiRequestErased,
268 ) -> FR {
269 #[cfg(not(target_family = "wasm"))]
273 let mut futures = FuturesUnordered::<Pin<Box<dyn Future<Output = _> + Send>>>::new();
274 #[cfg(target_family = "wasm")]
275 let mut futures = FuturesUnordered::<Pin<Box<dyn Future<Output = _>>>>::new();
276
277 for peer in self.all_peers() {
278 futures.push(Box::pin({
279 let method = &method;
280 let params = ¶ms;
281 async move {
282 let response = util::retry(
283 format!("api-request-{method}-{peer}"),
284 api_networking_backoff(),
285 || async {
286 self.request_single_peer(method.clone(), params.clone(), *peer)
287 .await
288 .inspect_err(|e| {
289 e.report_if_unusual(*peer, "QueryWithStrategyRetry");
290 })
291 },
292 )
293 .await
294 .expect("Number of retries has no limit");
295
296 (*peer, response)
297 }
298 }));
299 }
300
301 loop {
302 let (peer, response) = match futures.next().await {
303 Some(t) => t,
304 None => pending().await,
305 };
306
307 match strategy.process(peer, response).await {
308 QueryStep::Retry(peers) => {
309 for peer in peers {
310 futures.push(Box::pin({
311 let method = &method;
312 let params = ¶ms;
313 async move {
314 let response = util::retry(
315 format!("api-request-{method}-{peer}"),
316 api_networking_backoff(),
317 || async {
318 self.request_single_peer(
319 method.clone(),
320 params.clone(),
321 peer,
322 )
323 .await
324 .inspect_err(|err| {
325 if err.is_unusual() {
326 debug!(target: LOG_CLIENT_NET_API, err = %err.fmt_compact(), "Unusual peer error");
327 }
328 })
329 },
330 )
331 .await
332 .expect("Number of retries has no limit");
333
334 (peer, response)
335 }
336 }));
337 }
338 }
339 QueryStep::Success(response) => return response,
340 QueryStep::Failure(e) => {
341 warn!(target: LOG_CLIENT_NET_API, "Query strategy returned non-retryable failure for peer {peer}: {e}");
342 }
343 QueryStep::Continue => {}
344 }
345 }
346 }
347
348 async fn request_current_consensus<Ret>(
349 &self,
350 method: String,
351 params: ApiRequestErased,
352 ) -> FederationResult<Ret>
353 where
354 Ret: DeserializeOwned + Eq + Debug + Clone + MaybeSend,
355 {
356 self.request_with_strategy(
357 ThresholdConsensus::new(self.all_peers().to_num_peers()),
358 method,
359 params,
360 )
361 .await
362 }
363
364 async fn request_current_consensus_retry<Ret>(
365 &self,
366 method: String,
367 params: ApiRequestErased,
368 ) -> Ret
369 where
370 Ret: DeserializeOwned + Eq + Debug + Clone + MaybeSend,
371 {
372 self.request_with_strategy_retry(
373 ThresholdConsensus::new(self.all_peers().to_num_peers()),
374 method,
375 params,
376 )
377 .await
378 }
379
380 async fn request_admin<Ret>(
381 &self,
382 method: &str,
383 params: ApiRequestErased,
384 auth: ApiAuth,
385 ) -> FederationResult<Ret>
386 where
387 Ret: DeserializeOwned + Eq + Debug + Clone + MaybeSend,
388 {
389 let Some(self_peer_id) = self.self_peer() else {
390 return Err(FederationError::general(
391 method,
392 params,
393 FederationGeneralError::AdminPeerIdNotSet,
394 ));
395 };
396
397 self.request_single_peer_federation(method.into(), params.with_auth(auth), self_peer_id)
398 .await
399 }
400
401 async fn request_admin_no_auth<Ret>(
402 &self,
403 method: &str,
404 params: ApiRequestErased,
405 ) -> FederationResult<Ret>
406 where
407 Ret: DeserializeOwned + Eq + Debug + Clone + MaybeSend,
408 {
409 let Some(self_peer_id) = self.self_peer() else {
410 return Err(FederationError::general(
411 method,
412 params,
413 FederationGeneralError::AdminPeerIdNotSet,
414 ));
415 };
416
417 self.request_single_peer_federation(method.into(), params, self_peer_id)
418 .await
419 }
420}
421
422#[apply(async_trait_maybe_send!)]
423impl<T: ?Sized> FederationApiExt for T where T: IRawFederationApi {}
424
425pub trait IModuleFederationApi: IRawFederationApi {}
427
428dyn_newtype_define! {
429 #[derive(Clone)]
430 pub DynModuleApi(Arc<IModuleFederationApi>)
431}
432
433dyn_newtype_define! {
434 #[derive(Clone)]
435 pub DynGlobalApi(Arc<IGlobalFederationApi>)
436}
437
438impl AsRef<dyn IGlobalFederationApi + 'static> for DynGlobalApi {
439 fn as_ref(&self) -> &(dyn IGlobalFederationApi + 'static) {
440 self.inner.as_ref()
441 }
442}
443
444impl DynGlobalApi {
445 pub fn new(
446 connectors: ConnectorRegistry,
447 peers: BTreeMap<PeerId, SafeUrl>,
448 api_secret: Option<&str>,
449 ) -> Self {
450 GlobalFederationApiWithCache::new(FederationApi::new(connectors, peers, None, api_secret))
451 .into()
452 }
453 pub fn new_admin(
454 connectors: ConnectorRegistry,
455 peer: PeerId,
456 url: SafeUrl,
457 api_secret: Option<&str>,
458 ) -> DynGlobalApi {
459 GlobalFederationApiWithCache::new(FederationApi::new(
460 connectors,
461 [(peer, url)].into(),
462 Some(peer),
463 api_secret,
464 ))
465 .into()
466 }
467
468 pub fn new_admin_setup(connectors: ConnectorRegistry, url: SafeUrl) -> Self {
469 Self::new_admin(
472 connectors,
473 PeerId::from(1024),
474 url,
475 None,
477 )
478 }
479}
480
481#[apply(async_trait_maybe_send!)]
483pub trait IGlobalFederationApi: IRawFederationApi {
484 async fn submit_transaction(
485 &self,
486 tx: Transaction,
487 ) -> SerdeModuleEncoding<TransactionSubmissionOutcome>;
488
489 async fn await_block(
490 &self,
491 block_index: u64,
492 decoders: &ModuleDecoderRegistry,
493 ) -> FederationResult<SessionOutcome>;
494
495 async fn get_session_status(
496 &self,
497 block_index: u64,
498 decoders: &ModuleDecoderRegistry,
499 core_api_version: ApiVersion,
500 broadcast_public_keys: Option<&BTreeMap<PeerId, secp256k1::PublicKey>>,
501 ) -> FederationResult<SessionStatus>;
502
503 async fn session_count(&self) -> FederationResult<u64>;
504
505 async fn await_transaction(&self, txid: TransactionId) -> TransactionId;
506
507 async fn upload_backup(&self, request: &SignedBackupRequest) -> FederationResult<()>;
508
509 async fn download_backup(
510 &self,
511 id: &secp256k1::PublicKey,
512 ) -> FederationResult<BTreeMap<PeerId, Option<ClientBackupSnapshot>>>;
513
514 async fn setup_status(&self, auth: ApiAuth) -> FederationResult<SetupStatus>;
515
516 async fn set_local_params(
517 &self,
518 name: String,
519 federation_name: Option<String>,
520 disable_base_fees: Option<bool>,
521 enabled_modules: Option<BTreeSet<ModuleKind>>,
522 federation_size: Option<u32>,
523 auth: ApiAuth,
524 ) -> FederationResult<String>;
525
526 async fn add_peer_connection_info(
527 &self,
528 info: String,
529 auth: ApiAuth,
530 ) -> FederationResult<String>;
531
532 async fn reset_peer_setup_codes(&self, auth: ApiAuth) -> FederationResult<()>;
534
535 async fn get_setup_code(&self, auth: ApiAuth) -> FederationResult<Option<String>>;
537
538 async fn start_dkg(&self, auth: ApiAuth) -> FederationResult<()>;
542
543 async fn status(&self) -> FederationResult<StatusResponse>;
545
546 async fn audit(&self, auth: ApiAuth) -> FederationResult<AuditSummary>;
548
549 async fn guardian_config_backup(&self, auth: ApiAuth)
551 -> FederationResult<GuardianConfigBackup>;
552
553 async fn auth(&self, auth: ApiAuth) -> FederationResult<()>;
555
556 async fn restart_federation_setup(&self, auth: ApiAuth) -> FederationResult<()>;
557
558 async fn submit_api_announcement(
560 &self,
561 peer_id: PeerId,
562 announcement: SignedApiAnnouncement,
563 ) -> FederationResult<()>;
564
565 async fn api_announcements(
566 &self,
567 guardian: PeerId,
568 ) -> ServerResult<BTreeMap<PeerId, SignedApiAnnouncement>>;
569
570 async fn sign_api_announcement(
571 &self,
572 api_url: SafeUrl,
573 auth: ApiAuth,
574 ) -> FederationResult<SignedApiAnnouncement>;
575
576 async fn submit_guardian_metadata(
578 &self,
579 peer_id: PeerId,
580 metadata: SignedGuardianMetadata,
581 ) -> FederationResult<()>;
582
583 async fn guardian_metadata(
584 &self,
585 guardian: PeerId,
586 ) -> ServerResult<BTreeMap<PeerId, SignedGuardianMetadata>>;
587
588 async fn sign_guardian_metadata(
589 &self,
590 metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
591 auth: ApiAuth,
592 ) -> FederationResult<SignedGuardianMetadata>;
593
594 async fn shutdown(&self, session: Option<u64>, auth: ApiAuth) -> FederationResult<()>;
595
596 async fn fedimintd_version(&self, peer_id: PeerId) -> ServerResult<String>;
598
599 async fn backup_statistics(&self, auth: ApiAuth) -> FederationResult<BackupStatistics>;
601
602 async fn get_invite_code(&self, guardian: PeerId) -> ServerResult<InviteCode>;
605
606 async fn chain_id(&self) -> FederationResult<ChainId>;
609}
610
611pub fn deserialize_outcome<R>(
612 outcome: &SerdeOutputOutcome,
613 module_decoder: &Decoder,
614) -> OutputOutcomeResult<R>
615where
616 R: OutputOutcome + MaybeSend,
617{
618 let dyn_outcome = outcome.try_into_inner_known_module_kind(module_decoder)?;
619
620 let module_instance_id = dyn_outcome.module_instance_id();
621
622 dyn_outcome
623 .as_any()
624 .downcast_ref()
625 .cloned()
626 .ok_or(OutputOutcomeError::WrongOutcomeType {
627 module_instance_id,
628 expected_type: std::any::type_name::<R>(),
629 })
630}
631
632#[derive(Clone, Debug)]
643pub struct FederationApi {
644 peers: BTreeMap<PeerId, SafeUrl>,
646 peers_keys: BTreeSet<PeerId>,
648 admin_id: Option<PeerId>,
650 module_id: Option<ModuleInstanceId>,
652 api_secret: Option<String>,
654 connection_pool: ConnectionPool<dyn IGuardianConnection>,
656}
657
658impl FederationApi {
659 pub fn new(
660 connectors: ConnectorRegistry,
661 peers: BTreeMap<PeerId, SafeUrl>,
662 admin_peer_id: Option<PeerId>,
663 api_secret: Option<&str>,
664 ) -> Self {
665 Self {
666 peers_keys: peers.keys().copied().collect(),
667 peers,
668 admin_id: admin_peer_id,
669 module_id: None,
670 api_secret: api_secret.map(ToOwned::to_owned),
671 connection_pool: ConnectionPool::new(connectors),
672 }
673 }
674
675 async fn get_or_create_connection(
676 &self,
677 url: &SafeUrl,
678 api_secret: Option<&str>,
679 ) -> ServerResult<DynGuaridianConnection> {
680 self.connection_pool
681 .get_or_create_connection(url, api_secret, |url, api_secret, connectors| async move {
682 let conn = connectors
683 .connect_guardian(&url, api_secret.as_deref())
684 .await?;
685 Ok(conn)
686 })
687 .await
688 }
689
690 async fn request(
691 &self,
692 peer: PeerId,
693 method: ApiMethod,
694 request: ApiRequestErased,
695 ) -> ServerResult<Value> {
696 trace!(target: LOG_CLIENT_NET_API, %peer, %method, "Api request");
697 let url = self
698 .peers
699 .get(&peer)
700 .ok_or_else(|| ServerError::InvalidPeerId { peer_id: peer })?;
701 let conn = self
702 .get_or_create_connection(url, self.api_secret.as_deref())
703 .await?;
704
705 let method_str = method.to_string();
706 let peer_str = peer.to_string();
707 let timer = CLIENT_API_REQUEST_DURATION_SECONDS
708 .with_label_values(&[&method_str, &peer_str])
709 .start_timer_ext();
710
711 let res = conn.request(method.clone(), request).await;
712
713 timer.observe_duration();
714
715 let result_label = if res.is_ok() { "success" } else { "error" }.to_string();
716 CLIENT_API_REQUESTS_TOTAL
717 .with_label_values(&[&method_str, &peer_str, &result_label])
718 .inc();
719
720 trace!(target: LOG_CLIENT_NET_API, ?method, res_ok = res.is_ok(), "Api response");
721
722 res
723 }
724
725 pub fn get_active_connection_receiver(&self) -> watch::Receiver<BTreeSet<SafeUrl>> {
729 self.connection_pool.get_active_connection_receiver()
730 }
731}
732
733impl IModuleFederationApi for FederationApi {}
734
735#[apply(async_trait_maybe_send!)]
736impl IRawFederationApi for FederationApi {
737 fn all_peers(&self) -> &BTreeSet<PeerId> {
738 &self.peers_keys
739 }
740
741 fn self_peer(&self) -> Option<PeerId> {
742 self.admin_id
743 }
744
745 fn with_module(&self, id: ModuleInstanceId) -> DynModuleApi {
746 FederationApi {
747 api_secret: self.api_secret.clone(),
748 peers: self.peers.clone(),
749 peers_keys: self.peers_keys.clone(),
750 admin_id: self.admin_id,
751 module_id: Some(id),
752 connection_pool: self.connection_pool.clone(),
753 }
754 .into()
755 }
756
757 #[instrument(
758 target = LOG_CLIENT_NET_API,
759 skip_all,
760 fields(
761 peer_id = %peer_id,
762 method = %method,
763 params = %params.params,
764 )
765 )]
766 async fn request_raw(
767 &self,
768 peer_id: PeerId,
769 method: &str,
770 params: &ApiRequestErased,
771 ) -> ServerResult<Value> {
772 let method = match self.module_id {
773 Some(module_id) => ApiMethod::Module(module_id, method.to_string()),
774 None => ApiMethod::Core(method.to_string()),
775 };
776
777 self.request(peer_id, method, params.clone()).await
778 }
779
780 fn connection_status_stream(&self) -> BoxStream<'static, BTreeMap<PeerId, PeerStatus>> {
781 let peers = self.peers.clone();
782 let pool = self.connection_pool.clone();
783 let active_rx = self.connection_pool.get_active_connection_receiver();
784
785 let membership_ticks = WatchStream::new(active_rx.clone()).map(|_| ());
791 let path_change_ticks =
792 WatchStream::new(self.connection_pool.connectivity_change_notifier()).map(|_| ());
793 let ticks = futures::stream::select(membership_ticks, path_change_ticks);
794
795 ticks
796 .map(move |()| {
797 let active_urls = active_rx.borrow().clone();
798 peers
799 .iter()
800 .map(|(peer_id, url)| {
801 let status = if active_urls.contains(url) {
802 match pool.connectivity(url) {
811 Connectivity::Unknown => PeerStatus::Disconnected,
812 connectivity => PeerStatus::Connected(connectivity),
813 }
814 } else {
815 PeerStatus::Disconnected
816 };
817 (*peer_id, status)
818 })
819 .collect()
820 })
821 .boxed()
822 }
823 async fn wait_for_initialized_connections(&self) {
824 self.connection_pool
825 .wait_for_initialized_connections()
826 .await;
827 }
828
829 async fn get_peer_connection(&self, peer_id: PeerId) -> ServerResult<DynGuaridianConnection> {
830 let url = self
831 .peers
832 .get(&peer_id)
833 .ok_or_else(|| ServerError::InvalidPeerId { peer_id })?;
834 self.get_or_create_connection(url, self.api_secret.as_deref())
835 .await
836 }
837}
838
839#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
841pub struct LegacyFederationStatus {
842 pub session_count: u64,
843 pub status_by_peer: BTreeMap<PeerId, LegacyPeerStatus>,
844 pub peers_online: u64,
845 pub peers_offline: u64,
846 pub peers_flagged: u64,
849 pub scheduled_shutdown: Option<u64>,
850}
851
852#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
853pub struct LegacyPeerStatus {
854 pub last_contribution: Option<u64>,
855 pub connection_status: LegacyP2PConnectionStatus,
856 pub flagged: bool,
859}
860
861#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
862#[serde(rename_all = "snake_case")]
863pub enum LegacyP2PConnectionStatus {
864 #[default]
865 Disconnected,
866 Connected,
867}
868
869#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
870pub struct StatusResponse {
871 pub server: ServerStatusLegacy,
872 pub federation: Option<LegacyFederationStatus>,
873}
874
875#[cfg(test)]
876mod tests;