Skip to main content

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