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