fedimint_client_module/module/
init.rs

1pub mod recovery;
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
6use fedimint_core::config::FederationId;
7use fedimint_core::core::ModuleKind;
8use fedimint_core::db::{Database, DatabaseVersion};
9use fedimint_core::module::{ApiAuth, ApiVersion, CommonModuleInit, ModuleInit, MultiApiVersion};
10use fedimint_core::task::TaskGroup;
11use fedimint_core::{NumPeers, apply, async_trait_maybe_send};
12use fedimint_derive_secret::DerivableSecret;
13use fedimint_logging::LOG_CLIENT;
14use tracing::warn;
15
16use super::ClientContext;
17use super::recovery::RecoveryProgress;
18use crate::db::ClientModuleMigrationFn;
19use crate::module::ClientModule;
20use crate::sm::ModuleNotifier;
21
22pub struct ClientModuleInitArgs<C>
23where
24    C: ClientModuleInit,
25{
26    pub federation_id: FederationId,
27    pub peer_num: usize,
28    pub cfg: <<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig,
29    pub db: Database,
30    pub core_api_version: ApiVersion,
31    pub module_api_version: ApiVersion,
32    pub module_root_secret: DerivableSecret,
33    pub notifier: ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States>,
34    pub api: DynGlobalApi,
35    pub admin_auth: Option<ApiAuth>,
36    pub module_api: DynModuleApi,
37    pub context: ClientContext<<C as ClientModuleInit>::Module>,
38    pub task_group: TaskGroup,
39}
40
41impl<C> ClientModuleInitArgs<C>
42where
43    C: ClientModuleInit,
44{
45    pub fn federation_id(&self) -> &FederationId {
46        &self.federation_id
47    }
48
49    pub fn peer_num(&self) -> usize {
50        self.peer_num
51    }
52
53    pub fn cfg(&self) -> &<<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig {
54        &self.cfg
55    }
56
57    pub fn db(&self) -> &Database {
58        &self.db
59    }
60
61    pub fn core_api_version(&self) -> &ApiVersion {
62        &self.core_api_version
63    }
64
65    pub fn module_api_version(&self) -> &ApiVersion {
66        &self.module_api_version
67    }
68
69    pub fn module_root_secret(&self) -> &DerivableSecret {
70        &self.module_root_secret
71    }
72
73    pub fn notifier(
74        &self,
75    ) -> &ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States> {
76        &self.notifier
77    }
78
79    pub fn api(&self) -> &DynGlobalApi {
80        &self.api
81    }
82
83    pub fn admin_auth(&self) -> Option<&ApiAuth> {
84        self.admin_auth.as_ref()
85    }
86
87    pub fn module_api(&self) -> &DynModuleApi {
88        &self.module_api
89    }
90
91    /// Get the [`ClientContext`] for later use
92    ///
93    /// Notably `ClientContext` can not be used during `ClientModuleInit::init`,
94    /// as the outer context is not yet complete. But it can be stored to be
95    /// used in the methods of [`ClientModule`], at which point it will be
96    /// ready.
97    pub fn context(&self) -> ClientContext<<C as ClientModuleInit>::Module> {
98        self.context.clone()
99    }
100
101    pub fn task_group(&self) -> &TaskGroup {
102        &self.task_group
103    }
104}
105
106pub struct ClientModuleRecoverArgs<C>
107where
108    C: ClientModuleInit,
109{
110    pub federation_id: FederationId,
111    pub num_peers: NumPeers,
112    pub cfg: <<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig,
113    pub db: Database,
114    pub core_api_version: ApiVersion,
115    pub module_api_version: ApiVersion,
116    pub module_root_secret: DerivableSecret,
117    pub notifier: ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States>,
118    pub api: DynGlobalApi,
119    pub admin_auth: Option<ApiAuth>,
120    pub module_api: DynModuleApi,
121    pub context: ClientContext<<C as ClientModuleInit>::Module>,
122    pub progress_tx: tokio::sync::watch::Sender<RecoveryProgress>,
123    pub task_group: TaskGroup,
124}
125
126impl<C> ClientModuleRecoverArgs<C>
127where
128    C: ClientModuleInit,
129{
130    pub fn federation_id(&self) -> &FederationId {
131        &self.federation_id
132    }
133
134    pub fn num_peers(&self) -> NumPeers {
135        self.num_peers
136    }
137
138    pub fn cfg(&self) -> &<<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig {
139        &self.cfg
140    }
141
142    pub fn db(&self) -> &Database {
143        &self.db
144    }
145
146    pub fn task_group(&self) -> &TaskGroup {
147        &self.task_group
148    }
149
150    pub fn core_api_version(&self) -> &ApiVersion {
151        &self.core_api_version
152    }
153
154    pub fn module_api_version(&self) -> &ApiVersion {
155        &self.module_api_version
156    }
157
158    pub fn module_root_secret(&self) -> &DerivableSecret {
159        &self.module_root_secret
160    }
161
162    pub fn notifier(
163        &self,
164    ) -> &ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States> {
165        &self.notifier
166    }
167
168    pub fn api(&self) -> &DynGlobalApi {
169        &self.api
170    }
171
172    pub fn admin_auth(&self) -> Option<&ApiAuth> {
173        self.admin_auth.as_ref()
174    }
175
176    pub fn module_api(&self) -> &DynModuleApi {
177        &self.module_api
178    }
179
180    /// Get the [`ClientContext`]
181    ///
182    /// Notably `ClientContext`, unlike [`ClientModuleInitArgs::context`],
183    /// the client context is guaranteed to be usable immediately.
184    pub fn context(&self) -> ClientContext<<C as ClientModuleInit>::Module> {
185        self.context.clone()
186    }
187
188    pub fn update_recovery_progress(&self, progress: RecoveryProgress) {
189        // we want a warning if the send channel was not connected to
190        #[allow(clippy::disallowed_methods)]
191        if progress.is_done() {
192            // Recovery is complete when the recovery function finishes. To avoid
193            // confusing any downstream code, we never send completed process.
194            warn!(target: LOG_CLIENT, "Module trying to send a completed recovery progress. Ignoring");
195        } else if progress.is_none() {
196            // Recovery starts with "none" none progress. To avoid
197            // confusing any downstream code, we never send none process afterwards.
198            warn!(target: LOG_CLIENT, "Module trying to send a none recovery progress. Ignoring");
199        } else if self.progress_tx.send(progress).is_err() {
200            warn!(target: LOG_CLIENT, "Module trying to send a recovery progress but nothing is listening");
201        }
202    }
203}
204
205#[apply(async_trait_maybe_send!)]
206pub trait ClientModuleInit: ModuleInit + Sized {
207    type Module: ClientModule;
208
209    /// Api versions of the corresponding server side module's API
210    /// that this client module implementation can use.
211    fn supported_api_versions(&self) -> MultiApiVersion;
212
213    fn kind() -> ModuleKind {
214        <Self::Module as ClientModule>::kind()
215    }
216
217    /// Recover the state of the client module, optionally from an existing
218    /// snapshot.
219    ///
220    /// If `Err` is returned, the higher level client/application might try
221    /// again at a different time (client restarted, code version changed, etc.)
222    async fn recover(
223        &self,
224        _args: &ClientModuleRecoverArgs<Self>,
225        _snapshot: Option<&<Self::Module as ClientModule>::Backup>,
226    ) -> anyhow::Result<()> {
227        warn!(
228            target: LOG_CLIENT,
229            kind = %<Self::Module as ClientModule>::kind(),
230            "Module does not support recovery, completing without doing anything"
231        );
232        Ok(())
233    }
234
235    /// Initialize a [`ClientModule`] instance from its config
236    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module>;
237
238    /// Retrieves the database migrations from the module to be applied to the
239    /// database before the module is initialized. The database migrations map
240    /// is indexed on the "from" version.
241    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
242        BTreeMap::new()
243    }
244
245    /// Db prefixes used by the module
246    ///
247    /// If `Some` is returned, it should contain list of database
248    /// prefixes actually used by the module for it's keys.
249    ///
250    /// In (some subset of) non-production tests,
251    /// module database will be scanned for presence of keys
252    /// that do not belong to this list to verify integrity
253    /// of data and possibly catch any unforeseen bugs.
254    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
255        None
256    }
257}