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}