Skip to main content

fedimint_api_client/api/global_api/
with_cache.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Debug;
3use std::future::ready;
4use std::num::NonZeroUsize;
5use std::sync::Arc;
6
7use bitcoin::secp256k1;
8use fedimint_connectors::{DynGuaridianConnection, PeerStatus, ServerResult};
9use fedimint_core::admin_client::{GuardianConfigBackup, SetLocalParamsRequest, SetupStatus};
10use fedimint_core::backup::{BackupStatistics, ClientBackupSnapshot};
11use fedimint_core::core::backup::SignedBackupRequest;
12use fedimint_core::core::{ModuleInstanceId, ModuleKind};
13use fedimint_core::endpoint_constants::{
14    ADD_PEER_SETUP_CODE_ENDPOINT, API_ANNOUNCEMENTS_ENDPOINT, AUDIT_ENDPOINT, AUTH_ENDPOINT,
15    AWAIT_SESSION_OUTCOME_ENDPOINT, AWAIT_TRANSACTION_ENDPOINT, BACKUP_ENDPOINT,
16    BACKUP_STATISTICS_ENDPOINT, CHAIN_ID_ENDPOINT, FEDIMINTD_VERSION_ENDPOINT,
17    GET_SETUP_CODE_ENDPOINT, GUARDIAN_CONFIG_BACKUP_ENDPOINT, GUARDIAN_METADATA_ENDPOINT,
18    INVITE_CODE_ENDPOINT, PEER_AVAILABILITY_ENDPOINT, RECOVER_ENDPOINT,
19    RESET_PEER_SETUP_CODES_ENDPOINT, RESTART_FEDERATION_SETUP_ENDPOINT, SESSION_COUNT_ENDPOINT,
20    SESSION_STATUS_ENDPOINT, SESSION_STATUS_V2_ENDPOINT, SET_LOCAL_PARAMS_ENDPOINT,
21    SETUP_STATUS_ENDPOINT, SHUTDOWN_ENDPOINT, SIGN_API_ANNOUNCEMENT_ENDPOINT,
22    SIGN_GUARDIAN_METADATA_ENDPOINT, START_DKG_ENDPOINT, STATUS_ENDPOINT,
23    SUBMIT_API_ANNOUNCEMENT_ENDPOINT, SUBMIT_GUARDIAN_METADATA_ENDPOINT,
24    SUBMIT_TRANSACTION_ENDPOINT,
25};
26use fedimint_core::invite_code::InviteCode;
27use fedimint_core::module::audit::AuditSummary;
28use fedimint_core::module::registry::ModuleDecoderRegistry;
29use fedimint_core::module::{
30    ApiAuth, ApiRequestErased, ApiVersion, SerdeModuleEncoding, SerdeModuleEncodingBase64,
31};
32use fedimint_core::net::api_announcement::{
33    SignedApiAnnouncement, SignedApiAnnouncementSubmission,
34};
35use fedimint_core::peer_availability::PeerAvailability;
36use fedimint_core::session_outcome::{
37    AcceptedItem, SessionOutcome, SessionStatus, SessionStatusV2,
38};
39use fedimint_core::task::{MaybeSend, MaybeSync};
40use fedimint_core::transaction::{SerdeTransaction, Transaction, TransactionSubmissionOutcome};
41use fedimint_core::util::SafeUrl;
42use fedimint_core::{ChainId, NumPeersExt, PeerId, TransactionId, apply, async_trait_maybe_send};
43use fedimint_logging::LOG_CLIENT_NET_API;
44use futures::future::join_all;
45use futures::stream::BoxStream;
46use itertools::Itertools;
47use rand::seq::SliceRandom;
48use serde_json::Value;
49use tokio::sync::OnceCell;
50use tracing::{debug, trace};
51
52use super::super::{DynModuleApi, IGlobalFederationApi, IRawFederationApi, StatusResponse};
53use crate::api::{
54    FederationApiExt, FederationError, FederationGeneralError, FederationResult,
55    VERSION_THAT_INTRODUCED_GET_SESSION_STATUS_V2,
56};
57use crate::query::FilterMapThreshold;
58
59/// Convenience extension trait used for wrapping [`IRawFederationApi`] in
60/// a [`GlobalFederationApiWithCache`]
61pub trait GlobalFederationApiWithCacheExt
62where
63    Self: Sized,
64{
65    fn with_cache(self) -> GlobalFederationApiWithCache<Self>;
66}
67
68impl<T> GlobalFederationApiWithCacheExt for T
69where
70    T: IRawFederationApi + MaybeSend + MaybeSync + 'static,
71{
72    fn with_cache(self) -> GlobalFederationApiWithCache<T> {
73        GlobalFederationApiWithCache::new(self)
74    }
75}
76
77/// [`IGlobalFederationApi`] wrapping some `T: IRawFederationApi` and adding
78/// a tiny bit of caching.
79///
80/// Use [`GlobalFederationApiWithCacheExt::with_cache`] to create.
81#[derive(Debug)]
82pub struct GlobalFederationApiWithCache<T> {
83    pub(crate) inner: T,
84    /// Small LRU used as [`IGlobalFederationApi::await_block`] cache.
85    ///
86    /// This is mostly to avoid multiple client module recovery processes
87    /// re-requesting same blocks and putting burden on the federation.
88    ///
89    /// The LRU can be be fairly small, as if the modules are
90    /// (near-)bottlenecked on fetching blocks they will naturally
91    /// synchronize, or split into a handful of groups. And if they are not,
92    /// no LRU here is going to help them.
93    pub(crate) await_session_lru:
94        Arc<tokio::sync::Mutex<lru::LruCache<u64, Arc<OnceCell<SessionOutcome>>>>>,
95
96    /// Like [`Self::await_session_lru`], but for
97    /// [`IGlobalFederationApi::get_session_status`].
98    ///
99    /// In theory these two LRUs have the same content, but one is locked by
100    /// potentially long-blocking operation, while the other non-blocking one.
101    /// Given how tiny they are, it's not worth complicating things to unify
102    /// them.
103    pub(crate) get_session_status_lru:
104        Arc<tokio::sync::Mutex<lru::LruCache<u64, Arc<OnceCell<SessionOutcome>>>>>,
105}
106
107impl<T> GlobalFederationApiWithCache<T> {
108    pub fn new(inner: T) -> GlobalFederationApiWithCache<T> {
109        Self {
110            inner,
111            await_session_lru: Arc::new(tokio::sync::Mutex::new(lru::LruCache::new(
112                NonZeroUsize::new(512).expect("is non-zero"),
113            ))),
114            get_session_status_lru: Arc::new(tokio::sync::Mutex::new(lru::LruCache::new(
115                NonZeroUsize::new(512).expect("is non-zero"),
116            ))),
117        }
118    }
119}
120
121impl<T> GlobalFederationApiWithCache<T>
122where
123    T: IRawFederationApi + MaybeSend + MaybeSync + 'static,
124{
125    pub(crate) async fn await_block_raw(
126        &self,
127        block_index: u64,
128        decoders: &ModuleDecoderRegistry,
129    ) -> FederationResult<SessionOutcome> {
130        if block_index.is_multiple_of(100) {
131            debug!(target: LOG_CLIENT_NET_API, block_index, "Awaiting block's outcome from Federation");
132        } else {
133            trace!(target: LOG_CLIENT_NET_API, block_index, "Awaiting block's outcome from Federation");
134        }
135        let response = self
136            .request_current_consensus::<SerdeModuleEncoding<SessionOutcome>>(
137                AWAIT_SESSION_OUTCOME_ENDPOINT.to_string(),
138                ApiRequestErased::new(block_index),
139            )
140            .await?;
141
142        response.try_into_inner(decoders).map_err(|e| {
143            FederationError::general(
144                AWAIT_SESSION_OUTCOME_ENDPOINT.to_string(),
145                ApiRequestErased::new(block_index),
146                FederationGeneralError::Decode(e),
147            )
148        })
149    }
150
151    pub(crate) fn select_peers_for_status(&self) -> impl Iterator<Item = PeerId> + '_ {
152        let mut peers = self.all_peers().iter().copied().collect_vec();
153        peers.shuffle(&mut rand::thread_rng());
154        peers.into_iter()
155    }
156
157    pub(crate) async fn get_session_status_raw_v2(
158        &self,
159        block_index: u64,
160        broadcast_public_keys: &BTreeMap<PeerId, secp256k1::PublicKey>,
161        decoders: &ModuleDecoderRegistry,
162    ) -> FederationResult<SessionStatus> {
163        if block_index.is_multiple_of(100) {
164            debug!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v2");
165        } else {
166            trace!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v2");
167        }
168        let params = ApiRequestErased::new(block_index);
169        let mut last_error = None;
170        // fetch serially
171        for peer_id in self.select_peers_for_status() {
172            let decoded = self
173                .request_single_peer_federation::<SerdeModuleEncodingBase64<SessionStatusV2>>(
174                    SESSION_STATUS_V2_ENDPOINT.to_string(),
175                    params.clone(),
176                    peer_id,
177                )
178                .await
179                .and_then(|s| {
180                    s.try_into_inner(decoders).map_err(|e| {
181                        FederationError::general(
182                            SESSION_STATUS_V2_ENDPOINT.to_string(),
183                            params.clone(),
184                            FederationGeneralError::Decode(e),
185                        )
186                    })
187                });
188
189            match decoded {
190                Ok(SessionStatusV2::Complete(signed_session_outcome)) => {
191                    if signed_session_outcome.verify(broadcast_public_keys, block_index) {
192                        // early return
193                        return Ok(SessionStatus::Complete(
194                            signed_session_outcome.session_outcome,
195                        ));
196                    }
197                    last_error = Some(FederationError::general(
198                        SESSION_STATUS_V2_ENDPOINT.to_string(),
199                        params.clone(),
200                        FederationGeneralError::InvalidSignature,
201                    ));
202                }
203                Ok(SessionStatusV2::Initial | SessionStatusV2::Pending(..)) => {
204                    // no signature: use fallback method
205                    return self.get_session_status_raw(block_index, decoders).await;
206                }
207                Err(err) => {
208                    last_error = Some(err);
209                }
210            }
211            // if we loop then we must have last_error
212            assert!(last_error.is_some());
213        }
214        Err(last_error.expect("must have at least one peer"))
215    }
216
217    pub(crate) async fn get_session_status_raw(
218        &self,
219        block_index: u64,
220        decoders: &ModuleDecoderRegistry,
221    ) -> FederationResult<SessionStatus> {
222        if block_index.is_multiple_of(100) {
223            debug!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v1");
224        } else {
225            trace!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v1");
226        }
227        let response = self
228            .request_current_consensus::<SerdeModuleEncoding<SessionStatus>>(
229                SESSION_STATUS_ENDPOINT.to_string(),
230                ApiRequestErased::new(block_index),
231            )
232            .await?;
233
234        response
235            .try_into_inner(&decoders.clone().with_fallback())
236            .map_err(|e| {
237                FederationError::general(
238                    SESSION_STATUS_ENDPOINT.to_string(),
239                    ApiRequestErased::new(block_index),
240                    FederationGeneralError::Decode(e),
241                )
242            })
243    }
244}
245
246#[apply(async_trait_maybe_send!)]
247impl<T> IRawFederationApi for GlobalFederationApiWithCache<T>
248where
249    T: IRawFederationApi + MaybeSend + MaybeSync + 'static,
250{
251    fn all_peers(&self) -> &BTreeSet<PeerId> {
252        self.inner.all_peers()
253    }
254
255    fn self_peer(&self) -> Option<PeerId> {
256        self.inner.self_peer()
257    }
258
259    fn with_module(&self, id: ModuleInstanceId) -> DynModuleApi {
260        self.inner.with_module(id)
261    }
262
263    /// Make request to a specific federation peer by `peer_id`
264    async fn request_raw(
265        &self,
266        peer_id: PeerId,
267        method: &str,
268        params: &ApiRequestErased,
269    ) -> ServerResult<Value> {
270        self.inner.request_raw(peer_id, method, params).await
271    }
272
273    fn connection_status_stream(&self) -> BoxStream<'static, BTreeMap<PeerId, PeerStatus>> {
274        self.inner.connection_status_stream()
275    }
276
277    async fn wait_for_initialized_connections(&self) {
278        self.inner.wait_for_initialized_connections().await;
279    }
280
281    async fn get_peer_connection(&self, peer_id: PeerId) -> ServerResult<DynGuaridianConnection> {
282        self.inner.get_peer_connection(peer_id).await
283    }
284}
285
286#[apply(async_trait_maybe_send!)]
287impl<T> IGlobalFederationApi for GlobalFederationApiWithCache<T>
288where
289    T: IRawFederationApi + MaybeSend + MaybeSync + 'static,
290{
291    async fn await_block(
292        &self,
293        session_idx: u64,
294        decoders: &ModuleDecoderRegistry,
295    ) -> FederationResult<SessionOutcome> {
296        let mut lru_lock = self.await_session_lru.lock().await;
297
298        let entry_arc = lru_lock
299            .get_or_insert(session_idx, || Arc::new(OnceCell::new()))
300            .clone();
301
302        // we drop the lru lock so requests for other `session_idx` can work in parallel
303        drop(lru_lock);
304
305        entry_arc
306            .get_or_try_init(|| self.await_block_raw(session_idx, decoders))
307            .await
308            .cloned()
309    }
310
311    async fn get_session_status(
312        &self,
313        session_idx: u64,
314        decoders: &ModuleDecoderRegistry,
315        core_api_version: ApiVersion,
316        broadcast_public_keys: Option<&BTreeMap<PeerId, secp256k1::PublicKey>>,
317    ) -> FederationResult<SessionStatus> {
318        enum NoCacheErr {
319            Initial,
320            Pending(Vec<AcceptedItem>),
321            Err(FederationError),
322        }
323
324        let mut lru_lock = self.get_session_status_lru.lock().await;
325
326        let entry_arc = lru_lock
327            .get_or_insert(session_idx, || Arc::new(OnceCell::new()))
328            .clone();
329
330        // we drop the lru lock so requests for other `session_idx` can work in parallel
331        drop(lru_lock);
332
333        match entry_arc
334            .get_or_try_init(|| async {
335                let session_status =
336                    if core_api_version < VERSION_THAT_INTRODUCED_GET_SESSION_STATUS_V2 {
337                        self.get_session_status_raw(session_idx, decoders).await
338                    } else if let Some(broadcast_public_keys) = broadcast_public_keys {
339                        self.get_session_status_raw_v2(session_idx, broadcast_public_keys, decoders)
340                            .await
341                    } else {
342                        self.get_session_status_raw(session_idx, decoders).await
343                    };
344                match session_status {
345                    Err(e) => Err(NoCacheErr::Err(e)),
346                    Ok(SessionStatus::Initial) => Err(NoCacheErr::Initial),
347                    Ok(SessionStatus::Pending(s)) => Err(NoCacheErr::Pending(s)),
348                    // only status we can cache (hance outer Ok)
349                    Ok(SessionStatus::Complete(s)) => Ok(s),
350                }
351            })
352            .await
353            .cloned()
354        {
355            Ok(s) => Ok(SessionStatus::Complete(s)),
356            Err(NoCacheErr::Initial) => Ok(SessionStatus::Initial),
357            Err(NoCacheErr::Pending(s)) => Ok(SessionStatus::Pending(s)),
358            Err(NoCacheErr::Err(e)) => Err(e),
359        }
360    }
361
362    async fn submit_transaction(
363        &self,
364        tx: Transaction,
365    ) -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
366        self.request_current_consensus_retry(
367            SUBMIT_TRANSACTION_ENDPOINT.to_owned(),
368            ApiRequestErased::new(SerdeTransaction::from(&tx)),
369        )
370        .await
371    }
372
373    async fn session_count(&self) -> FederationResult<u64> {
374        self.request_current_consensus(
375            SESSION_COUNT_ENDPOINT.to_owned(),
376            ApiRequestErased::default(),
377        )
378        .await
379    }
380
381    async fn await_transaction(&self, txid: TransactionId) -> TransactionId {
382        self.request_current_consensus_retry(
383            AWAIT_TRANSACTION_ENDPOINT.to_owned(),
384            ApiRequestErased::new(txid),
385        )
386        .await
387    }
388
389    async fn upload_backup(&self, request: &SignedBackupRequest) -> FederationResult<()> {
390        self.request_current_consensus(BACKUP_ENDPOINT.to_owned(), ApiRequestErased::new(request))
391            .await
392    }
393
394    async fn download_backup(
395        &self,
396        id: &secp256k1::PublicKey,
397    ) -> FederationResult<BTreeMap<PeerId, Option<ClientBackupSnapshot>>> {
398        self.request_with_strategy(
399            FilterMapThreshold::new(
400                |_, snapshot| ready(Ok(snapshot)),
401                self.all_peers().to_num_peers(),
402            ),
403            RECOVER_ENDPOINT.to_owned(),
404            ApiRequestErased::new(id),
405        )
406        .await
407    }
408
409    async fn setup_status(&self, auth: ApiAuth) -> FederationResult<SetupStatus> {
410        self.request_admin(SETUP_STATUS_ENDPOINT, ApiRequestErased::default(), auth)
411            .await
412    }
413
414    async fn set_local_params(
415        &self,
416        name: String,
417        federation_name: Option<String>,
418        disable_base_fees: Option<bool>,
419        enabled_modules: Option<BTreeSet<ModuleKind>>,
420        federation_size: Option<u32>,
421        auth: ApiAuth,
422    ) -> FederationResult<String> {
423        self.request_admin(
424            SET_LOCAL_PARAMS_ENDPOINT,
425            ApiRequestErased::new(SetLocalParamsRequest {
426                name,
427                federation_name,
428                disable_base_fees,
429                enabled_modules,
430                federation_size,
431            }),
432            auth,
433        )
434        .await
435    }
436
437    async fn add_peer_connection_info(
438        &self,
439        info: String,
440        auth: ApiAuth,
441    ) -> FederationResult<String> {
442        self.request_admin(
443            ADD_PEER_SETUP_CODE_ENDPOINT,
444            ApiRequestErased::new(info),
445            auth,
446        )
447        .await
448    }
449
450    async fn reset_peer_setup_codes(&self, auth: ApiAuth) -> FederationResult<()> {
451        self.request_admin(
452            RESET_PEER_SETUP_CODES_ENDPOINT,
453            ApiRequestErased::default(),
454            auth,
455        )
456        .await
457    }
458
459    async fn get_setup_code(&self, auth: ApiAuth) -> FederationResult<Option<String>> {
460        self.request_admin(GET_SETUP_CODE_ENDPOINT, ApiRequestErased::default(), auth)
461            .await
462    }
463
464    async fn start_dkg(&self, auth: ApiAuth) -> FederationResult<()> {
465        self.request_admin(START_DKG_ENDPOINT, ApiRequestErased::default(), auth)
466            .await
467    }
468
469    async fn status(&self) -> FederationResult<StatusResponse> {
470        self.request_admin_no_auth(STATUS_ENDPOINT, ApiRequestErased::default())
471            .await
472    }
473
474    async fn audit(&self, auth: ApiAuth) -> FederationResult<AuditSummary> {
475        self.request_admin(AUDIT_ENDPOINT, ApiRequestErased::default(), auth)
476            .await
477    }
478
479    async fn guardian_config_backup(
480        &self,
481        auth: ApiAuth,
482    ) -> FederationResult<GuardianConfigBackup> {
483        self.request_admin(
484            GUARDIAN_CONFIG_BACKUP_ENDPOINT,
485            ApiRequestErased::default(),
486            auth,
487        )
488        .await
489    }
490
491    async fn auth(&self, auth: ApiAuth) -> FederationResult<()> {
492        self.request_admin(AUTH_ENDPOINT, ApiRequestErased::default(), auth)
493            .await
494    }
495
496    async fn restart_federation_setup(&self, auth: ApiAuth) -> FederationResult<()> {
497        self.request_admin(
498            RESTART_FEDERATION_SETUP_ENDPOINT,
499            ApiRequestErased::default(),
500            auth,
501        )
502        .await
503    }
504
505    async fn submit_api_announcement(
506        &self,
507        announcement_peer_id: PeerId,
508        announcement: SignedApiAnnouncement,
509    ) -> FederationResult<()> {
510        let peer_errors = join_all(self.all_peers().iter().map(|&peer_id| {
511            let announcement_inner = announcement.clone();
512            async move {
513                (
514                    peer_id,
515                    self.request_single_peer::<()>(
516                        SUBMIT_API_ANNOUNCEMENT_ENDPOINT.into(),
517                        ApiRequestErased::new(SignedApiAnnouncementSubmission {
518                            signed_api_announcement: announcement_inner,
519                            peer_id: announcement_peer_id,
520                        }),
521                        peer_id,
522                    )
523                    .await,
524                )
525            }
526        }))
527        .await
528        .into_iter()
529        .filter_map(|(peer_id, result)| match result {
530            Ok(()) => None,
531            Err(e) => Some((peer_id, e)),
532        })
533        .collect::<BTreeMap<_, _>>();
534
535        if peer_errors.is_empty() {
536            Ok(())
537        } else {
538            Err(FederationError {
539                method: SUBMIT_API_ANNOUNCEMENT_ENDPOINT.to_string(),
540                params: serde_json::to_value(announcement).expect("can be serialized"),
541                general: None,
542                peer_errors,
543            })
544        }
545    }
546
547    async fn api_announcements(
548        &self,
549        guardian: PeerId,
550    ) -> ServerResult<BTreeMap<PeerId, SignedApiAnnouncement>> {
551        self.request_single_peer(
552            API_ANNOUNCEMENTS_ENDPOINT.to_owned(),
553            ApiRequestErased::default(),
554            guardian,
555        )
556        .await
557    }
558
559    async fn sign_api_announcement(
560        &self,
561        api_url: SafeUrl,
562        auth: ApiAuth,
563    ) -> FederationResult<SignedApiAnnouncement> {
564        self.request_admin(
565            SIGN_API_ANNOUNCEMENT_ENDPOINT,
566            ApiRequestErased::new(api_url),
567            auth,
568        )
569        .await
570    }
571
572    async fn submit_guardian_metadata(
573        &self,
574        announcement_peer_id: PeerId,
575        metadata: fedimint_core::net::guardian_metadata::SignedGuardianMetadata,
576    ) -> FederationResult<()> {
577        use fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission;
578        let peer_errors = join_all(self.all_peers().iter().map(|&peer_id| {
579            let metadata_inner = metadata.clone();
580            async move {
581                (
582                    peer_id,
583                    self.request_single_peer::<()>(
584                        SUBMIT_GUARDIAN_METADATA_ENDPOINT.into(),
585                        ApiRequestErased::new(SignedGuardianMetadataSubmission {
586                            signed_guardian_metadata: metadata_inner,
587                            peer_id: announcement_peer_id,
588                        }),
589                        peer_id,
590                    )
591                    .await,
592                )
593            }
594        }))
595        .await
596        .into_iter()
597        .filter_map(|(peer_id, result)| match result {
598            Ok(()) => None,
599            Err(e) => Some((peer_id, e)),
600        })
601        .collect::<BTreeMap<_, _>>();
602
603        if peer_errors.is_empty() {
604            Ok(())
605        } else {
606            Err(FederationError {
607                method: SUBMIT_GUARDIAN_METADATA_ENDPOINT.to_string(),
608                params: serde_json::to_value(&metadata).expect("can be serialized"),
609                general: None,
610                peer_errors,
611            })
612        }
613    }
614
615    async fn guardian_metadata(
616        &self,
617        guardian: PeerId,
618    ) -> ServerResult<BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata>>
619    {
620        self.request_single_peer(
621            GUARDIAN_METADATA_ENDPOINT.to_owned(),
622            ApiRequestErased::default(),
623            guardian,
624        )
625        .await
626    }
627
628    async fn sign_guardian_metadata(
629        &self,
630        metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
631        auth: ApiAuth,
632    ) -> FederationResult<fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
633        self.request_admin(
634            SIGN_GUARDIAN_METADATA_ENDPOINT,
635            ApiRequestErased::new(metadata),
636            auth,
637        )
638        .await
639    }
640
641    async fn shutdown(&self, session: Option<u64>, auth: ApiAuth) -> FederationResult<()> {
642        self.request_admin(SHUTDOWN_ENDPOINT, ApiRequestErased::new(session), auth)
643            .await
644    }
645
646    async fn backup_statistics(&self, auth: ApiAuth) -> FederationResult<BackupStatistics> {
647        self.request_admin(
648            BACKUP_STATISTICS_ENDPOINT,
649            ApiRequestErased::default(),
650            auth,
651        )
652        .await
653    }
654
655    async fn fedimintd_version(&self, peer_id: PeerId) -> ServerResult<String> {
656        self.request_single_peer(
657            FEDIMINTD_VERSION_ENDPOINT.to_owned(),
658            ApiRequestErased::default(),
659            peer_id,
660        )
661        .await
662    }
663
664    async fn peer_availability(&self, peer_id: PeerId) -> ServerResult<PeerAvailability> {
665        self.request_single_peer(
666            PEER_AVAILABILITY_ENDPOINT.to_owned(),
667            ApiRequestErased::default(),
668            peer_id,
669        )
670        .await
671    }
672
673    async fn get_invite_code(&self, guardian: PeerId) -> ServerResult<InviteCode> {
674        self.request_single_peer(
675            INVITE_CODE_ENDPOINT.to_owned(),
676            ApiRequestErased::default(),
677            guardian,
678        )
679        .await
680    }
681
682    async fn chain_id(&self) -> FederationResult<ChainId> {
683        self.request_current_consensus(CHAIN_ID_ENDPOINT.to_owned(), ApiRequestErased::default())
684            .await
685    }
686}