fedimint_client_module/module/
init.rs

1pub mod recovery;
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
9use fedimint_bitcoind::DynBitcoindRpc;
10use fedimint_connectors::ConnectorRegistry;
11use fedimint_core::config::FederationId;
12use fedimint_core::core::ModuleKind;
13use fedimint_core::db::{Database, DatabaseVersion};
14use fedimint_core::module::{ApiAuth, ApiVersion, CommonModuleInit, ModuleInit, MultiApiVersion};
15use fedimint_core::task::TaskGroup;
16use fedimint_core::util::SafeUrl;
17use fedimint_core::{ChainId, NumPeers, apply, async_trait_maybe_send};
18use fedimint_derive_secret::DerivableSecret;
19use fedimint_logging::LOG_CLIENT;
20use tracing::warn;
21
22use super::ClientContext;
23use super::recovery::RecoveryProgress;
24use crate::db::ClientModuleMigrationFn;
25use crate::module::ClientModule;
26use crate::sm::ModuleNotifier;
27
28/// Factory function type for creating a Bitcoin RPC client from a chain ID.
29///
30/// This allows applications to provide their own Bitcoin RPC client
31/// implementation based on the chain the federation operates on.
32pub type BitcoindRpcFactory = Box<
33    dyn FnOnce(ChainId) -> Pin<Box<dyn Future<Output = Option<DynBitcoindRpc>> + Send>>
34        + Send
35        + Sync,
36>;
37
38/// Factory function type for creating a Bitcoin RPC client from a URL.
39///
40/// This is used when the federation does not have ChainId support yet.
41/// The factory receives a URL (typically from the module config) and can be
42/// called to get an RPC client.
43pub type BitcoindRpcNoChainIdFactory = Arc<
44    dyn Fn(SafeUrl) -> Pin<Box<dyn Future<Output = Option<DynBitcoindRpc>> + Send>> + Send + Sync,
45>;
46
47pub struct ClientModuleInitArgs<C>
48where
49    C: ClientModuleInit,
50{
51    pub federation_id: FederationId,
52    pub peer_num: usize,
53    pub cfg: <<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig,
54    pub db: Database,
55    pub core_api_version: ApiVersion,
56    pub module_api_version: ApiVersion,
57    pub module_root_secret: DerivableSecret,
58    pub notifier: ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States>,
59    pub api: DynGlobalApi,
60    pub admin_auth: Option<ApiAuth>,
61    pub module_api: DynModuleApi,
62    pub context: ClientContext<<C as ClientModuleInit>::Module>,
63    pub task_group: TaskGroup,
64    pub connector_registry: ConnectorRegistry,
65    /// User-provided Bitcoin RPC client
66    ///
67    /// If set by the application using `ClientBuilder::with_bitcoind_rpc`,
68    /// modules (particularly the wallet module) can use this instead of
69    /// creating their own Bitcoin RPC connection.
70    pub user_bitcoind_rpc: Option<DynBitcoindRpc>,
71    /// User-provided Bitcoin RPC factory for when ChainId is not available
72    ///
73    /// If set by the application using
74    /// `ClientBuilder::with_bitcoind_rpc_no_chain_id`, modules can call
75    /// this with a URL from their config to get an RPC client. This is used
76    /// as a fallback when `user_bitcoind_rpc` is None.
77    pub user_bitcoind_rpc_no_chain_id: Option<BitcoindRpcNoChainIdFactory>,
78}
79
80impl<C> ClientModuleInitArgs<C>
81where
82    C: ClientModuleInit,
83{
84    pub fn federation_id(&self) -> &FederationId {
85        &self.federation_id
86    }
87
88    pub fn peer_num(&self) -> usize {
89        self.peer_num
90    }
91
92    pub fn cfg(&self) -> &<<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig {
93        &self.cfg
94    }
95
96    pub fn db(&self) -> &Database {
97        &self.db
98    }
99
100    pub fn core_api_version(&self) -> &ApiVersion {
101        &self.core_api_version
102    }
103
104    pub fn module_api_version(&self) -> &ApiVersion {
105        &self.module_api_version
106    }
107
108    pub fn module_root_secret(&self) -> &DerivableSecret {
109        &self.module_root_secret
110    }
111
112    pub fn notifier(
113        &self,
114    ) -> &ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States> {
115        &self.notifier
116    }
117
118    pub fn api(&self) -> &DynGlobalApi {
119        &self.api
120    }
121
122    pub fn admin_auth(&self) -> Option<&ApiAuth> {
123        self.admin_auth.as_ref()
124    }
125
126    pub fn module_api(&self) -> &DynModuleApi {
127        &self.module_api
128    }
129
130    /// Get the [`ClientContext`] for later use
131    ///
132    /// Notably `ClientContext` can not be used during `ClientModuleInit::init`,
133    /// as the outer context is not yet complete. But it can be stored to be
134    /// used in the methods of [`ClientModule`], at which point it will be
135    /// ready.
136    pub fn context(&self) -> ClientContext<<C as ClientModuleInit>::Module> {
137        self.context.clone()
138    }
139
140    pub fn task_group(&self) -> &TaskGroup {
141        &self.task_group
142    }
143
144    pub fn connector_registry(&self) -> &ConnectorRegistry {
145        &self.connector_registry
146    }
147
148    /// Returns the user-provided Bitcoin RPC client, if any
149    ///
150    /// Modules (particularly the wallet module) should check this first
151    /// before creating their own Bitcoin RPC connection.
152    pub fn user_bitcoind_rpc(&self) -> Option<&DynBitcoindRpc> {
153        self.user_bitcoind_rpc.as_ref()
154    }
155
156    /// Returns the user-provided Bitcoin RPC factory for when ChainId is not
157    /// available
158    ///
159    /// Modules can call this with a URL from their config to get an RPC client.
160    /// This is used as a fallback when `user_bitcoind_rpc()` returns None.
161    pub fn user_bitcoind_rpc_no_chain_id(&self) -> Option<&BitcoindRpcNoChainIdFactory> {
162        self.user_bitcoind_rpc_no_chain_id.as_ref()
163    }
164}
165
166pub struct ClientModuleRecoverArgs<C>
167where
168    C: ClientModuleInit,
169{
170    pub federation_id: FederationId,
171    pub num_peers: NumPeers,
172    pub cfg: <<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig,
173    pub db: Database,
174    pub core_api_version: ApiVersion,
175    pub module_api_version: ApiVersion,
176    pub module_root_secret: DerivableSecret,
177    pub notifier: ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States>,
178    pub api: DynGlobalApi,
179    pub admin_auth: Option<ApiAuth>,
180    pub module_api: DynModuleApi,
181    pub context: ClientContext<<C as ClientModuleInit>::Module>,
182    pub progress_tx: tokio::sync::watch::Sender<RecoveryProgress>,
183    pub task_group: TaskGroup,
184    /// User-provided Bitcoin RPC client
185    ///
186    /// If set by the application using `ClientBuilder::with_bitcoind_rpc`,
187    /// modules (particularly the wallet module) can use this instead of
188    /// creating their own Bitcoin RPC connection.
189    pub user_bitcoind_rpc: Option<DynBitcoindRpc>,
190    /// User-provided Bitcoin RPC factory for when ChainId is not available
191    ///
192    /// If set by the application using
193    /// `ClientBuilder::with_bitcoind_rpc_no_chain_id`, modules can call
194    /// this with a URL from their config to get an RPC client. This is used
195    /// as a fallback when `user_bitcoind_rpc` is None.
196    pub user_bitcoind_rpc_no_chain_id: Option<BitcoindRpcNoChainIdFactory>,
197}
198
199impl<C> ClientModuleRecoverArgs<C>
200where
201    C: ClientModuleInit,
202{
203    pub fn federation_id(&self) -> &FederationId {
204        &self.federation_id
205    }
206
207    pub fn num_peers(&self) -> NumPeers {
208        self.num_peers
209    }
210
211    pub fn cfg(&self) -> &<<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig {
212        &self.cfg
213    }
214
215    pub fn db(&self) -> &Database {
216        &self.db
217    }
218
219    pub fn task_group(&self) -> &TaskGroup {
220        &self.task_group
221    }
222
223    pub fn core_api_version(&self) -> &ApiVersion {
224        &self.core_api_version
225    }
226
227    pub fn module_api_version(&self) -> &ApiVersion {
228        &self.module_api_version
229    }
230
231    pub fn module_root_secret(&self) -> &DerivableSecret {
232        &self.module_root_secret
233    }
234
235    pub fn notifier(
236        &self,
237    ) -> &ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States> {
238        &self.notifier
239    }
240
241    pub fn api(&self) -> &DynGlobalApi {
242        &self.api
243    }
244
245    pub fn admin_auth(&self) -> Option<&ApiAuth> {
246        self.admin_auth.as_ref()
247    }
248
249    pub fn module_api(&self) -> &DynModuleApi {
250        &self.module_api
251    }
252
253    /// Get the [`ClientContext`]
254    ///
255    /// Notably `ClientContext`, unlike [`ClientModuleInitArgs::context`],
256    /// the client context is guaranteed to be usable immediately.
257    pub fn context(&self) -> ClientContext<<C as ClientModuleInit>::Module> {
258        self.context.clone()
259    }
260
261    pub fn update_recovery_progress(&self, progress: RecoveryProgress) {
262        // we want a warning if the send channel was not connected to
263        #[allow(clippy::disallowed_methods)]
264        if progress.is_done() {
265            // Recovery is complete when the recovery function finishes. To avoid
266            // confusing any downstream code, we never send completed process.
267            warn!(target: LOG_CLIENT, "Module trying to send a completed recovery progress. Ignoring");
268        } else if progress.is_none() {
269            // Recovery starts with "none" none progress. To avoid
270            // confusing any downstream code, we never send none process afterwards.
271            warn!(target: LOG_CLIENT, "Module trying to send a none recovery progress. Ignoring");
272        } else if self.progress_tx.send(progress).is_err() {
273            warn!(target: LOG_CLIENT, "Module trying to send a recovery progress but nothing is listening");
274        }
275    }
276
277    /// Returns the user-provided Bitcoin RPC client, if any
278    ///
279    /// Modules (particularly the wallet module) should check this first
280    /// before creating their own Bitcoin RPC connection.
281    pub fn user_bitcoind_rpc(&self) -> Option<&DynBitcoindRpc> {
282        self.user_bitcoind_rpc.as_ref()
283    }
284
285    /// Returns the user-provided Bitcoin RPC factory for when ChainId is not
286    /// available
287    ///
288    /// Modules can call this with a URL from their config to get an RPC client.
289    /// This is used as a fallback when `user_bitcoind_rpc()` returns None.
290    pub fn user_bitcoind_rpc_no_chain_id(&self) -> Option<&BitcoindRpcNoChainIdFactory> {
291        self.user_bitcoind_rpc_no_chain_id.as_ref()
292    }
293}
294
295#[apply(async_trait_maybe_send!)]
296pub trait ClientModuleInit: ModuleInit + Sized {
297    type Module: ClientModule;
298
299    /// Api versions of the corresponding server side module's API
300    /// that this client module implementation can use.
301    fn supported_api_versions(&self) -> MultiApiVersion;
302
303    fn kind() -> ModuleKind {
304        <Self::Module as ClientModule>::kind()
305    }
306
307    /// Recover the state of the client module, optionally from an existing
308    /// snapshot.
309    ///
310    /// If `Err` is returned, the higher level client/application might try
311    /// again at a different time (client restarted, code version changed, etc.)
312    async fn recover(
313        &self,
314        _args: &ClientModuleRecoverArgs<Self>,
315        _snapshot: Option<&<Self::Module as ClientModule>::Backup>,
316    ) -> anyhow::Result<()> {
317        warn!(
318            target: LOG_CLIENT,
319            kind = %<Self::Module as ClientModule>::kind(),
320            "Module does not support recovery, completing without doing anything"
321        );
322        Ok(())
323    }
324
325    /// Initialize a [`ClientModule`] instance from its config
326    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module>;
327
328    /// Retrieves the database migrations from the module to be applied to the
329    /// database before the module is initialized. The database migrations map
330    /// is indexed on the "from" version.
331    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
332        BTreeMap::new()
333    }
334
335    /// Db prefixes used by the module
336    ///
337    /// If `Some` is returned, it should contain list of database
338    /// prefixes actually used by the module for it's keys.
339    ///
340    /// In (some subset of) non-production tests,
341    /// module database will be scanned for presence of keys
342    /// that do not belong to this list to verify integrity
343    /// of data and possibly catch any unforeseen bugs.
344    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
345        None
346    }
347}