fedimint_client_module/
api.rs

1use std::collections::BTreeSet;
2use std::string::ToString;
3
4use fedimint_api_client::api::{DynModuleApi, IRawFederationApi, PeerResult};
5use fedimint_core::core::ModuleInstanceId;
6use fedimint_core::db::{Database, DatabaseTransaction};
7use fedimint_core::module::ApiRequestErased;
8use fedimint_core::task::{MaybeSend, MaybeSync};
9use fedimint_core::{PeerId, apply, async_trait_maybe_send};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use tokio::sync::watch;
13
14/// Event log event right before making an api call
15///
16/// Notably there is no guarantee that a corresponding [`ApiCallDone`]
17/// is ever called, or that the api call actually reached the server.
18#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct ApiCallStarted {
20    method: String,
21    peer_id: PeerId,
22}
23
24impl Event for ApiCallStarted {
25    const MODULE: Option<fedimint_core::core::ModuleKind> = None;
26
27    const KIND: EventKind = EventKind::from_static("api-call-started");
28
29    /// These were deemed heavy volume enough and mostly diagnostics, so they
30    /// are not persisted
31    const PERSIST: bool = false;
32}
33
34/// Event log event right after an api call
35///
36/// Notably there is no guarantee this event is always created. If the
37/// client completed the call, but was abruptly terminated before logging
38/// an event, the call might have completed on the server side, but never
39/// create this event.
40#[derive(Serialize, Deserialize, Debug, Clone)]
41pub struct ApiCallDone {
42    method: String,
43    peer_id: PeerId,
44    duration_ms: u64,
45    success: bool,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    error_str: Option<String>,
48}
49
50impl Event for ApiCallDone {
51    const MODULE: Option<fedimint_core::core::ModuleKind> = None;
52
53    const KIND: EventKind = EventKind::from_static("api-call-done");
54    const PERSIST: bool = false;
55}
56
57use fedimint_eventlog::{DBTransactionEventLogExt as _, Event, EventKind};
58
59/// Convenience extension trait used for wrapping [`IRawFederationApi`] in
60/// a [`ClientRawFederationApi`]
61pub trait ClientRawFederationApiExt
62where
63    Self: Sized,
64{
65    fn with_client_ext(
66        self,
67        db: Database,
68        log_ordering_wakeup_tx: watch::Sender<()>,
69    ) -> ClientRawFederationApi<Self>;
70}
71
72impl<T> ClientRawFederationApiExt for T
73where
74    T: IRawFederationApi + MaybeSend + MaybeSync + 'static,
75{
76    fn with_client_ext(
77        self,
78        db: Database,
79        log_ordering_wakeup_tx: watch::Sender<()>,
80    ) -> ClientRawFederationApi<T> {
81        db.ensure_global().expect("Must be given global db");
82        ClientRawFederationApi {
83            inner: self,
84            db,
85            log_ordering_wakeup_tx,
86        }
87    }
88}
89
90/// A wrapper over [`IRawFederationApi`] adding client side event logging
91///
92/// Create using [`ClientRawFederationApiExt::with_client_ext`]
93#[derive(Debug)]
94pub struct ClientRawFederationApi<I> {
95    inner: I,
96    db: Database,
97    log_ordering_wakeup_tx: watch::Sender<()>,
98}
99
100impl<I> ClientRawFederationApi<I> {
101    pub async fn log_event<E>(&self, event: E)
102    where
103        E: Event + Send,
104    {
105        let mut dbtx = self.db.begin_transaction().await;
106        self.log_event_dbtx(&mut dbtx, event).await;
107        dbtx.commit_tx().await;
108    }
109
110    pub async fn log_event_dbtx<E, Cap>(&self, dbtx: &mut DatabaseTransaction<'_, Cap>, event: E)
111    where
112        E: Event + Send,
113        Cap: Send,
114    {
115        dbtx.log_event(self.log_ordering_wakeup_tx.clone(), None, event)
116            .await;
117    }
118}
119
120#[apply(async_trait_maybe_send!)]
121impl<I> IRawFederationApi for ClientRawFederationApi<I>
122where
123    I: IRawFederationApi,
124{
125    fn all_peers(&self) -> &BTreeSet<PeerId> {
126        self.inner.all_peers()
127    }
128
129    fn self_peer(&self) -> Option<PeerId> {
130        self.inner.self_peer()
131    }
132
133    fn with_module(&self, id: ModuleInstanceId) -> DynModuleApi {
134        self.inner.with_module(id)
135    }
136
137    async fn request_raw(
138        &self,
139        peer_id: PeerId,
140        method: &str,
141        params: &ApiRequestErased,
142    ) -> PeerResult<Value> {
143        self.log_event(ApiCallStarted {
144            method: method.to_string(),
145            peer_id,
146        })
147        .await;
148
149        let start = fedimint_core::time::now();
150        let res = self.inner.request_raw(peer_id, method, params).await;
151        let end = fedimint_core::time::now();
152
153        self.log_event(ApiCallDone {
154            method: method.to_string(),
155            peer_id,
156            duration_ms: end
157                .duration_since(start)
158                .unwrap_or_default()
159                .as_millis()
160                .try_into()
161                .unwrap_or(u64::MAX),
162            success: res.is_ok(),
163            error_str: res.as_ref().err().map(ToString::to_string),
164        })
165        .await;
166
167        res
168    }
169}