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 tracing::debug;
39use transaction::{
40    ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputSM, TxSubmissionStatesSM,
41};
42
43pub use crate::error::{
44    AddStateMachinesError, ApiVersionDiscoveryError, ClientModuleError, MetaFetchError,
45    ModuleLookupError, OperationAlreadyExistsError, OperationLookupError, OperationNotFoundError,
46    TransactionSubmitError,
47};
48pub use crate::module::{ClientModule, StateGenerator};
49use crate::sm::executor::ContextGen;
50use crate::sm::{ClientSMDatabaseTransaction, DynState, IState, State};
51use crate::transaction::{ClientInput, ClientOutputBundle, TxSubmissionStates};
52
53pub mod api;
54
55pub mod db;
56
57/// Error types shared between the client and its modules
58pub mod error;
59
60pub mod backup;
61/// Environment variables
62pub mod envs;
63pub mod meta;
64/// Module client interface definitions
65pub mod module;
66/// Operation log subsystem of the client
67pub mod oplog;
68/// Secret handling & derivation
69pub mod secret;
70/// Client state machine interfaces and executor implementation
71pub mod sm;
72/// Structs and interfaces to construct Fedimint transactions
73pub mod transaction;
74
75pub mod api_version_discovery;
76
77#[derive(Serialize, Deserialize)]
78pub struct TxCreatedEvent {
79    pub txid: TransactionId,
80    pub operation_id: OperationId,
81}
82
83impl Event for TxCreatedEvent {
84    const MODULE: Option<ModuleKind> = None;
85    const KIND: EventKind = EventKind::from_static("tx-created");
86    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
87}
88
89#[derive(Serialize, Deserialize)]
90pub struct TxAcceptedEvent {
91    txid: TransactionId,
92    operation_id: OperationId,
93}
94
95impl Event for TxAcceptedEvent {
96    const MODULE: Option<ModuleKind> = None;
97    const KIND: EventKind = EventKind::from_static("tx-accepted");
98    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
99}
100
101#[derive(Serialize, Deserialize)]
102pub struct TxRejectedEvent {
103    txid: TransactionId,
104    error: String,
105    operation_id: OperationId,
106}
107impl Event for TxRejectedEvent {
108    const MODULE: Option<ModuleKind> = None;
109    const KIND: EventKind = EventKind::from_static("tx-rejected");
110    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
111}
112
113/// Diagnostic signal that a transaction submission is being re-attempted
114/// indefinitely without reaching consensus.
115///
116/// Emitted only on the retry iterations where `submit_transaction` returns a
117/// decodable, non-rejection outcome — i.e. a peer accepted the submission but
118/// the transaction has still not been accepted into (or rejected from)
119/// consensus. Repeated transport, API, or decoding errors instead take the
120/// retry error path and do not emit this event, so its absence does not imply
121/// the submission has stopped.
122///
123/// Cadence: first emitted after roughly 30 minutes of continuous re-submission
124/// within a single client run, then repeated about every 30 minutes while the
125/// condition persists. Elapsed time is measured per client run, so a restart
126/// resets it.
127///
128/// It is [`EventPersistence::Transient`], so it is broadcast to live
129/// subscribers (and transits the unordered event staging table) but is never
130/// written to the ordered event log.
131#[derive(Serialize, Deserialize)]
132pub struct TxSubmissionStalledEvent {
133    pub txid: TransactionId,
134    pub operation_id: OperationId,
135    /// Submission attempts so far in this client run.
136    pub attempt: u64,
137    /// Seconds elapsed in this client run since submission started.
138    pub elapsed_s: u64,
139}
140
141impl Event for TxSubmissionStalledEvent {
142    const MODULE: Option<ModuleKind> = None;
143    const KIND: EventKind = EventKind::from_static("tx-submission-stalled");
144    const PERSISTENCE: EventPersistence = EventPersistence::Transient;
145}
146
147#[derive(Serialize, Deserialize)]
148pub struct ModuleRecoveryStarted {
149    module_id: ModuleInstanceId,
150}
151
152impl ModuleRecoveryStarted {
153    pub fn new(module_id: ModuleInstanceId) -> Self {
154        Self { module_id }
155    }
156}
157
158impl Event for ModuleRecoveryStarted {
159    const MODULE: Option<ModuleKind> = None;
160    const KIND: EventKind = EventKind::from_static("module-recovery-started");
161    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
162}
163
164#[derive(Serialize, Deserialize)]
165pub struct ModuleRecoveryCompleted {
166    pub module_id: ModuleInstanceId,
167    /// The kind of the module that finished recovering.
168    ///
169    /// This lets consumers match on the module without cross-referencing
170    /// `module_id` against the federation config. `#[serde(default)]` keeps it
171    /// backwards-compatible with events persisted before it was added, which
172    /// deserialize as `None`.
173    #[serde(default)]
174    pub kind: Option<ModuleKind>,
175    /// The total amount recovered from this module, if the module tracks it.
176    ///
177    /// Modules like the mint know the exact value of the ecash notes they
178    /// reconstruct, while others (e.g. the wallet) only discover which
179    /// on-chain outputs belonged to the client and can't determine the
180    /// amount at recovery-completion time, in which case this is `None`.
181    ///
182    /// `#[serde(default)]` keeps this field backwards-compatible with events
183    /// persisted before it was added, which deserialize as `None`.
184    #[serde(default)]
185    pub amount: Option<Amount>,
186}
187
188impl Event for ModuleRecoveryCompleted {
189    const MODULE: Option<ModuleKind> = None;
190    const KIND: EventKind = EventKind::from_static("module-recovery-completed");
191    const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
192}
193
194pub type InstancelessDynClientInput = ClientInput<Box<maybe_add_send_sync!(dyn IInput + 'static)>>;
195
196pub type InstancelessDynClientInputSM =
197    ClientInputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
198
199pub type InstancelessDynClientInputBundle = ClientInputBundle<
200    Box<maybe_add_send_sync!(dyn IInput + 'static)>,
201    Box<maybe_add_send_sync!(dyn IState + 'static)>,
202>;
203
204pub type InstancelessDynClientOutput =
205    ClientOutput<Box<maybe_add_send_sync!(dyn IOutput + 'static)>>;
206
207pub type InstancelessDynClientOutputSM =
208    ClientOutputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
209pub type InstancelessDynClientOutputBundle = ClientOutputBundle<
210    Box<maybe_add_send_sync!(dyn IOutput + 'static)>,
211    Box<maybe_add_send_sync!(dyn IState + 'static)>,
212>;
213
214pub type AddStateMachinesResult = Result<(), AddStateMachinesError>;
215
216#[apply(async_trait_maybe_send!)]
217pub trait IGlobalClientContext: Debug + MaybeSend + MaybeSync + 'static {
218    /// Returned a reference client's module API client, so that module-specific
219    /// calls can be made
220    fn module_api(&self) -> DynModuleApi;
221
222    async fn client_config(&self) -> ClientConfig;
223
224    /// Returns a reference to the client's federation API client. The provided
225    /// interface [`fedimint_api_client::api::IGlobalFederationApi`] typically
226    /// does not provide the necessary functionality, for this extension
227    /// traits like [`fedimint_api_client::api::IGlobalFederationApi`] have
228    /// to be used.
229    // TODO: Could be removed in favor of client() except for testing
230    fn api(&self) -> &DynGlobalApi;
231
232    fn decoders(&self) -> &ModuleDecoderRegistry;
233
234    /// This function is mostly meant for internal use, you are probably looking
235    /// for [`DynGlobalClientContext::claim_inputs`].
236    /// Returns transaction id of the funding transaction and an optional
237    /// `OutPoint` that represents change if change was added.
238    async fn claim_inputs_dyn(
239        &self,
240        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
241        inputs: InstancelessDynClientInputBundle,
242    ) -> Result<OutPointRange, TransactionSubmitError>;
243
244    /// This function is mostly meant for internal use, you are probably looking
245    /// for [`DynGlobalClientContext::fund_output`].
246    /// Returns transaction id of the funding transaction and an optional
247    /// `OutPoint` that represents change if change was added.
248    async fn fund_output_dyn(
249        &self,
250        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
251        outputs: InstancelessDynClientOutputBundle,
252    ) -> Result<OutPointRange, TransactionSubmitError>;
253
254    /// Adds a state machine to the executor.
255    async fn add_state_machine_dyn(
256        &self,
257        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
258        sm: Box<maybe_add_send_sync!(dyn IState)>,
259    ) -> AddStateMachinesResult;
260
261    async fn log_event_json(
262        &self,
263        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
264        kind: EventKind,
265        module: Option<(ModuleKind, ModuleInstanceId)>,
266        payload: serde_json::Value,
267        persist: EventPersistence,
268    );
269
270    /// Like [`Self::log_event_json`], but opens its own database transaction
271    /// instead of borrowing a [`ClientSMDatabaseTransaction`]. Meant for call
272    /// sites that run outside a state transition (e.g. the submission retry
273    /// loop) and therefore have no dbtx in scope. The implementation pairs the
274    /// event's `module` kind with its own module instance id, mirroring what
275    /// `log_event_json` derives from the transaction's `module_id()`.
276    async fn log_event_json_no_dbtx(
277        &self,
278        kind: EventKind,
279        module_kind: Option<ModuleKind>,
280        payload: serde_json::Value,
281        persist: EventPersistence,
282    );
283
284    async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM>;
285
286    /// Returns the core API version that the federation supports
287    async fn core_api_version(&self) -> ApiVersion;
288}
289
290#[apply(async_trait_maybe_send!)]
291impl IGlobalClientContext for () {
292    fn module_api(&self) -> DynModuleApi {
293        unimplemented!("fake implementation, only for tests");
294    }
295
296    async fn client_config(&self) -> ClientConfig {
297        unimplemented!("fake implementation, only for tests");
298    }
299
300    fn api(&self) -> &DynGlobalApi {
301        unimplemented!("fake implementation, only for tests");
302    }
303
304    fn decoders(&self) -> &ModuleDecoderRegistry {
305        unimplemented!("fake implementation, only for tests");
306    }
307
308    async fn claim_inputs_dyn(
309        &self,
310        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
311        _input: InstancelessDynClientInputBundle,
312    ) -> Result<OutPointRange, TransactionSubmitError> {
313        unimplemented!("fake implementation, only for tests");
314    }
315
316    async fn fund_output_dyn(
317        &self,
318        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
319        _outputs: InstancelessDynClientOutputBundle,
320    ) -> Result<OutPointRange, TransactionSubmitError> {
321        unimplemented!("fake implementation, only for tests");
322    }
323
324    async fn add_state_machine_dyn(
325        &self,
326        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
327        _sm: Box<maybe_add_send_sync!(dyn IState)>,
328    ) -> AddStateMachinesResult {
329        unimplemented!("fake implementation, only for tests");
330    }
331
332    async fn log_event_json(
333        &self,
334        _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
335        _kind: EventKind,
336        _module: Option<(ModuleKind, ModuleInstanceId)>,
337        _payload: serde_json::Value,
338        _persist: EventPersistence,
339    ) {
340        unimplemented!("fake implementation, only for tests");
341    }
342
343    async fn log_event_json_no_dbtx(
344        &self,
345        _kind: EventKind,
346        _module_kind: Option<ModuleKind>,
347        _payload: serde_json::Value,
348        _persist: EventPersistence,
349    ) {
350        unimplemented!("fake implementation, only for tests");
351    }
352
353    async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM> {
354        unimplemented!("fake implementation, only for tests");
355    }
356
357    async fn core_api_version(&self) -> ApiVersion {
358        unimplemented!("fake implementation, only for tests");
359    }
360}
361
362dyn_newtype_define! {
363    /// Global state and functionality provided to all state machines running in the
364    /// client
365    #[derive(Clone)]
366    pub DynGlobalClientContext(Arc<IGlobalClientContext>)
367}
368
369impl DynGlobalClientContext {
370    pub fn new_fake() -> Self {
371        DynGlobalClientContext::from(())
372    }
373
374    pub async fn await_tx_accepted(&self, query_txid: TransactionId) -> Result<(), String> {
375        self.transaction_update_stream()
376            .await
377            .filter_map(|tx_update| {
378                std::future::ready(match tx_update.state {
379                    TxSubmissionStates::Accepted(txid) if txid == query_txid => Some(Ok(())),
380                    TxSubmissionStates::Rejected(txid, submit_error) if txid == query_txid => {
381                        Some(Err(submit_error))
382                    }
383                    _ => None,
384                })
385            })
386            .next_or_pending()
387            .await
388    }
389
390    pub async fn claim_inputs<I, S>(
391        &self,
392        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
393        inputs: ClientInputBundle<I, S>,
394    ) -> Result<OutPointRange, TransactionSubmitError>
395    where
396        I: IInput + MaybeSend + MaybeSync + 'static,
397        S: IState + MaybeSend + MaybeSync + 'static,
398    {
399        self.claim_inputs_dyn(dbtx, inputs.into_instanceless())
400            .await
401    }
402
403    /// Creates a transaction with the supplied output and funding added by the
404    /// primary module if possible. If the primary module does not have the
405    /// required funds this function fails.
406    ///
407    /// The transactions submission state machine as well as the state machines
408    /// for the funding inputs are generated automatically. The caller is
409    /// responsible for the output's state machines, should there be any
410    /// required.
411    pub async fn fund_output<O, S>(
412        &self,
413        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
414        outputs: ClientOutputBundle<O, S>,
415    ) -> Result<OutPointRange, TransactionSubmitError>
416    where
417        O: IOutput + MaybeSend + MaybeSync + 'static,
418        S: IState + MaybeSend + MaybeSync + 'static,
419    {
420        self.fund_output_dyn(dbtx, outputs.into_instanceless())
421            .await
422    }
423
424    /// Allows adding state machines from inside a transition to the executor.
425    /// The added state machine belongs to the same module instance as the state
426    /// machine from inside which it was spawned.
427    pub async fn add_state_machine<S>(
428        &self,
429        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
430        sm: S,
431    ) -> AddStateMachinesResult
432    where
433        S: State + MaybeSend + MaybeSync + 'static,
434    {
435        self.add_state_machine_dyn(dbtx, box_up_state(sm)).await
436    }
437
438    async fn log_event<E>(&self, dbtx: &mut ClientSMDatabaseTransaction<'_, '_>, event: E)
439    where
440        E: Event + Send,
441    {
442        self.log_event_json(
443            dbtx,
444            E::KIND,
445            E::MODULE.map(|m| (m, dbtx.module_id())),
446            serde_json::to_value(&event).expect("Payload serialization can't fail"),
447            <E as Event>::PERSISTENCE,
448        )
449        .await;
450    }
451
452    /// Log an event from outside a state transition, where no
453    /// [`ClientSMDatabaseTransaction`] is in scope. Opens its own database
454    /// transaction. Prefer [`Self::log_event`] whenever a dbtx is available so
455    /// the event commits atomically with the surrounding transition.
456    async fn log_event_no_dbtx<E>(&self, event: E)
457    where
458        E: Event + Send,
459    {
460        self.log_event_json_no_dbtx(
461            E::KIND,
462            E::MODULE,
463            serde_json::to_value(&event).expect("Payload serialization can't fail"),
464            <E as Event>::PERSISTENCE,
465        )
466        .await;
467    }
468}
469
470fn states_to_instanceless_dyn<S: IState + MaybeSend + MaybeSync + 'static>(
471    state_gen: StateGenerator<S>,
472) -> StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>> {
473    Arc::new(move |out_point_range| {
474        let states: Vec<S> = state_gen(out_point_range);
475        states
476            .into_iter()
477            .map(|state| box_up_state(state))
478            .collect()
479    })
480}
481
482/// Not sure why I couldn't just directly call `Box::new` ins
483/// [`states_to_instanceless_dyn`], but this fixed it.
484fn box_up_state(state: impl IState + 'static) -> Box<maybe_add_send_sync!(dyn IState + 'static)> {
485    Box::new(state)
486}
487
488impl<T> From<Arc<T>> for DynGlobalClientContext
489where
490    T: IGlobalClientContext,
491{
492    fn from(inner: Arc<T>) -> Self {
493        DynGlobalClientContext { inner }
494    }
495}
496
497fn states_add_instance(
498    module_instance_id: ModuleInstanceId,
499    state_gen: StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>>,
500) -> StateGenerator<DynState> {
501    Arc::new(move |out_point_range| {
502        let states = state_gen(out_point_range);
503        Iterator::collect(
504            states
505                .into_iter()
506                .map(|state| DynState::from_parts(module_instance_id, state)),
507        )
508    })
509}
510
511pub type ModuleGlobalContextGen = ContextGen;
512
513/// Resources particular to a module instance
514pub struct ClientModuleInstance<'m, M: ClientModule> {
515    /// Instance id of the module
516    pub id: ModuleInstanceId,
517    /// Module-specific DB
518    pub db: Database,
519    /// Module-specific API
520    pub api: DynModuleApi,
521
522    pub module: &'m M,
523}
524
525impl<'m, M: ClientModule> ClientModuleInstance<'m, M> {
526    /// Get a reference to the module
527    pub fn inner(&self) -> &'m M {
528        self.module
529    }
530}
531
532impl<M> ops::Deref for ClientModuleInstance<'_, M>
533where
534    M: ClientModule,
535{
536    type Target = M;
537
538    fn deref(&self) -> &Self::Target {
539        self.module
540    }
541}
542#[derive(Deserialize)]
543pub struct GetInviteCodeRequest {
544    pub peer: PeerId,
545}
546
547pub struct TransactionUpdates {
548    pub update_stream: BoxStream<'static, TxSubmissionStatesSM>,
549}
550
551impl TransactionUpdates {
552    /// Waits for the transaction to be accepted or rejected as part of the
553    /// operation to which the `TransactionUpdates` object is subscribed.
554    pub async fn await_tx_accepted(self, await_txid: TransactionId) -> Result<(), String> {
555        debug!(target: LOG_CLIENT, %await_txid, "Await tx accepted");
556        self.update_stream
557            .filter_map(|tx_update| {
558                std::future::ready(match tx_update.state {
559                    TxSubmissionStates::Accepted(txid) if txid == await_txid => Some(Ok(())),
560                    TxSubmissionStates::Rejected(txid, submit_error) if txid == await_txid => {
561                        Some(Err(submit_error))
562                    }
563                    _ => None,
564                })
565            })
566            .next_or_pending()
567            .await?;
568        debug!(target: LOG_CLIENT, %await_txid, "Tx accepted");
569        Ok(())
570    }
571}
572
573/// Admin (guardian) identification and authentication
574pub struct AdminCreds {
575    /// Guardian's own `peer_id`
576    pub peer_id: PeerId,
577    /// Authentication details
578    pub auth: ApiAuth,
579}