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