Skip to main content

fedimint_api_client/api/global_api/
with_cache.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Debug;
3use std::num::NonZeroUsize;
4use std::sync::Arc;
5
6use anyhow::{anyhow, format_err};
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, 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    ) -> anyhow::Result<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        self.request_current_consensus::<SerdeModuleEncoding<SessionOutcome>>(
134            AWAIT_SESSION_OUTCOME_ENDPOINT.to_string(),
135            ApiRequestErased::new(block_index),
136        )
137        .await?
138        .try_into_inner(decoders)
139        .map_err(|e| anyhow!(e.to_string()))
140    }
141
142    pub(crate) fn select_peers_for_status(&self) -> impl Iterator<Item = PeerId> + '_ {
143        let mut peers = self.all_peers().iter().copied().collect_vec();
144        peers.shuffle(&mut rand::thread_rng());
145        peers.into_iter()
146    }
147
148    pub(crate) async fn get_session_status_raw_v2(
149        &self,
150        block_index: u64,
151        broadcast_public_keys: &BTreeMap<PeerId, secp256k1::PublicKey>,
152        decoders: &ModuleDecoderRegistry,
153    ) -> anyhow::Result<SessionStatus> {
154        if block_index.is_multiple_of(100) {
155            debug!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v2");
156        } else {
157            trace!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v2");
158        }
159        let params = ApiRequestErased::new(block_index);
160        let mut last_error = None;
161        // fetch serially
162        for peer_id in self.select_peers_for_status() {
163            match self
164                .request_single_peer_federation::<SerdeModuleEncodingBase64<SessionStatusV2>>(
165                    SESSION_STATUS_V2_ENDPOINT.to_string(),
166                    params.clone(),
167                    peer_id,
168                )
169                .await
170                .map_err(anyhow::Error::from)
171                .and_then(|s| Ok(s.try_into_inner(decoders)?))
172            {
173                Ok(SessionStatusV2::Complete(signed_session_outcome)) => {
174                    if signed_session_outcome.verify(broadcast_public_keys, block_index) {
175                        // early return
176                        return Ok(SessionStatus::Complete(
177                            signed_session_outcome.session_outcome,
178                        ));
179                    }
180                    last_error = Some(format_err!("Invalid signature"));
181                }
182                Ok(SessionStatusV2::Initial | SessionStatusV2::Pending(..)) => {
183                    // no signature: use fallback method
184                    return self.get_session_status_raw(block_index, decoders).await;
185                }
186                Err(err) => {
187                    last_error = Some(err);
188                }
189            }
190            // if we loop then we must have last_error
191            assert!(last_error.is_some());
192        }
193        Err(last_error.expect("must have at least one peer"))
194    }
195
196    pub(crate) async fn get_session_status_raw(
197        &self,
198        block_index: u64,
199        decoders: &ModuleDecoderRegistry,
200    ) -> anyhow::Result<SessionStatus> {
201        if block_index.is_multiple_of(100) {
202            debug!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v1");
203        } else {
204            trace!(target: LOG_CLIENT_NET_API, block_index, "Get session status raw v1");
205        }
206        self.request_current_consensus::<SerdeModuleEncoding<SessionStatus>>(
207            SESSION_STATUS_ENDPOINT.to_string(),
208            ApiRequestErased::new(block_index),
209        )
210        .await?
211        .try_into_inner(&decoders.clone().with_fallback())
212        .map_err(|e| anyhow!(e))
213    }
214}
215
216#[apply(async_trait_maybe_send!)]
217impl<T> IRawFederationApi for GlobalFederationApiWithCache<T>
218where
219    T: IRawFederationApi + MaybeSend + MaybeSync + 'static,
220{
221    fn all_peers(&self) -> &BTreeSet<PeerId> {
222        self.inner.all_peers()
223    }
224
225    fn self_peer(&self) -> Option<PeerId> {
226        self.inner.self_peer()
227    }
228
229    fn with_module(&self, id: ModuleInstanceId) -> DynModuleApi {
230        self.inner.with_module(id)
231    }
232
233    /// Make request to a specific federation peer by `peer_id`
234    async fn request_raw(
235        &self,
236        peer_id: PeerId,
237        method: &str,
238        params: &ApiRequestErased,
239    ) -> ServerResult<Value> {
240        self.inner.request_raw(peer_id, method, params).await
241    }
242
243    fn connection_status_stream(&self) -> BoxStream<'static, BTreeMap<PeerId, PeerStatus>> {
244        self.inner.connection_status_stream()
245    }
246
247    async fn wait_for_initialized_connections(&self) {
248        self.inner.wait_for_initialized_connections().await;
249    }
250
251    async fn get_peer_connection(&self, peer_id: PeerId) -> ServerResult<DynGuaridianConnection> {
252        self.inner.get_peer_connection(peer_id).await
253    }
254}
255
256#[apply(async_trait_maybe_send!)]
257impl<T> IGlobalFederationApi for GlobalFederationApiWithCache<T>
258where
259    T: IRawFederationApi + MaybeSend + MaybeSync + 'static,
260{
261    async fn await_block(
262        &self,
263        session_idx: u64,
264        decoders: &ModuleDecoderRegistry,
265    ) -> anyhow::Result<SessionOutcome> {
266        let mut lru_lock = self.await_session_lru.lock().await;
267
268        let entry_arc = lru_lock
269            .get_or_insert(session_idx, || Arc::new(OnceCell::new()))
270            .clone();
271
272        // we drop the lru lock so requests for other `session_idx` can work in parallel
273        drop(lru_lock);
274
275        entry_arc
276            .get_or_try_init(|| self.await_block_raw(session_idx, decoders))
277            .await
278            .cloned()
279    }
280
281    async fn get_session_status(
282        &self,
283        session_idx: u64,
284        decoders: &ModuleDecoderRegistry,
285        core_api_version: ApiVersion,
286        broadcast_public_keys: Option<&BTreeMap<PeerId, secp256k1::PublicKey>>,
287    ) -> anyhow::Result<SessionStatus> {
288        enum NoCacheErr {
289            Initial,
290            Pending(Vec<AcceptedItem>),
291            Err(anyhow::Error),
292        }
293
294        let mut lru_lock = self.get_session_status_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        match entry_arc
304            .get_or_try_init(|| async {
305                let session_status =
306                    if core_api_version < VERSION_THAT_INTRODUCED_GET_SESSION_STATUS_V2 {
307                        self.get_session_status_raw(session_idx, decoders).await
308                    } else if let Some(broadcast_public_keys) = broadcast_public_keys {
309                        self.get_session_status_raw_v2(session_idx, broadcast_public_keys, decoders)
310                            .await
311                    } else {
312                        self.get_session_status_raw(session_idx, decoders).await
313                    };
314                match session_status {
315                    Err(e) => Err(NoCacheErr::Err(e)),
316                    Ok(SessionStatus::Initial) => Err(NoCacheErr::Initial),
317                    Ok(SessionStatus::Pending(s)) => Err(NoCacheErr::Pending(s)),
318                    // only status we can cache (hance outer Ok)
319                    Ok(SessionStatus::Complete(s)) => Ok(s),
320                }
321            })
322            .await
323            .cloned()
324        {
325            Ok(s) => Ok(SessionStatus::Complete(s)),
326            Err(NoCacheErr::Initial) => Ok(SessionStatus::Initial),
327            Err(NoCacheErr::Pending(s)) => Ok(SessionStatus::Pending(s)),
328            Err(NoCacheErr::Err(e)) => Err(e),
329        }
330    }
331
332    async fn submit_transaction(
333        &self,
334        tx: Transaction,
335    ) -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
336        self.request_current_consensus_retry(
337            SUBMIT_TRANSACTION_ENDPOINT.to_owned(),
338            ApiRequestErased::new(SerdeTransaction::from(&tx)),
339        )
340        .await
341    }
342
343    async fn session_count(&self) -> FederationResult<u64> {
344        self.request_current_consensus(
345            SESSION_COUNT_ENDPOINT.to_owned(),
346            ApiRequestErased::default(),
347        )
348        .await
349    }
350
351    async fn await_transaction(&self, txid: TransactionId) -> TransactionId {
352        self.request_current_consensus_retry(
353            AWAIT_TRANSACTION_ENDPOINT.to_owned(),
354            ApiRequestErased::new(txid),
355        )
356        .await
357    }
358
359    async fn upload_backup(&self, request: &SignedBackupRequest) -> FederationResult<()> {
360        self.request_current_consensus(BACKUP_ENDPOINT.to_owned(), ApiRequestErased::new(request))
361            .await
362    }
363
364    async fn download_backup(
365        &self,
366        id: &secp256k1::PublicKey,
367    ) -> FederationResult<BTreeMap<PeerId, Option<ClientBackupSnapshot>>> {
368        self.request_with_strategy(
369            FilterMapThreshold::new(|_, snapshot| Ok(snapshot), self.all_peers().to_num_peers()),
370            RECOVER_ENDPOINT.to_owned(),
371            ApiRequestErased::new(id),
372        )
373        .await
374    }
375
376    async fn setup_status(&self, auth: ApiAuth) -> FederationResult<SetupStatus> {
377        self.request_admin(SETUP_STATUS_ENDPOINT, ApiRequestErased::default(), auth)
378            .await
379    }
380
381    async fn set_local_params(
382        &self,
383        name: String,
384        federation_name: Option<String>,
385        disable_base_fees: Option<bool>,
386        enabled_modules: Option<BTreeSet<ModuleKind>>,
387        federation_size: Option<u32>,
388        auth: ApiAuth,
389    ) -> FederationResult<String> {
390        self.request_admin(
391            SET_LOCAL_PARAMS_ENDPOINT,
392            ApiRequestErased::new(SetLocalParamsRequest {
393                name,
394                federation_name,
395                disable_base_fees,
396                enabled_modules,
397                federation_size,
398            }),
399            auth,
400        )
401        .await
402    }
403
404    async fn add_peer_connection_info(
405        &self,
406        info: String,
407        auth: ApiAuth,
408    ) -> FederationResult<String> {
409        self.request_admin(
410            ADD_PEER_SETUP_CODE_ENDPOINT,
411            ApiRequestErased::new(info),
412            auth,
413        )
414        .await
415    }
416
417    async fn reset_peer_setup_codes(&self, auth: ApiAuth) -> FederationResult<()> {
418        self.request_admin(
419            RESET_PEER_SETUP_CODES_ENDPOINT,
420            ApiRequestErased::default(),
421            auth,
422        )
423        .await
424    }
425
426    async fn get_setup_code(&self, auth: ApiAuth) -> FederationResult<Option<String>> {
427        self.request_admin(GET_SETUP_CODE_ENDPOINT, ApiRequestErased::default(), auth)
428            .await
429    }
430
431    async fn start_dkg(&self, auth: ApiAuth) -> FederationResult<()> {
432        self.request_admin(START_DKG_ENDPOINT, ApiRequestErased::default(), auth)
433            .await
434    }
435
436    async fn status(&self) -> FederationResult<StatusResponse> {
437        self.request_admin_no_auth(STATUS_ENDPOINT, ApiRequestErased::default())
438            .await
439    }
440
441    async fn audit(&self, auth: ApiAuth) -> FederationResult<AuditSummary> {
442        self.request_admin(AUDIT_ENDPOINT, ApiRequestErased::default(), auth)
443            .await
444    }
445
446    async fn guardian_config_backup(
447        &self,
448        auth: ApiAuth,
449    ) -> FederationResult<GuardianConfigBackup> {
450        self.request_admin(
451            GUARDIAN_CONFIG_BACKUP_ENDPOINT,
452            ApiRequestErased::default(),
453            auth,
454        )
455        .await
456    }
457
458    async fn auth(&self, auth: ApiAuth) -> FederationResult<()> {
459        self.request_admin(AUTH_ENDPOINT, ApiRequestErased::default(), auth)
460            .await
461    }
462
463    async fn restart_federation_setup(&self, auth: ApiAuth) -> FederationResult<()> {
464        self.request_admin(
465            RESTART_FEDERATION_SETUP_ENDPOINT,
466            ApiRequestErased::default(),
467            auth,
468        )
469        .await
470    }
471
472    async fn submit_api_announcement(
473        &self,
474        announcement_peer_id: PeerId,
475        announcement: SignedApiAnnouncement,
476    ) -> FederationResult<()> {
477        let peer_errors = join_all(self.all_peers().iter().map(|&peer_id| {
478            let announcement_inner = announcement.clone();
479            async move {
480                (
481                    peer_id,
482                    self.request_single_peer::<()>(
483                        SUBMIT_API_ANNOUNCEMENT_ENDPOINT.into(),
484                        ApiRequestErased::new(SignedApiAnnouncementSubmission {
485                            signed_api_announcement: announcement_inner,
486                            peer_id: announcement_peer_id,
487                        }),
488                        peer_id,
489                    )
490                    .await,
491                )
492            }
493        }))
494        .await
495        .into_iter()
496        .filter_map(|(peer_id, result)| match result {
497            Ok(()) => None,
498            Err(e) => Some((peer_id, e)),
499        })
500        .collect::<BTreeMap<_, _>>();
501
502        if peer_errors.is_empty() {
503            Ok(())
504        } else {
505            Err(FederationError {
506                method: SUBMIT_API_ANNOUNCEMENT_ENDPOINT.to_string(),
507                params: serde_json::to_value(announcement).expect("can be serialized"),
508                general: None,
509                peer_errors,
510            })
511        }
512    }
513
514    async fn api_announcements(
515        &self,
516        guardian: PeerId,
517    ) -> ServerResult<BTreeMap<PeerId, SignedApiAnnouncement>> {
518        self.request_single_peer(
519            API_ANNOUNCEMENTS_ENDPOINT.to_owned(),
520            ApiRequestErased::default(),
521            guardian,
522        )
523        .await
524    }
525
526    async fn sign_api_announcement(
527        &self,
528        api_url: SafeUrl,
529        auth: ApiAuth,
530    ) -> FederationResult<SignedApiAnnouncement> {
531        self.request_admin(
532            SIGN_API_ANNOUNCEMENT_ENDPOINT,
533            ApiRequestErased::new(api_url),
534            auth,
535        )
536        .await
537    }
538
539    async fn submit_guardian_metadata(
540        &self,
541        announcement_peer_id: PeerId,
542        metadata: fedimint_core::net::guardian_metadata::SignedGuardianMetadata,
543    ) -> FederationResult<()> {
544        use fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission;
545        let peer_errors = join_all(self.all_peers().iter().map(|&peer_id| {
546            let metadata_inner = metadata.clone();
547            async move {
548                (
549                    peer_id,
550                    self.request_single_peer::<()>(
551                        SUBMIT_GUARDIAN_METADATA_ENDPOINT.into(),
552                        ApiRequestErased::new(SignedGuardianMetadataSubmission {
553                            signed_guardian_metadata: metadata_inner,
554                            peer_id: announcement_peer_id,
555                        }),
556                        peer_id,
557                    )
558                    .await,
559                )
560            }
561        }))
562        .await
563        .into_iter()
564        .filter_map(|(peer_id, result)| match result {
565            Ok(()) => None,
566            Err(e) => Some((peer_id, e)),
567        })
568        .collect::<BTreeMap<_, _>>();
569
570        if peer_errors.is_empty() {
571            Ok(())
572        } else {
573            Err(FederationError {
574                method: SUBMIT_GUARDIAN_METADATA_ENDPOINT.to_string(),
575                params: serde_json::to_value(&metadata).expect("can be serialized"),
576                general: None,
577                peer_errors,
578            })
579        }
580    }
581
582    async fn guardian_metadata(
583        &self,
584        guardian: PeerId,
585    ) -> ServerResult<BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata>>
586    {
587        self.request_single_peer(
588            GUARDIAN_METADATA_ENDPOINT.to_owned(),
589            ApiRequestErased::default(),
590            guardian,
591        )
592        .await
593    }
594
595    async fn sign_guardian_metadata(
596        &self,
597        metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
598        auth: ApiAuth,
599    ) -> FederationResult<fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
600        self.request_admin(
601            SIGN_GUARDIAN_METADATA_ENDPOINT,
602            ApiRequestErased::new(metadata),
603            auth,
604        )
605        .await
606    }
607
608    async fn shutdown(&self, session: Option<u64>, auth: ApiAuth) -> FederationResult<()> {
609        self.request_admin(SHUTDOWN_ENDPOINT, ApiRequestErased::new(session), auth)
610            .await
611    }
612
613    async fn backup_statistics(&self, auth: ApiAuth) -> FederationResult<BackupStatistics> {
614        self.request_admin(
615            BACKUP_STATISTICS_ENDPOINT,
616            ApiRequestErased::default(),
617            auth,
618        )
619        .await
620    }
621
622    async fn fedimintd_version(&self, peer_id: PeerId) -> ServerResult<String> {
623        self.request_single_peer(
624            FEDIMINTD_VERSION_ENDPOINT.to_owned(),
625            ApiRequestErased::default(),
626            peer_id,
627        )
628        .await
629    }
630
631    async fn get_invite_code(&self, guardian: PeerId) -> ServerResult<InviteCode> {
632        self.request_single_peer(
633            INVITE_CODE_ENDPOINT.to_owned(),
634            ApiRequestErased::default(),
635            guardian,
636        )
637        .await
638    }
639
640    async fn chain_id(&self) -> FederationResult<ChainId> {
641        self.request_current_consensus(CHAIN_ID_ENDPOINT.to_owned(), ApiRequestErased::default())
642            .await
643    }
644}