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