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