Skip to main content

fedimint_client_module/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::doc_markdown)]
4#![allow(clippy::explicit_deref_methods)]
5#![allow(clippy::missing_errors_doc)]
6#![allow(clippy::missing_panics_doc)]
7#![allow(clippy::module_name_repetitions)]
8#![allow(clippy::must_use_candidate)]
9#![allow(clippy::needless_lifetimes)]
10#![allow(clippy::return_self_not_must_use)]
11#![allow(clippy::too_many_lines)]
12#![allow(clippy::type_complexity)]
13
14#[cfg(feature = "uniffi")]
15uniffi::setup_scaffolding!();
16
17use std::fmt::Debug;
18use std::ops::{self};
19use std::sync::Arc;
20
21use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
22use fedimint_core::config::ClientConfig;
23pub use fedimint_core::core::{IInput, IOutput, ModuleInstanceId, ModuleKind, OperationId};
24use fedimint_core::db::Database;
25use fedimint_core::module::registry::ModuleDecoderRegistry;
26use fedimint_core::module::{ApiAuth, ApiVersion};
27use fedimint_core::task::{MaybeSend, MaybeSync};
28use fedimint_core::util::{BoxStream, NextOrPending};
29use fedimint_core::{
30    Amount, PeerId, TransactionId, apply, async_trait_maybe_send, dyn_newtype_define,
31    maybe_add_send_sync,
32};
33use fedimint_eventlog::{Event, EventKind, EventPersistence};
34use fedimint_logging::LOG_CLIENT;
35use futures::StreamExt;
36use module::OutPointRange;
37use serde::{Deserialize, Serialize};
38use thiserror::Error;
39use tracing::debug;
40use transaction::{
41    ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputSM, TxSubmissionStatesSM,
42};
43
44pub use crate::module::{ClientModule, StateGenerator};
45use crate::sm::executor::ContextGen;
46use crate::sm::{ClientSMDatabaseTransaction, DynState, IState, State};
47use crate::transaction::{ClientInput, ClientOutputBundle, TxSubmissionStates};
48
49pub mod api;
50
51pub mod db;
52
53pub mod backup;
54/// Environment variables
55pub mod envs;
56pub mod meta;
57/// Module client interface definitions
58pub mod module;
59/// Operation log subsystem of the client
60pub mod oplog;
61/// Secret handling & derivation
62pub mod secret;
63/// Client state machine interfaces and executor implementation
64pub mod sm;
65/// Structs and interfaces to construct Fedimint transactions
66pub mod transaction;
67
68pub mod api_version_discovery;
69
70#[derive(Serialize, Deserialize)]
71pub struct TxCreatedEvent {
72    pub txid: TransactionId,
73    pub operation_id: OperationId,
74}
75
76impl Event for TxCreatedEvent {
77    const MODULE: Option<ModuleKind> = None;
78    const KIND: EventKind = EventKind::from_static("tx-created");
79    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
80}
81
82#[derive(Serialize, Deserialize)]
83pub struct TxAcceptedEvent {
84    txid: TransactionId,
85    operation_id: OperationId,
86}
87
88impl Event for TxAcceptedEvent {
89    const MODULE: Option<ModuleKind> = None;
90    const KIND: EventKind = EventKind::from_static("tx-accepted");
91    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
92}
93
94#[derive(Serialize, Deserialize)]
95pub struct TxRejectedEvent {
96    txid: TransactionId,
97    error: String,
98    operation_id: OperationId,
99}
100impl Event for TxRejectedEvent {
101    const MODULE: Option<ModuleKind> = None;
102    const KIND: EventKind = EventKind::from_static("tx-rejected");
103    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
104}
105
106#[derive(Serialize, Deserialize)]
107pub struct ModuleRecoveryStarted {
108    module_id: ModuleInstanceId,
109}
110
111impl ModuleRecoveryStarted {
112    pub fn new(module_id: ModuleInstanceId) -> Self {
113        Self { module_id }
114    }
115}
116
117impl Event for ModuleRecoveryStarted {
118    const MODULE: Option<ModuleKind> = None;
119    const KIND: EventKind = EventKind::from_static("module-recovery-started");
120    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
121}
122
123#[derive(Serialize, Deserialize)]
124pub struct ModuleRecoveryCompleted {
125    pub module_id: ModuleInstanceId,
126    /// The kind of the module that finished recovering.
127    ///
128    /// This lets consumers match on the module without cross-referencing
129    /// `module_id` against the federation config. `#[serde(default)]` keeps it
130    /// backwards-compatible with events persisted before it was added, which
131    /// deserialize as `None`.
132    #[serde(default)]
133    pub kind: Option<ModuleKind>,
134    /// The total amount recovered from this module, if the module tracks it.
135    ///
136    /// Modules like the mint know the exact value of the ecash notes they
137    /// reconstruct, while others (e.g. the wallet) only discover which
138    /// on-chain outputs belonged to the client and can't determine the
139    /// amount at recovery-completion time, in which case this is `None`.
140    ///
141    /// `#[serde(default)]` keeps this field backwards-compatible with events
142    /// persisted before it was added, which deserialize as `None`.
143    #[serde(default)]
144    pub amount: Option<Amount>,
145}
146
147impl Event for ModuleRecoveryCompleted {
148    const MODULE: Option<ModuleKind> = None;
149    const KIND: EventKind = EventKind::from_static("module-recovery-completed");
150    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
151}
152
153pub type InstancelessDynClientInput = ClientInput<Box<maybe_add_send_sync!(dyn IInput + 'static)>>;
154
155pub type InstancelessDynClientInputSM =
156    ClientInputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
157
158pub type InstancelessDynClientInputBundle = ClientInputBundle<
159    Box<maybe_add_send_sync!(dyn IInput + 'static)>,
160    Box<maybe_add_send_sync!(dyn IState + 'static)>,
161>;
162
163pub type InstancelessDynClientOutput =
164    ClientOutput<Box<maybe_add_send_sync!(dyn IOutput + 'static)>>;
165
166pub type InstancelessDynClientOutputSM =
167    ClientOutputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
168pub type InstancelessDynClientOutputBundle = ClientOutputBundle<
169    Box<maybe_add_send_sync!(dyn IOutput + 'static)>,
170    Box<maybe_add_send_sync!(dyn IState + 'static)>,
171>;
172
173#[derive(Debug, Error)]
174pub enum AddStateMachinesError {
175    #[error("State already exists in database")]
176    StateAlreadyExists,
177    #[error("Got {0}")]
178    Other(#[from] anyhow::Error),
179}
180
181pub type AddStateMachinesResult = Result<(), AddStateMachinesError>;
182
183#[apply(async_trait_maybe_send!)]
184pub trait IGlobalClientContext: Debug + MaybeSend + MaybeSync + 'static {
185    /// Returned a reference client's module API client, so that module-specific
186    /// calls can be made
187    fn module_api(&self) -> DynModuleApi;
188
189    async fn client_config(&self) -> ClientConfig;
190
191    /// Returns a reference to the client's federation API client. The provided
192    /// interface [`fedimint_api_client::api::IGlobalFederationApi`] typically
193    /// does not provide the necessary functionality, for this extension
194    /// traits like [`fedimint_api_client::api::IGlobalFederationApi`] have
195    /// to be used.
196    // TODO: Could be removed in favor of client() except for testing
197    fn api(&self) -> &DynGlobalApi;
198
199    fn decoders(&self) -> &ModuleDecoderRegistry;
200
201    /// This function is mostly meant for internal use, you are probably looking
202    /// for [`DynGlobalClientContext::claim_inputs`].
203    /// Returns transaction id of the funding transaction and an optional
204    /// `OutPoint` that represents change if change was added.
205    async fn claim_inputs_dyn(
206        &self,
207        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
208        inputs: InstancelessDynClientInputBundle,
209    ) -> anyhow::Result<OutPointRange>;
210
211    /// This function is mostly meant for internal use, you are probably looking
212    /// for [`DynGlobalClientContext::fund_output`].
213    /// Returns transaction id of the funding transaction and an optional
214    /// `OutPoint` that represents change if change was added.
215    async fn fund_output_dyn(
216        &self,
217        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
218        outputs: InstancelessDynClientOutputBundle,
219    ) -> anyhow::Result<OutPointRange>;
220
221    /// Adds a state machine to the executor.
222    async fn add_state_machine_dyn(
223        &self,
224        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
225        sm: Box<maybe_add_send_sync!(dyn IState)>,
226    ) -> AddStateMachinesResult;
227
228    async fn log_event_json(
229        &self,
230        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
231        kind: EventKind,
232        module: Option<(ModuleKind, ModuleInstanceId)>,
233        payload: serde_json::Value,
234        persist: EventPersistence,
235    );
236
237    async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM>;
238
239    /// Returns the core API version that the federation supports
240    async fn core_api_version(&self) -> ApiVersion;
241}
242
243#[apply(async_trait_maybe_send!)]
244impl IGlobalClientContext for () {
245    fn module_api(&self) -> DynModuleApi {
246        unimplemented!("fake implementation, only for tests");
247    }
248
249    async fn client_config(&self) -> ClientConfig {
250        unimplemented!("fake implementation, only for tests");
251    }
252
253    fn api(&self) -> &DynGlobalApi {
254        unimplemented!("fake implementation, only for tests");
255    }
256
257    fn decoders(&self) -> &ModuleDecoderRegistry {
258        unimplemented!("fake implementation, only for tests");
259    }
260
261    async fn claim_inputs_dyn(
262        &self,
263        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
264        _input: InstancelessDynClientInputBundle,
265    ) -> anyhow::Result<OutPointRange> {
266        unimplemented!("fake implementation, only for tests");
267    }
268
269    async fn fund_output_dyn(
270        &self,
271        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
272        _outputs: InstancelessDynClientOutputBundle,
273    ) -> anyhow::Result<OutPointRange> {
274        unimplemented!("fake implementation, only for tests");
275    }
276
277    async fn add_state_machine_dyn(
278        &self,
279        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
280        _sm: Box<maybe_add_send_sync!(dyn IState)>,
281    ) -> AddStateMachinesResult {
282        unimplemented!("fake implementation, only for tests");
283    }
284
285    async fn log_event_json(
286        &self,
287        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
288        _kind: EventKind,
289        _module: Option<(ModuleKind, ModuleInstanceId)>,
290        _payload: serde_json::Value,
291        _persist: EventPersistence,
292    ) {
293        unimplemented!("fake implementation, only for tests");
294    }
295
296    async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM> {
297        unimplemented!("fake implementation, only for tests");
298    }
299
300    async fn core_api_version(&self) -> ApiVersion {
301        unimplemented!("fake implementation, only for tests");
302    }
303}
304
305dyn_newtype_define! {
306    /// Global state and functionality provided to all state machines running in the
307    /// client
308    #[derive(Clone)]
309    pub DynGlobalClientContext(Arc<IGlobalClientContext>)
310}
311
312impl DynGlobalClientContext {
313    pub fn new_fake() -> Self {
314        DynGlobalClientContext::from(())
315    }
316
317    pub async fn await_tx_accepted(&self, query_txid: TransactionId) -> Result<(), String> {
318        self.transaction_update_stream()
319            .await
320            .filter_map(|tx_update| {
321                std::future::ready(match tx_update.state {
322                    TxSubmissionStates::Accepted(txid) if txid == query_txid => Some(Ok(())),
323                    TxSubmissionStates::Rejected(txid, submit_error) if txid == query_txid => {
324                        Some(Err(submit_error))
325                    }
326                    _ => None,
327                })
328            })
329            .next_or_pending()
330            .await
331    }
332
333    pub async fn claim_inputs<I, S>(
334        &self,
335        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
336        inputs: ClientInputBundle<I, S>,
337    ) -> anyhow::Result<OutPointRange>
338    where
339        I: IInput + MaybeSend + MaybeSync + 'static,
340        S: IState + MaybeSend + MaybeSync + 'static,
341    {
342        self.claim_inputs_dyn(dbtx, inputs.into_instanceless())
343            .await
344    }
345
346    /// Creates a transaction with the supplied output and funding added by the
347    /// primary module if possible. If the primary module does not have the
348    /// required funds this function fails.
349    ///
350    /// The transactions submission state machine as well as the state machines
351    /// for the funding inputs are generated automatically. The caller is
352    /// responsible for the output's state machines, should there be any
353    /// required.
354    pub async fn fund_output<O, S>(
355        &self,
356        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
357        outputs: ClientOutputBundle<O, S>,
358    ) -> anyhow::Result<OutPointRange>
359    where
360        O: IOutput + MaybeSend + MaybeSync + 'static,
361        S: IState + MaybeSend + MaybeSync + 'static,
362    {
363        self.fund_output_dyn(dbtx, outputs.into_instanceless())
364            .await
365    }
366
367    /// Allows adding state machines from inside a transition to the executor.
368    /// The added state machine belongs to the same module instance as the state
369    /// machine from inside which it was spawned.
370    pub async fn add_state_machine<S>(
371        &self,
372        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
373        sm: S,
374    ) -> AddStateMachinesResult
375    where
376        S: State + MaybeSend + MaybeSync + 'static,
377    {
378        self.add_state_machine_dyn(dbtx, box_up_state(sm)).await
379    }
380
381    async fn log_event<E>(&self, dbtx: &mut ClientSMDatabaseTransaction<'_, '_>, event: E)
382    where
383        E: Event + Send,
384    {
385        self.log_event_json(
386            dbtx,
387            E::KIND,
388            E::MODULE.map(|m| (m, dbtx.module_id())),
389            serde_json::to_value(&event).expect("Payload serialization can't fail"),
390            <E as Event>::PERSISTENCE,
391        )
392        .await;
393    }
394}
395
396fn states_to_instanceless_dyn<S: IState + MaybeSend + MaybeSync + 'static>(
397    state_gen: StateGenerator<S>,
398) -> StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>> {
399    Arc::new(move |out_point_range| {
400        let states: Vec<S> = state_gen(out_point_range);
401        states
402            .into_iter()
403            .map(|state| box_up_state(state))
404            .collect()
405    })
406}
407
408/// Not sure why I couldn't just directly call `Box::new` ins
409/// [`states_to_instanceless_dyn`], but this fixed it.
410fn box_up_state(state: impl IState + 'static) -> Box<maybe_add_send_sync!(dyn IState + 'static)> {
411    Box::new(state)
412}
413
414impl<T> From<Arc<T>> for DynGlobalClientContext
415where
416    T: IGlobalClientContext,
417{
418    fn from(inner: Arc<T>) -> Self {
419        DynGlobalClientContext { inner }
420    }
421}
422
423fn states_add_instance(
424    module_instance_id: ModuleInstanceId,
425    state_gen: StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>>,
426) -> StateGenerator<DynState> {
427    Arc::new(move |out_point_range| {
428        let states = state_gen(out_point_range);
429        Iterator::collect(
430            states
431                .into_iter()
432                .map(|state| DynState::from_parts(module_instance_id, state)),
433        )
434    })
435}
436
437pub type ModuleGlobalContextGen = ContextGen;
438
439/// Resources particular to a module instance
440pub struct ClientModuleInstance<'m, M: ClientModule> {
441    /// Instance id of the module
442    pub id: ModuleInstanceId,
443    /// Module-specific DB
444    pub db: Database,
445    /// Module-specific API
446    pub api: DynModuleApi,
447
448    pub module: &'m M,
449}
450
451impl<'m, M: ClientModule> ClientModuleInstance<'m, M> {
452    /// Get a reference to the module
453    pub fn inner(&self) -> &'m M {
454        self.module
455    }
456}
457
458impl<M> ops::Deref for ClientModuleInstance<'_, M>
459where
460    M: ClientModule,
461{
462    type Target = M;
463
464    fn deref(&self) -> &Self::Target {
465        self.module
466    }
467}
468#[derive(Deserialize)]
469pub struct GetInviteCodeRequest {
470    pub peer: PeerId,
471}
472
473pub struct TransactionUpdates {
474    pub update_stream: BoxStream<'static, TxSubmissionStatesSM>,
475}
476
477impl TransactionUpdates {
478    /// Waits for the transaction to be accepted or rejected as part of the
479    /// operation to which the `TransactionUpdates` object is subscribed.
480    pub async fn await_tx_accepted(self, await_txid: TransactionId) -> Result<(), String> {
481        debug!(target: LOG_CLIENT, %await_txid, "Await tx accepted");
482        self.update_stream
483            .filter_map(|tx_update| {
484                std::future::ready(match tx_update.state {
485                    TxSubmissionStates::Accepted(txid) if txid == await_txid => Some(Ok(())),
486                    TxSubmissionStates::Rejected(txid, submit_error) if txid == await_txid => {
487                        Some(Err(submit_error))
488                    }
489                    _ => None,
490                })
491            })
492            .next_or_pending()
493            .await?;
494        debug!(target: LOG_CLIENT, %await_txid, "Tx accepted");
495        Ok(())
496    }
497}
498
499/// Admin (guardian) identification and authentication
500pub struct AdminCreds {
501    /// Guardian's own `peer_id`
502    pub peer_id: PeerId,
503    /// Authentication details
504    pub auth: ApiAuth,
505}