Skip to main content

fedimint_client_module/
oplog.rs

1use std::fmt::Debug;
2use std::future;
3use std::time::SystemTime;
4
5use fedimint_core::core::OperationId;
6use fedimint_core::db::DatabaseTransaction;
7use fedimint_core::encoding::{
8    Decodable, DecodeError, Encodable, decode_field_from_finite_reader,
9    decode_legacy_system_time_from_finite_reader, encode_legacy_system_time, with_decoding_context,
10};
11use fedimint_core::module::registry::ModuleDecoderRegistry;
12use fedimint_core::task::{MaybeSend, MaybeSync};
13use fedimint_core::util::BoxStream;
14use fedimint_core::{apply, async_trait_maybe_send, maybe_add_send, maybe_add_send_sync};
15use futures::{StreamExt, stream};
16use serde::de::DeserializeOwned;
17use serde::{Deserialize, Serialize};
18
19/// Json value using string representation as db encoding.
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(transparent)]
22pub struct JsonStringed(pub serde_json::Value);
23
24impl Encodable for JsonStringed {
25    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
26        let json_str = serde_json::to_string(&self.0).expect("JSON serialization should not fail");
27        json_str.consensus_encode(writer)
28    }
29}
30
31impl Decodable for JsonStringed {
32    fn consensus_decode_partial<R: std::io::Read>(
33        r: &mut R,
34        modules: &ModuleDecoderRegistry,
35    ) -> Result<Self, DecodeError> {
36        let json_str = String::consensus_decode_partial(r, modules)?;
37        let value = serde_json::from_str(&json_str).map_err(DecodeError::from_err)?;
38        Ok(JsonStringed(value))
39    }
40}
41
42#[apply(async_trait_maybe_send!)]
43pub trait IOperationLog {
44    async fn get_operation(&self, operation_id: OperationId) -> Option<OperationLogEntry>;
45
46    async fn get_operation_dbtx(
47        &self,
48        dbtx: &mut DatabaseTransaction<'_>,
49        operation_id: OperationId,
50    ) -> Option<OperationLogEntry>;
51
52    async fn add_operation_log_entry_dbtx(
53        &self,
54        dbtx: &mut DatabaseTransaction<'_>,
55        operation_id: OperationId,
56        operation_type: &str,
57        operation_meta: serde_json::Value,
58    );
59
60    /// Wrap a module's operation update stream so that its last update is
61    /// cached as the operation's outcome — only when `is_terminal` reports
62    /// that update as a final state of the operation. Outcome resolution
63    /// (returning a cached outcome instead of a stream) happens in the typed
64    /// caller ([`crate::module::ClientContext::outcome_or_updates`]), which
65    /// can deserialize the cached value into the module's update type and
66    /// fall back to this stream when that fails.
67    fn caching_operation_update_stream(
68        &self,
69        operation_id: OperationId,
70        stream_gen: Box<maybe_add_send!(dyn FnOnce() -> BoxStream<'static, serde_json::Value>)>,
71        is_terminal: Box<maybe_add_send_sync!(dyn Fn(&serde_json::Value) -> bool)>,
72    ) -> BoxStream<'static, serde_json::Value>;
73}
74
75/// Represents the outcome of an operation, combining both the outcome value and
76/// its timestamp
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct OperationOutcome {
79    pub time: SystemTime,
80    pub outcome: JsonStringed,
81}
82
83impl Encodable for OperationOutcome {
84    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
85        encode_legacy_system_time(&self.time, writer)?;
86        self.outcome.consensus_encode(writer)
87    }
88}
89
90impl Decodable for OperationOutcome {
91    fn consensus_decode_partial_from_finite_reader<R: std::io::Read>(
92        reader: &mut R,
93        modules: &ModuleDecoderRegistry,
94    ) -> Result<Self, DecodeError> {
95        Ok(Self {
96            time: with_decoding_context(
97                decode_legacy_system_time_from_finite_reader(reader, modules),
98                "Decoding named block field: OperationOutcome{ ... time ... }",
99            )?,
100            outcome: decode_field_from_finite_reader(
101                reader,
102                modules,
103                "Decoding named block field: OperationOutcome{ ... outcome ... }",
104            )?,
105        })
106    }
107}
108
109/// Represents an operation triggered by a user, typically related to sending or
110/// receiving money.
111///
112/// There are three levels of introspection possible for `OperationLogEntry`s:
113///   1. The [`OperationLogEntry::operation_module_kind`] function returns the
114///      kind of the module that created the operation.
115///   2. The [`OperationLogEntry::meta`] function returns static meta data that
116///      was associated with the operation when it was created. Modules define
117///      their own meta structures, so the module kind has to be used to
118///      determine the structure of the meta data.
119///   3. To find out the current state of the operation there is a two-step
120///      process:
121///      * First, the [`OperationLogEntry::outcome`] function returns the
122///        outcome if the operation finished **and** the update subscription
123///        stream has been processed till its end at least once.
124///      * If that isn't the case, the [`OperationLogEntry::outcome`] method
125///        will return `None` and the appropriate update subscription function
126///        has to be called. See the respective client extension trait for these
127///        functions.
128#[derive(Debug, Serialize, Deserialize, Encodable, Decodable)]
129pub struct OperationLogEntry {
130    pub(crate) operation_module_kind: String,
131    pub(crate) meta: JsonStringed,
132    // TODO: probably change all that JSON to Dyn-types
133    pub(crate) outcome: Option<OperationOutcome>,
134}
135
136impl OperationLogEntry {
137    pub fn new(
138        operation_module_kind: String,
139        meta: JsonStringed,
140        outcome: Option<OperationOutcome>,
141    ) -> Self {
142        Self {
143            operation_module_kind,
144            meta,
145            outcome,
146        }
147    }
148
149    /// Returns the kind of the module that generated the operation
150    pub fn operation_module_kind(&self) -> &str {
151        &self.operation_module_kind
152    }
153
154    /// Returns the meta data of the operation. This is a JSON value that can be
155    /// either returned as a [`serde_json::Value`] or deserialized into a
156    /// specific type. The specific type should be named `<Module>OperationMeta`
157    /// in the module's client crate. The module can be determined by calling
158    /// [`OperationLogEntry::operation_module_kind`].
159    pub fn meta<M: DeserializeOwned>(&self) -> M {
160        self.try_meta()
161            .expect("JSON deserialization should not fail")
162    }
163
164    /// Fallible version of [`OperationLogEntry::meta`]. Used to avoid panics in
165    /// the case of failed past migrations, resulting in invalid encodings in
166    /// the DB.
167    pub fn try_meta<M: DeserializeOwned>(&self) -> Result<M, serde_json::Error> {
168        serde_json::from_value(self.meta.0.clone())
169    }
170
171    /// Returns the last state update of the operation, if any was cached yet.
172    /// If this hasn't been the case yet and `None` is returned subscribe to the
173    /// appropriate update stream.
174    ///
175    /// ## Determining the return type
176    /// [`OperationLogEntry::meta`] should tell you the which operation type of
177    /// a given module the outcome belongs to. The operation type will have a
178    /// corresponding `async fn subscribe_type(&self, operation_id:
179    /// OperationId) -> Result<UpdateStreamOrOutcome<TypeState>, E>;`
180    /// function that returns a `UpdateStreamOrOutcome<S>` where `S` is the
181    /// high-level state the operation is in. If this state is terminal, i.e.
182    /// the stream closes after returning it, it will be cached as the `outcome`
183    /// of the operation.
184    ///
185    /// This means the type to be used for deserializing the outcome is `S`,
186    /// often called `<OperationType>State`. Alternatively one can also use
187    /// [`serde_json::Value`] to get the unstructured data.
188    pub fn outcome<D: DeserializeOwned>(&self) -> Option<D> {
189        self.try_outcome()
190            .expect("JSON deserialization should not fail")
191    }
192
193    /// Fallible version of [`OperationLogEntry::outcome`]. Used to avoid panics
194    /// in the case of failed past migrations, resulting in invalid encodings in
195    /// the DB.
196    pub fn try_outcome<D: DeserializeOwned>(&self) -> Result<Option<D>, serde_json::Error> {
197        self.outcome
198            .as_ref()
199            .map(|outcome| serde_json::from_value(outcome.outcome.0.clone()))
200            .transpose()
201    }
202
203    /// Returns the time when the outcome was cached.
204    pub fn outcome_time(&self) -> Option<SystemTime> {
205        self.outcome.as_ref().map(|o| o.time)
206    }
207
208    pub fn set_outcome(&mut self, outcome: impl Into<Option<OperationOutcome>>) {
209        self.outcome = outcome.into();
210    }
211}
212
213/// Either a stream of operation updates if the operation hasn't finished yet or
214/// its outcome otherwise.
215pub enum UpdateStreamOrOutcome<U> {
216    UpdateStream(BoxStream<'static, U>),
217    Outcome(U),
218}
219
220impl<U: Debug> Debug for UpdateStreamOrOutcome<U> {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        match self {
223            UpdateStreamOrOutcome::UpdateStream(_) => write!(f, "UpdateStream"),
224            UpdateStreamOrOutcome::Outcome(o) => f.debug_tuple("Outcome").field(o).finish(),
225        }
226    }
227}
228
229impl<U> UpdateStreamOrOutcome<U>
230where
231    U: MaybeSend + MaybeSync + 'static,
232{
233    /// Returns a stream no matter if the operation is finished. If there
234    /// already is a cached outcome the stream will only return that, otherwise
235    /// all updates will be returned until the operation finishes.
236    pub fn into_stream(self) -> BoxStream<'static, U> {
237        match self {
238            UpdateStreamOrOutcome::UpdateStream(stream) => stream,
239            UpdateStreamOrOutcome::Outcome(outcome) => {
240                Box::pin(stream::once(future::ready(outcome)))
241            }
242        }
243    }
244
245    /// Awaits the outcome of the operation update stream, either by returning
246    /// the cached value or by consuming the entire stream and returning the
247    /// last update.
248    pub async fn await_outcome(self) -> Option<U> {
249        match self {
250            UpdateStreamOrOutcome::Outcome(outcome) => Some(outcome),
251            UpdateStreamOrOutcome::UpdateStream(mut stream) => {
252                let mut last_update = None;
253                while let Some(update) = stream.next().await {
254                    last_update = Some(update);
255                }
256                last_update
257            }
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests;