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