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 anyhow::bail;
9use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
10use fedimint_bitcoind::DynBitcoindRpc;
11use fedimint_connectors::ConnectorRegistry;
12use fedimint_core::config::FederationId;
13use fedimint_core::core::ModuleKind;
14use fedimint_core::db::{Database, DatabaseVersion};
15use fedimint_core::module::{ApiAuth, ApiVersion, CommonModuleInit, ModuleInit, MultiApiVersion};
16use fedimint_core::task::{MaybeSend, ShuttingDownError, TaskGroup, TaskHandle};
17use fedimint_core::util::SafeUrl;
18use fedimint_core::{Amount, ChainId, NumPeers, apply, async_trait_maybe_send};
19use fedimint_derive_secret::DerivableSecret;
20use fedimint_logging::LOG_CLIENT;
21use tokio::sync::oneshot;
22use tracing::{Span, warn};
23
24use super::ClientContext;
25use super::recovery::RecoveryProgress;
26use crate::db::ClientModuleMigrationFn;
27use crate::module::ClientModule;
28use crate::sm::ModuleNotifier;
29
30/// Factory function type for creating a Bitcoin RPC client from a chain ID.
31///
32/// This allows applications to provide their own Bitcoin RPC client
33/// implementation based on the chain the federation operates on.
34pub type BitcoindRpcFactory = Box<
35    dyn FnOnce(ChainId) -> Pin<Box<dyn Future<Output = Option<DynBitcoindRpc>> + Send>>
36        + Send
37        + Sync,
38>;
39
40/// Factory function type for creating a Bitcoin RPC client from a URL.
41///
42/// This is used when the federation does not have ChainId support yet.
43/// The factory receives a URL (typically from the module config) and can be
44/// called to get an RPC client.
45pub type BitcoindRpcNoChainIdFactory = Arc<
46    dyn Fn(SafeUrl) -> Pin<Box<dyn Future<Output = Option<DynBitcoindRpc>> + Send>> + Send + Sync,
47>;
48
49pub struct ClientModuleInitArgs<C>
50where
51    C: ClientModuleInit,
52{
53    pub federation_id: FederationId,
54    pub peer_num: usize,
55    pub cfg: <<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig,
56    pub db: Database,
57    pub core_api_version: ApiVersion,
58    pub module_api_version: ApiVersion,
59    pub module_root_secret: DerivableSecret,
60    pub notifier: ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States>,
61    pub api: DynGlobalApi,
62    pub admin_auth: Option<ApiAuth>,
63    pub module_api: DynModuleApi,
64    pub context: ClientContext<<C as ClientModuleInit>::Module>,
65    pub task_group: TaskGroup,
66    /// Long-lived span carrying `fed_id`. Use [`Self::spawn_cancellable`] /
67    /// [`Self::spawn`] (or pass [`Self::client_span`] to
68    /// [`TaskGroup::spawn_cancellable_with_span`]) so log events from
69    /// background tasks carry the federation prefix.
70    pub client_span: Span,
71    pub connector_registry: ConnectorRegistry,
72    /// User-provided Bitcoin RPC client
73    ///
74    /// If set by the application using `ClientBuilder::with_bitcoind_rpc`,
75    /// modules (particularly the wallet module) can use this instead of
76    /// creating their own Bitcoin RPC connection.
77    pub user_bitcoind_rpc: Option<DynBitcoindRpc>,
78    /// User-provided Bitcoin RPC factory for when ChainId is not available
79    ///
80    /// If set by the application using
81    /// `ClientBuilder::with_bitcoind_rpc_no_chain_id`, modules can call
82    /// this with a URL from their config to get an RPC client. This is used
83    /// as a fallback when `user_bitcoind_rpc` is None.
84    pub user_bitcoind_rpc_no_chain_id: Option<BitcoindRpcNoChainIdFactory>,
85}
86
87impl<C> ClientModuleInitArgs<C>
88where
89    C: ClientModuleInit,
90{
91    pub fn federation_id(&self) -> &FederationId {
92        &self.federation_id
93    }
94
95    pub fn peer_num(&self) -> usize {
96        self.peer_num
97    }
98
99    pub fn cfg(&self) -> &<<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig {
100        &self.cfg
101    }
102
103    pub fn db(&self) -> &Database {
104        &self.db
105    }
106
107    pub fn core_api_version(&self) -> &ApiVersion {
108        &self.core_api_version
109    }
110
111    pub fn module_api_version(&self) -> &ApiVersion {
112        &self.module_api_version
113    }
114
115    pub fn module_root_secret(&self) -> &DerivableSecret {
116        &self.module_root_secret
117    }
118
119    pub fn notifier(
120        &self,
121    ) -> &ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States> {
122        &self.notifier
123    }
124
125    pub fn api(&self) -> &DynGlobalApi {
126        &self.api
127    }
128
129    pub fn admin_auth(&self) -> Option<&ApiAuth> {
130        self.admin_auth.as_ref()
131    }
132
133    pub fn module_api(&self) -> &DynModuleApi {
134        &self.module_api
135    }
136
137    /// Get the [`ClientContext`] for later use
138    ///
139    /// Notably `ClientContext` can not be used during `ClientModuleInit::init`,
140    /// as the outer context is not yet complete. But it can be stored to be
141    /// used in the methods of [`ClientModule`], at which point it will be
142    /// ready.
143    pub fn context(&self) -> ClientContext<<C as ClientModuleInit>::Module> {
144        self.context.clone()
145    }
146
147    pub fn task_group(&self) -> &TaskGroup {
148        &self.task_group
149    }
150
151    /// Long-lived span identifying this client (with `fed_id`).
152    pub fn client_span(&self) -> &Span {
153        &self.client_span
154    }
155
156    /// Spawn a cancellable task on the client's task group, parented to the
157    /// client's span so all events from the task carry `fed_id` (including
158    /// the lifecycle events emitted by [`TaskGroup`] itself).
159    pub fn spawn_cancellable<R>(
160        &self,
161        name: impl Into<String>,
162        future: impl std::future::Future<Output = R> + MaybeSend + 'static,
163    ) -> oneshot::Receiver<Result<R, ShuttingDownError>>
164    where
165        R: MaybeSend + 'static,
166    {
167        self.task_group
168            .spawn_cancellable_with_span(self.client_span.clone(), name, future)
169    }
170
171    /// Spawn a task on the client's task group, parented to the client's span.
172    pub fn spawn<Fut, R>(
173        &self,
174        name: impl Into<String>,
175        f: impl FnOnce(TaskHandle) -> Fut + MaybeSend + 'static,
176    ) -> oneshot::Receiver<R>
177    where
178        Fut: std::future::Future<Output = R> + MaybeSend + 'static,
179        R: MaybeSend + 'static,
180    {
181        self.task_group
182            .spawn_with_span(self.client_span.clone(), name, f)
183    }
184
185    pub fn connector_registry(&self) -> &ConnectorRegistry {
186        &self.connector_registry
187    }
188
189    /// Returns the user-provided Bitcoin RPC client, if any
190    ///
191    /// Modules (particularly the wallet module) should check this first
192    /// before creating their own Bitcoin RPC connection.
193    pub fn user_bitcoind_rpc(&self) -> Option<&DynBitcoindRpc> {
194        self.user_bitcoind_rpc.as_ref()
195    }
196
197    /// Returns the user-provided Bitcoin RPC factory for when ChainId is not
198    /// available
199    ///
200    /// Modules can call this with a URL from their config to get an RPC client.
201    /// This is used as a fallback when `user_bitcoind_rpc()` returns None.
202    pub fn user_bitcoind_rpc_no_chain_id(&self) -> Option<&BitcoindRpcNoChainIdFactory> {
203        self.user_bitcoind_rpc_no_chain_id.as_ref()
204    }
205}
206
207/// How a module's recovery relates to using the module.
208///
209/// A recovering client runs a recovery for every module that has one, and only
210/// the modules that can be used while that recovery runs join the module
211/// registry; the rest become available once the client is reopened with their
212/// recovery complete. This is a module's declaration of which of those it is,
213/// returned from [`ClientModuleInit::recovery_mode`].
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum RecoveryMode {
216    /// The module implements no recovery.
217    ///
218    /// It is left out of a recovering client's recovery entirely: no recovery
219    /// is started for it and it is initialized as it would be on any other
220    /// client open, rather than being held back for a recovery that would do
221    /// nothing.
222    None,
223    /// The module has a recovery and cannot be used while it runs.
224    ///
225    /// The recovery runs, but the module stays out of the module registry — and
226    /// is therefore unusable — until the client is reopened with the recovery
227    /// complete.
228    Unusable,
229    /// The module has a recovery and can be used while it runs.
230    ///
231    /// [`ClientModuleInit::prepare_recovery`] commits the boundary between the
232    /// recovery and live operation, after which the module is initialized and
233    /// usable straight away, with its recovery running in the background.
234    Usable,
235}
236
237/// Arguments to [`ClientModuleInit::prepare_recovery`], which runs before the
238/// module exists and so gets only what it takes to record where the recovery
239/// ends and live operation begins.
240pub struct ClientModuleRecoveryPrepareArgs {
241    pub db: Database,
242    pub module_api: DynModuleApi,
243}
244
245impl ClientModuleRecoveryPrepareArgs {
246    /// Database isolated for this module instance
247    pub fn db(&self) -> &Database {
248        &self.db
249    }
250
251    /// Api of this module instance
252    pub fn module_api(&self) -> &DynModuleApi {
253        &self.module_api
254    }
255}
256
257pub struct ClientModuleRecoverArgs<C>
258where
259    C: ClientModuleInit,
260{
261    pub federation_id: FederationId,
262    pub num_peers: NumPeers,
263    pub cfg: <<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig,
264    pub db: Database,
265    pub core_api_version: ApiVersion,
266    pub module_api_version: ApiVersion,
267    pub module_root_secret: DerivableSecret,
268    pub notifier: ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States>,
269    pub api: DynGlobalApi,
270    pub admin_auth: Option<ApiAuth>,
271    pub module_api: DynModuleApi,
272    pub context: ClientContext<<C as ClientModuleInit>::Module>,
273    pub progress_tx: tokio::sync::watch::Sender<RecoveryProgress>,
274    pub task_group: TaskGroup,
275    /// See [`ClientModuleInitArgs::client_span`].
276    pub client_span: Span,
277    /// User-provided Bitcoin RPC client
278    ///
279    /// If set by the application using `ClientBuilder::with_bitcoind_rpc`,
280    /// modules (particularly the wallet module) can use this instead of
281    /// creating their own Bitcoin RPC connection.
282    pub user_bitcoind_rpc: Option<DynBitcoindRpc>,
283    /// User-provided Bitcoin RPC factory for when ChainId is not available
284    ///
285    /// If set by the application using
286    /// `ClientBuilder::with_bitcoind_rpc_no_chain_id`, modules can call
287    /// this with a URL from their config to get an RPC client. This is used
288    /// as a fallback when `user_bitcoind_rpc` is None.
289    pub user_bitcoind_rpc_no_chain_id: Option<BitcoindRpcNoChainIdFactory>,
290}
291
292impl<C> ClientModuleRecoverArgs<C>
293where
294    C: ClientModuleInit,
295{
296    pub fn federation_id(&self) -> &FederationId {
297        &self.federation_id
298    }
299
300    pub fn num_peers(&self) -> NumPeers {
301        self.num_peers
302    }
303
304    pub fn cfg(&self) -> &<<C as ModuleInit>::Common as CommonModuleInit>::ClientConfig {
305        &self.cfg
306    }
307
308    pub fn db(&self) -> &Database {
309        &self.db
310    }
311
312    pub fn task_group(&self) -> &TaskGroup {
313        &self.task_group
314    }
315
316    /// Long-lived span identifying this client (with `fed_id`).
317    pub fn client_span(&self) -> &Span {
318        &self.client_span
319    }
320
321    /// Spawn a cancellable task on the client's task group, parented to the
322    /// client's span so all events from the task carry `fed_id`.
323    pub fn spawn_cancellable<R>(
324        &self,
325        name: impl Into<String>,
326        future: impl std::future::Future<Output = R> + MaybeSend + 'static,
327    ) -> oneshot::Receiver<Result<R, ShuttingDownError>>
328    where
329        R: MaybeSend + 'static,
330    {
331        self.task_group
332            .spawn_cancellable_with_span(self.client_span.clone(), name, future)
333    }
334
335    /// Spawn a task on the client's task group, parented to the client's span.
336    pub fn spawn<Fut, R>(
337        &self,
338        name: impl Into<String>,
339        f: impl FnOnce(TaskHandle) -> Fut + MaybeSend + 'static,
340    ) -> oneshot::Receiver<R>
341    where
342        Fut: std::future::Future<Output = R> + MaybeSend + 'static,
343        R: MaybeSend + 'static,
344    {
345        self.task_group
346            .spawn_with_span(self.client_span.clone(), name, f)
347    }
348
349    pub fn core_api_version(&self) -> &ApiVersion {
350        &self.core_api_version
351    }
352
353    pub fn module_api_version(&self) -> &ApiVersion {
354        &self.module_api_version
355    }
356
357    pub fn module_root_secret(&self) -> &DerivableSecret {
358        &self.module_root_secret
359    }
360
361    pub fn notifier(
362        &self,
363    ) -> &ModuleNotifier<<<C as ClientModuleInit>::Module as ClientModule>::States> {
364        &self.notifier
365    }
366
367    pub fn api(&self) -> &DynGlobalApi {
368        &self.api
369    }
370
371    pub fn admin_auth(&self) -> Option<&ApiAuth> {
372        self.admin_auth.as_ref()
373    }
374
375    pub fn module_api(&self) -> &DynModuleApi {
376        &self.module_api
377    }
378
379    /// Get the [`ClientContext`]
380    ///
381    /// Notably `ClientContext`, unlike [`ClientModuleInitArgs::context`],
382    /// the client context is guaranteed to be usable immediately.
383    pub fn context(&self) -> ClientContext<<C as ClientModuleInit>::Module> {
384        self.context.clone()
385    }
386
387    pub fn update_recovery_progress(&self, progress: RecoveryProgress) {
388        // we want a warning if the send channel was not connected to
389        #[allow(clippy::disallowed_methods)]
390        if progress.is_done() {
391            // Recovery is complete when the recovery function finishes. To avoid
392            // confusing any downstream code, we never send completed process.
393            warn!(target: LOG_CLIENT, "Module trying to send a completed recovery progress. Ignoring");
394        } else if progress.is_none() {
395            // Recovery starts with "none" none progress. To avoid
396            // confusing any downstream code, we never send none process afterwards.
397            warn!(target: LOG_CLIENT, "Module trying to send a none recovery progress. Ignoring");
398        } else if self.progress_tx.send(progress).is_err() {
399            warn!(target: LOG_CLIENT, "Module trying to send a recovery progress but nothing is listening");
400        }
401    }
402
403    /// Returns the user-provided Bitcoin RPC client, if any
404    ///
405    /// Modules (particularly the wallet module) should check this first
406    /// before creating their own Bitcoin RPC connection.
407    pub fn user_bitcoind_rpc(&self) -> Option<&DynBitcoindRpc> {
408        self.user_bitcoind_rpc.as_ref()
409    }
410
411    /// Returns the user-provided Bitcoin RPC factory for when ChainId is not
412    /// available
413    ///
414    /// Modules can call this with a URL from their config to get an RPC client.
415    /// This is used as a fallback when `user_bitcoind_rpc()` returns None.
416    pub fn user_bitcoind_rpc_no_chain_id(&self) -> Option<&BitcoindRpcNoChainIdFactory> {
417        self.user_bitcoind_rpc_no_chain_id.as_ref()
418    }
419}
420
421#[apply(async_trait_maybe_send!)]
422pub trait ClientModuleInit: ModuleInit + Sized {
423    type Module: ClientModule;
424
425    /// Api versions of the corresponding server side module's API
426    /// that this client module implementation can use.
427    fn supported_api_versions(&self) -> MultiApiVersion;
428
429    fn kind() -> ModuleKind {
430        <Self::Module as ClientModule>::kind()
431    }
432
433    /// How this module's recovery relates to using the module, see
434    /// [`RecoveryMode`].
435    ///
436    /// Must be overridden together with [`Self::recover`]: leaving this at
437    /// [`RecoveryMode::None`] while implementing a recovery leaves that
438    /// recovery unreachable, and overriding only this makes the module's
439    /// recovery fail.
440    fn recovery_mode(&self) -> RecoveryMode {
441        RecoveryMode::None
442    }
443
444    /// Commit the boundary between a recovery and live operation.
445    ///
446    /// Called on every client open that starts or resumes a recovery, before
447    /// [`Self::init`] and before the module joins the module registry.
448    ///
449    /// [`RecoveryMode::Usable`] is a claim that the recovery and the live
450    /// module cannot interfere: the recovery must not rewrite state the module
451    /// also writes, and must not rediscover what the live module is about to
452    /// do. Whatever separates the two has to be recorded durably *here*,
453    /// because everything after this point can run concurrently with the
454    /// module. If this fails the client fails to open and neither the module
455    /// nor its recovery is started, so [`Self::recover`] may rely on the
456    /// boundary having been committed.
457    ///
458    /// Only called for modules whose [`Self::recovery_mode`] is
459    /// [`RecoveryMode::Usable`].
460    async fn prepare_recovery(
461        &self,
462        _args: &ClientModuleRecoveryPrepareArgs,
463    ) -> anyhow::Result<()> {
464        Ok(())
465    }
466
467    /// Recover the state of the client module, optionally from an existing
468    /// snapshot.
469    ///
470    /// Only called for modules whose [`Self::recovery_mode`] is not
471    /// [`RecoveryMode::None`].
472    ///
473    /// On success, returns the total amount recovered from this module, if the
474    /// module tracks it (`None` for modules that can't determine the amount at
475    /// recovery-completion time). This is surfaced in the
476    /// `ModuleRecoveryCompleted` event.
477    ///
478    /// If `Err` is returned, the higher level client/application might try
479    /// again at a different time (client restarted, code version changed, etc.)
480    async fn recover(
481        &self,
482        _args: &ClientModuleRecoverArgs<Self>,
483        _snapshot: Option<&<Self::Module as ClientModule>::Backup>,
484    ) -> anyhow::Result<Option<Amount>> {
485        bail!(
486            "Module kind {} declares a recovery mode without implementing a recovery",
487            <Self::Module as ClientModule>::kind()
488        )
489    }
490
491    /// Initialize a [`ClientModule`] instance from its config
492    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module>;
493
494    /// Retrieves the database migrations from the module to be applied to the
495    /// database before the module is initialized. The database migrations map
496    /// is indexed on the "from" version.
497    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
498        BTreeMap::new()
499    }
500
501    /// Db prefixes used by the module
502    ///
503    /// If `Some` is returned, it should contain list of database
504    /// prefixes actually used by the module for it's keys.
505    ///
506    /// In (some subset of) non-production tests,
507    /// module database will be scanned for presence of keys
508    /// that do not belong to this list to verify integrity
509    /// of data and possibly catch any unforeseen bugs.
510    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
511        None
512    }
513}