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