Skip to main content

fedimint_client/
client.rs

1use std::collections::{BTreeMap, HashSet};
2use std::fmt::{self, Formatter};
3use std::future::{Future, pending};
4use std::ops::Range;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9use anyhow::{Context as _, anyhow, bail, format_err};
10use async_stream::try_stream;
11use bitcoin::key::Secp256k1;
12use bitcoin::key::rand::thread_rng;
13use bitcoin::secp256k1::{self, PublicKey};
14use fedimint_api_client::api::global_api::with_request_hook::ApiRequestHook;
15use fedimint_api_client::api::{
16    ApiVersionSet, DynGlobalApi, FederationApiExt as _, FederationResult, IGlobalFederationApi,
17};
18use fedimint_bitcoind::DynBitcoindRpc;
19use fedimint_client_module::module::recovery::RecoveryProgress;
20use fedimint_client_module::module::{
21    ClientContextIface, ClientModule, ClientModuleRegistry, DynClientModule, FinalClientIface,
22    IClientModule, IdxRange, OutPointRange, PrimaryModulePriority,
23};
24use fedimint_client_module::oplog::IOperationLog;
25use fedimint_client_module::secret::{PlainRootSecretStrategy, RootSecretStrategy as _};
26use fedimint_client_module::sm::executor::{ActiveStateKey, IExecutor, InactiveStateKey};
27use fedimint_client_module::sm::{ActiveStateMeta, DynState, InactiveStateMeta};
28use fedimint_client_module::transaction::{
29    FeeQuote, FeeQuoteRequest, TRANSACTION_SUBMISSION_MODULE_INSTANCE, TransactionBuilder,
30    TxSubmissionStates, TxSubmissionStatesSM,
31};
32use fedimint_client_module::{
33    AddStateMachinesResult, ClientModuleInstance, GetInviteCodeRequest, ModuleGlobalContextGen,
34    ModuleRecoveryCompleted, TransactionUpdates, TxCreatedEvent,
35};
36use fedimint_connectors::{ConnectorRegistry, PeerStatus};
37use fedimint_core::config::{
38    ClientConfig, FederationId, GlobalClientConfig, JsonClientConfig, ModuleInitRegistry,
39};
40use fedimint_core::core::{DynInput, DynOutput, ModuleInstanceId, ModuleKind, OperationId};
41use fedimint_core::db::{
42    AutocommitError, Database, DatabaseRecord, DatabaseTransaction,
43    IDatabaseTransactionOpsCore as _, IDatabaseTransactionOpsCoreTyped as _, NonCommittable,
44};
45use fedimint_core::encoding::{Decodable, Encodable};
46use fedimint_core::endpoint_constants::{CLIENT_CONFIG_ENDPOINT, VERSION_ENDPOINT};
47use fedimint_core::envs::is_running_in_test_env;
48use fedimint_core::invite_code::InviteCode;
49use fedimint_core::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
50use fedimint_core::module::{
51    AmountUnit, Amounts, ApiRequestErased, ApiVersion, MultiApiVersion,
52    SupportedApiVersionsSummary, SupportedCoreApiVersions, SupportedModuleApiVersions,
53};
54use fedimint_core::net::api_announcement::SignedApiAnnouncement;
55use fedimint_core::runtime::sleep;
56use fedimint_core::task::{
57    Elapsed, MaybeSend, MaybeSync, ShuttingDownError, TaskGroup, TaskHandle,
58};
59use fedimint_core::transaction::Transaction;
60use fedimint_core::util::backoff_util::custom_backoff;
61use fedimint_core::util::{
62    BoxStream, FmtCompact as _, FmtCompactAnyhow as _, SafeUrl, backoff_util, retry,
63};
64use fedimint_core::{
65    Amount, ChainId, NumPeers, OutPoint, PeerId, apply, async_trait_maybe_send, maybe_add_send,
66    maybe_add_send_sync, runtime,
67};
68use fedimint_derive_secret::DerivableSecret;
69use fedimint_eventlog::{
70    DBTransactionEventLogExt as _, DynEventLogTrimableTracker, Event, EventKind, EventLogEntry,
71    EventLogId, EventLogTrimableId, EventLogTrimableTracker, EventPersistence, PersistedLogEntry,
72};
73use fedimint_logging::{LOG_CLIENT, LOG_CLIENT_NET_API, LOG_CLIENT_RECOVERY};
74use futures::stream::FuturesUnordered;
75use futures::{Stream, StreamExt as _};
76use global_ctx::ModuleGlobalClientContext;
77use serde::{Deserialize, Serialize};
78use tokio::sync::{broadcast, oneshot, watch};
79use tokio_stream::wrappers::WatchStream;
80use tracing::{Span, debug, info, warn};
81
82use crate::ClientBuilder;
83use crate::api_announcements::{ApiAnnouncementPrefix, get_api_urls};
84use crate::backup::Metadata;
85use crate::client::event_log::DefaultApplicationEventLogKey;
86use crate::db::{
87    ApiSecretKey, CachedApiVersionSet, CachedApiVersionSetKey, ChainIdKey,
88    ChronologicalOperationLogKey, ClientConfigKey, ClientMetadataKey, ClientModuleRecovery,
89    ClientModuleRecoveryState, EncodedClientSecretKey, OperationLogKey, PeerLastApiVersionsSummary,
90    PeerLastApiVersionsSummaryKey, PendingClientConfigKey, TransactionFeesKey,
91    apply_migrations_core_client_dbtx, get_decoded_client_secret, verify_client_db_integrity_dbtx,
92};
93use crate::meta::MetaService;
94use crate::module_init::{ClientModuleInitRegistry, DynClientModuleInit, IClientModuleInit};
95use crate::oplog::OperationLog;
96use crate::sm::executor::{
97    ActiveModuleOperationStateKeyPrefix, ActiveOperationStateKeyPrefix, Executor,
98    InactiveModuleOperationStateKeyPrefix, InactiveOperationStateKeyPrefix,
99};
100
101pub(crate) mod builder;
102pub(crate) mod event_log;
103pub(crate) mod global_ctx;
104pub(crate) mod handle;
105
106/// List of core api versions supported by the implementation.
107/// Notably `major` version is the one being supported, and corresponding
108/// `minor` version is the one required (for given `major` version).
109const SUPPORTED_CORE_API_VERSIONS: &[fedimint_core::module::ApiVersion] =
110    &[ApiVersion { major: 0, minor: 0 }];
111
112struct FinalizedTransaction {
113    transaction: Transaction,
114    states: Vec<DynState>,
115    change_range: Range<u64>,
116    fees: Amounts,
117}
118
119/// Primary module candidates at specific priority level
120#[derive(Default)]
121pub(crate) struct PrimaryModuleCandidates {
122    /// Modules that listed specific units they handle
123    specific: BTreeMap<AmountUnit, Vec<ModuleInstanceId>>,
124    /// Modules handling any unit
125    wildcard: Vec<ModuleInstanceId>,
126}
127
128/// Main client type
129///
130/// A handle and API to interacting with a single federation. End user
131/// applications that want to support interacting with multiple federations at
132/// the same time, will need to instantiate and manage multiple instances of
133/// this struct.
134///
135/// Under the hood it is starting and managing service tasks, state machines,
136/// database and other resources required.
137///
138/// This type is shared externally and internally, and
139/// [`crate::ClientHandle`] is responsible for external lifecycle management
140/// and resource freeing of the [`Client`].
141pub struct Client {
142    final_client: FinalClientIface,
143    config: tokio::sync::RwLock<ClientConfig>,
144    api_secret: Option<String>,
145    decoders: ModuleDecoderRegistry,
146    connectors: ConnectorRegistry,
147    db: Database,
148    federation_id: FederationId,
149    federation_config_meta: BTreeMap<String, String>,
150    primary_modules: BTreeMap<PrimaryModulePriority, PrimaryModuleCandidates>,
151    pub(crate) modules: ClientModuleRegistry,
152    module_inits: ClientModuleInitRegistry,
153    executor: Executor,
154    pub(crate) api: DynGlobalApi,
155    root_secret: DerivableSecret,
156    operation_log: OperationLog,
157    secp_ctx: Secp256k1<secp256k1::All>,
158    meta_service: Arc<MetaService>,
159
160    task_group: TaskGroup,
161
162    /// Long-lived span attached to every task spawned via [`Client::spawn`] /
163    /// [`Client::spawn_cancellable`], so logs from background tasks carry the
164    /// federation prefix.
165    client_span: Span,
166
167    /// Updates about client recovery progress
168    client_recovery_progress_receiver:
169        watch::Receiver<BTreeMap<ModuleInstanceId, RecoveryProgress>>,
170
171    /// Internal client sender to wake up log ordering task every time a
172    /// (unuordered) log event is added.
173    log_ordering_wakeup_tx: watch::Sender<()>,
174    /// Receiver for events fired every time (ordered) log event is added.
175    log_event_added_rx: watch::Receiver<()>,
176    log_event_added_transient_tx: broadcast::Sender<EventLogEntry>,
177    request_hook: ApiRequestHook,
178    iroh_enable_dht: bool,
179    /// User-provided Bitcoin RPC client for modules to use
180    ///
181    /// Stored here for potential future access; currently passed to modules
182    /// during initialization.
183    #[allow(dead_code)]
184    user_bitcoind_rpc: Option<DynBitcoindRpc>,
185    /// User-provided Bitcoin RPC factory for when ChainId is not available
186    ///
187    /// This is used as a fallback when the federation doesn't support ChainId.
188    /// Modules can call this with a URL from their config to get an RPC client.
189    pub(crate) user_bitcoind_rpc_no_chain_id:
190        Option<fedimint_client_module::module::init::BitcoindRpcNoChainIdFactory>,
191}
192
193#[derive(Debug, Serialize, Deserialize)]
194struct ListOperationsParams {
195    limit: Option<usize>,
196    last_seen: Option<ChronologicalOperationLogKey>,
197}
198
199const DEFAULT_EVENT_LOG_PAGE_SIZE: u64 = 100;
200const MAX_EVENT_LOG_PAGE_SIZE: u64 = 10_000;
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203struct GetEventLogRequest {
204    pos: Option<EventLogId>,
205    limit: Option<u64>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct GetOperationIdRequest {
210    operation_id: OperationId,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct GetBalanceChangesRequest {
215    #[serde(default = "AmountUnit::bitcoin")]
216    unit: AmountUnit,
217}
218
219impl Client {
220    /// Initialize a client builder that can be configured to create a new
221    /// client.
222    pub async fn builder() -> anyhow::Result<ClientBuilder> {
223        Ok(ClientBuilder::new())
224    }
225
226    pub fn api(&self) -> &(dyn IGlobalFederationApi + 'static) {
227        self.api.as_ref()
228    }
229
230    pub fn api_clone(&self) -> DynGlobalApi {
231        self.api.clone()
232    }
233
234    /// Returns a stream that emits the current connection status of all peers
235    /// whenever any peer's status changes. Emits initial state immediately.
236    pub fn connection_status_stream(&self) -> impl Stream<Item = BTreeMap<PeerId, PeerStatus>> {
237        self.api.connection_status_stream()
238    }
239
240    /// Establishes connections to all federation guardians once.
241    ///
242    /// Spawns tasks to connect to each guardian in the federation. Unlike
243    /// [`Self::spawn_federation_reconnect`], this only attempts to establish
244    /// connections once and completes - it does not maintain or reconnect.
245    ///
246    /// Useful for warming up connections before making API calls.
247    pub fn federation_reconnect(&self) {
248        let peers: Vec<PeerId> = self.api.all_peers().iter().copied().collect();
249
250        for peer_id in peers {
251            let api = self.api.clone();
252            self.spawn_cancellable(format!("federation-reconnect-once-{peer_id}"), async move {
253                if let Err(e) = api.get_peer_connection(peer_id).await {
254                    debug!(
255                        target: LOG_CLIENT_NET_API,
256                        %peer_id,
257                        err = %e.fmt_compact(),
258                        "Failed to connect to peer"
259                    );
260                }
261            });
262        }
263    }
264
265    /// Spawns background tasks that proactively maintain connections to all
266    /// federation guardians unconditionally.
267    ///
268    /// For each guardian, a task loops: establishes a connection, waits for it
269    /// to disconnect, then reconnects.
270    ///
271    /// The tasks are cancellable and will be terminated when the client shuts
272    /// down.
273    ///
274    /// By default [`Client`] creates connections on demand only, and share
275    /// them as long as they are alive.
276    ///
277    /// Reconnecting continuously might increase data and battery usage,
278    /// but potentially improve UX, depending on the time it takes to establish
279    /// a new network connection in given network conditions.
280    ///
281    /// Downstream users are encouraged to implement their own version of
282    /// this function, e.g. by reconnecting only when it is anticipated
283    /// that connection might be needed, or alternatively pre-warm
284    /// connections by calling [`Self::federation_reconnect`] when it seems
285    /// worthwhile.
286    pub fn spawn_federation_reconnect(&self) {
287        let peers: Vec<PeerId> = self.api.all_peers().iter().copied().collect();
288
289        for peer_id in peers {
290            let api = self.api.clone();
291            self.spawn_cancellable(format!("federation-reconnect-{peer_id}"), async move {
292                loop {
293                    match api.get_peer_connection(peer_id).await {
294                        Ok(conn) => {
295                            conn.await_disconnection().await;
296                        }
297                        Err(e) => {
298                            // Connection failed, backoff is handled inside
299                            // get_or_create_connection
300                            debug!(
301                                target: LOG_CLIENT_NET_API,
302                                %peer_id,
303                                err = %e.fmt_compact(),
304                                "Failed to connect to peer, will retry"
305                            );
306                        }
307                    }
308                }
309            });
310        }
311    }
312
313    /// Get the [`TaskGroup`] that is tied to Client's lifetime.
314    pub fn task_group(&self) -> &TaskGroup {
315        &self.task_group
316    }
317
318    /// Construct the long-lived span attached to all tasks spawned by this
319    /// client.
320    ///
321    /// `parent: None` keeps the span tree shallow; `runtime::spawn` already
322    /// wraps each task in its own `spawn(task=…)` span, so log events from
323    /// these tasks carry both `task` and `fed_id`.
324    pub(crate) fn make_client_span(federation_id: FederationId) -> Span {
325        tracing::info_span!(
326            target: LOG_CLIENT,
327            parent: None,
328            "client",
329            fed_id = %federation_id.to_prefix(),
330        )
331    }
332
333    /// Spawn a cancellable task on the client's task group, instrumented with
334    /// the client's [`Span`] so all events from the task carry `fed_id`.
335    pub(crate) fn spawn_cancellable<R>(
336        &self,
337        name: impl Into<String>,
338        future: impl Future<Output = R> + MaybeSend + 'static,
339    ) -> oneshot::Receiver<Result<R, ShuttingDownError>>
340    where
341        R: MaybeSend + 'static,
342    {
343        self.task_group
344            .spawn_cancellable_with_span(self.client_span.clone(), name, future)
345    }
346
347    /// Spawn a task on the client's task group, parented to the client's
348    /// [`Span`] so all events from the task carry `fed_id` (including the
349    /// task lifecycle events emitted by [`TaskGroup`] itself).
350    pub(crate) fn spawn<Fut, R>(
351        &self,
352        name: impl Into<String>,
353        f: impl FnOnce(TaskHandle) -> Fut + MaybeSend + 'static,
354    ) -> oneshot::Receiver<R>
355    where
356        Fut: Future<Output = R> + MaybeSend + 'static,
357        R: MaybeSend + 'static,
358    {
359        self.task_group
360            .spawn_with_span(self.client_span.clone(), name, f)
361    }
362
363    /// Returns all registered Prometheus metrics encoded in text format.
364    ///
365    /// This can be used by downstream clients to expose metrics via their own
366    /// HTTP server or print them for debugging purposes.
367    pub fn get_metrics() -> anyhow::Result<String> {
368        fedimint_metrics::get_metrics()
369    }
370
371    /// Useful for our CLI tooling, not meant for external use
372    #[doc(hidden)]
373    pub fn executor(&self) -> &Executor {
374        &self.executor
375    }
376
377    pub async fn get_config_from_db(db: &Database) -> Option<ClientConfig> {
378        let mut dbtx = db.begin_transaction_nc().await;
379        dbtx.get_value(&ClientConfigKey).await
380    }
381
382    pub async fn get_pending_config_from_db(db: &Database) -> Option<ClientConfig> {
383        let mut dbtx = db.begin_transaction_nc().await;
384        dbtx.get_value(&PendingClientConfigKey).await
385    }
386
387    pub async fn get_api_secret_from_db(db: &Database) -> Option<String> {
388        let mut dbtx = db.begin_transaction_nc().await;
389        dbtx.get_value(&ApiSecretKey).await
390    }
391
392    pub async fn store_encodable_client_secret<T: Encodable>(
393        db: &Database,
394        secret: T,
395    ) -> anyhow::Result<()> {
396        let mut dbtx = db.begin_transaction().await;
397
398        // Don't overwrite an existing secret
399        if dbtx.get_value(&EncodedClientSecretKey).await.is_some() {
400            bail!("Encoded client secret already exists, cannot overwrite")
401        }
402
403        let encoded_secret = T::consensus_encode_to_vec(&secret);
404        dbtx.insert_entry(&EncodedClientSecretKey, &encoded_secret)
405            .await;
406        dbtx.commit_tx().await;
407        Ok(())
408    }
409
410    pub async fn load_decodable_client_secret<T: Decodable>(db: &Database) -> anyhow::Result<T> {
411        let Some(secret) = Self::load_decodable_client_secret_opt(db).await? else {
412            bail!("Encoded client secret not present in DB")
413        };
414
415        Ok(secret)
416    }
417    pub async fn load_decodable_client_secret_opt<T: Decodable>(
418        db: &Database,
419    ) -> anyhow::Result<Option<T>> {
420        let mut dbtx = db.begin_transaction_nc().await;
421
422        let client_secret = dbtx.get_value(&EncodedClientSecretKey).await;
423
424        Ok(match client_secret {
425            Some(client_secret) => Some(
426                T::consensus_decode_whole(&client_secret, &ModuleRegistry::default())
427                    .map_err(|e| anyhow!("Decoding failed: {e}"))?,
428            ),
429            None => None,
430        })
431    }
432
433    pub async fn load_or_generate_client_secret(db: &Database) -> anyhow::Result<[u8; 64]> {
434        let client_secret = match Self::load_decodable_client_secret::<[u8; 64]>(db).await {
435            Ok(secret) => secret,
436            _ => {
437                let secret = PlainRootSecretStrategy::random(&mut thread_rng());
438                Self::store_encodable_client_secret(db, secret)
439                    .await
440                    .expect("Storing client secret must work");
441                secret
442            }
443        };
444        Ok(client_secret)
445    }
446
447    pub async fn is_initialized(db: &Database) -> bool {
448        let mut dbtx = db.begin_transaction_nc().await;
449        dbtx.raw_get_bytes(&[ClientConfigKey::DB_PREFIX])
450            .await
451            .expect("Unrecoverable error occurred while reading and entry from the database")
452            .is_some()
453    }
454
455    pub fn start_executor(self: &Arc<Self>) {
456        self.client_span.in_scope(|| {
457            debug!(
458                target: LOG_CLIENT,
459                "Starting fedimint client executor",
460            );
461        });
462        self.executor
463            .start_executor(self.context_gen(), self.client_span.clone());
464    }
465
466    pub fn federation_id(&self) -> FederationId {
467        self.federation_id
468    }
469
470    fn context_gen(self: &Arc<Self>) -> ModuleGlobalContextGen {
471        let client_inner = Arc::downgrade(self);
472        Arc::new(move |module_instance, operation| {
473            ModuleGlobalClientContext {
474                client: client_inner
475                    .clone()
476                    .upgrade()
477                    .expect("ModuleGlobalContextGen called after client was dropped"),
478                module_instance_id: module_instance,
479                operation,
480            }
481            .into()
482        })
483    }
484
485    pub async fn config(&self) -> ClientConfig {
486        self.config.read().await.clone()
487    }
488
489    // TODO: change to `-> Option<&str>`
490    pub fn api_secret(&self) -> &Option<String> {
491        &self.api_secret
492    }
493
494    /// Returns the core API version that the federation supports
495    ///
496    /// This reads from the cached version stored during client initialization.
497    /// If no cache is available (e.g., during initial setup), returns a default
498    /// version (0, 0).
499    pub async fn core_api_version(&self) -> ApiVersion {
500        // Try to get from cache. If not available, return a conservative
501        // default. The cache should always be populated after successful client init.
502        self.db
503            .begin_transaction_nc()
504            .await
505            .get_value(&CachedApiVersionSetKey)
506            .await
507            .map(|cached: CachedApiVersionSet| cached.0.core)
508            .unwrap_or(ApiVersion { major: 0, minor: 0 })
509    }
510
511    /// Returns the chain ID (bitcoin block hash at height 1) from the
512    /// federation
513    ///
514    /// This is cached in the database after the first successful fetch.
515    /// The chain ID uniquely identifies which bitcoin network the federation
516    /// operates on (mainnet, testnet, signet, regtest).
517    pub async fn chain_id(&self) -> anyhow::Result<ChainId> {
518        // Check cache first
519        if let Some(chain_id) = self
520            .db
521            .begin_transaction_nc()
522            .await
523            .get_value(&ChainIdKey)
524            .await
525        {
526            return Ok(chain_id);
527        }
528
529        // Fetch from federation with consensus
530        let chain_id = self.api.chain_id().await?;
531
532        // Cache the result
533        let mut dbtx = self.db.begin_transaction().await;
534        dbtx.insert_entry(&ChainIdKey, &chain_id).await;
535        dbtx.commit_tx().await;
536
537        Ok(chain_id)
538    }
539
540    pub fn decoders(&self) -> &ModuleDecoderRegistry {
541        &self.decoders
542    }
543
544    /// Returns a reference to the module, panics if not found
545    fn get_module(&self, instance: ModuleInstanceId) -> &maybe_add_send_sync!(dyn IClientModule) {
546        self.try_get_module(instance)
547            .expect("Module instance not found")
548    }
549
550    fn try_get_module(
551        &self,
552        instance: ModuleInstanceId,
553    ) -> Option<&maybe_add_send_sync!(dyn IClientModule)> {
554        Some(self.modules.get(instance)?.as_ref())
555    }
556
557    pub fn has_module(&self, instance: ModuleInstanceId) -> bool {
558        self.modules.get(instance).is_some()
559    }
560
561    /// Returns the input amount and output amount of a transaction
562    ///
563    /// # Panics
564    /// If any of the input or output versions in the transaction builder are
565    /// unknown by the respective module.
566    fn transaction_builder_get_balance(&self, builder: &TransactionBuilder) -> (Amounts, Amounts) {
567        // FIXME: prevent overflows, currently not suitable for untrusted input
568        let mut in_amounts = Amounts::ZERO;
569        let mut out_amounts = Amounts::ZERO;
570        let mut fee_amounts = Amounts::ZERO;
571
572        for input in builder.inputs() {
573            let module = self.get_module(input.input.module_instance_id());
574
575            let item_fees = module.input_fee(&input.amounts, &input.input).expect(
576                "We only build transactions with input versions that are supported by the module",
577            );
578
579            in_amounts.checked_add_mut(&input.amounts);
580            fee_amounts.checked_add_mut(&item_fees);
581        }
582
583        for output in builder.outputs() {
584            let module = self.get_module(output.output.module_instance_id());
585
586            let item_fees = module.output_fee(&output.amounts, &output.output).expect(
587                "We only build transactions with output versions that are supported by the module",
588            );
589
590            out_amounts.checked_add_mut(&output.amounts);
591            fee_amounts.checked_add_mut(&item_fees);
592        }
593
594        out_amounts.checked_add_mut(&fee_amounts);
595        (in_amounts, out_amounts)
596    }
597
598    pub fn get_internal_payment_markers(&self) -> anyhow::Result<(PublicKey, u64)> {
599        Ok((self.federation_id().to_fake_ln_pub_key(&self.secp_ctx)?, 0))
600    }
601
602    /// Get metadata value from the federation config itself
603    pub fn get_config_meta(&self, key: &str) -> Option<String> {
604        self.federation_config_meta.get(key).cloned()
605    }
606
607    pub(crate) fn root_secret(&self) -> DerivableSecret {
608        self.root_secret.clone()
609    }
610
611    pub async fn add_state_machines(
612        &self,
613        dbtx: &mut DatabaseTransaction<'_>,
614        states: Vec<DynState>,
615    ) -> AddStateMachinesResult {
616        self.executor.add_state_machines_dbtx(dbtx, states).await
617    }
618
619    // TODO: implement as part of [`OperationLog`]
620    pub async fn get_active_operations(&self) -> HashSet<OperationId> {
621        let active_states = self.executor.get_active_states().await;
622        let mut active_operations = HashSet::with_capacity(active_states.len());
623        let mut dbtx = self.db().begin_transaction_nc().await;
624        for (state, _) in active_states {
625            let operation_id = state.operation_id();
626            if dbtx
627                .get_value(&OperationLogKey { operation_id })
628                .await
629                .is_some()
630            {
631                active_operations.insert(operation_id);
632            }
633        }
634        active_operations
635    }
636
637    pub fn operation_log(&self) -> &OperationLog {
638        &self.operation_log
639    }
640
641    /// Get the meta manager to read meta fields.
642    pub fn meta_service(&self) -> &Arc<MetaService> {
643        &self.meta_service
644    }
645
646    /// Get the meta manager to read meta fields.
647    pub async fn get_meta_expiration_timestamp(&self) -> Option<SystemTime> {
648        let meta_service = self.meta_service();
649        let ts = meta_service
650            .get_field::<u64>(self.db(), "federation_expiry_timestamp")
651            .await
652            .and_then(|v| v.value)?;
653        Some(UNIX_EPOCH + Duration::from_secs(ts))
654    }
655
656    /// Adds funding to a transaction or removes over-funding via change.
657    async fn finalize_transaction(
658        &self,
659        dbtx: &mut DatabaseTransaction<'_>,
660        operation_id: OperationId,
661        mut partial_transaction: TransactionBuilder,
662    ) -> anyhow::Result<FinalizedTransaction> {
663        let (in_amounts, out_amounts) = self.transaction_builder_get_balance(&partial_transaction);
664
665        let mut added_inputs_bundles = vec![];
666        let mut added_outputs_bundles = vec![];
667
668        // The way currently things are implemented is OK for modules which can
669        // collect a fee relative to one being used, but will break down in any
670        // fancy scenarios. Future TODOs:
671        //
672        // * create_final_inputs_and_outputs needs to get broken down, so we can use
673        //   primary modules using priorities (possibly separate prios for inputs and
674        //   outputs to be able to drain modules, etc.); we need the split "check if
675        //   possible" and "take" steps,
676        // * extra inputs and outputs adding fees needs to be taken into account,
677        //   possibly with some looping
678        for unit in in_amounts.units().union(&out_amounts.units()) {
679            let input_amount = in_amounts.get(unit).copied().unwrap_or_default();
680            let output_amount = out_amounts.get(unit).copied().unwrap_or_default();
681            if input_amount == output_amount {
682                continue;
683            }
684
685            let Some((module_id, module)) = self.primary_module_for_unit(*unit) else {
686                bail!("No module to balance a partial transaction (affected unit: {unit:?}");
687            };
688
689            let (added_input_bundle, added_output_bundle) = module
690                .create_final_inputs_and_outputs(
691                    module_id,
692                    dbtx,
693                    operation_id,
694                    *unit,
695                    input_amount,
696                    output_amount,
697                )
698                .await?;
699
700            added_inputs_bundles.push(added_input_bundle);
701            added_outputs_bundles.push(added_output_bundle);
702        }
703
704        // This is the range of  outputs that will be added to the transaction
705        // in order to balance it. Notice that it may stay empty in case the transaction
706        // is already balanced.
707        let change_range = Range {
708            start: partial_transaction.outputs().count() as u64,
709            end: (partial_transaction.outputs().count() as u64
710                + added_outputs_bundles
711                    .iter()
712                    .map(|output| output.outputs().len() as u64)
713                    .sum::<u64>()),
714        };
715
716        for added_inputs in added_inputs_bundles {
717            partial_transaction = partial_transaction.with_inputs(added_inputs);
718        }
719
720        for added_outputs in added_outputs_bundles {
721            partial_transaction = partial_transaction.with_outputs(added_outputs);
722        }
723
724        let (input_amounts, output_amounts) =
725            self.transaction_builder_get_balance(&partial_transaction);
726
727        for (unit, output_amount) in output_amounts {
728            let input_amount = input_amounts.get(&unit).copied().unwrap_or_default();
729
730            assert!(input_amount >= output_amount, "Transaction is underfunded");
731        }
732
733        // Compute fees as the difference between total input and output amounts.
734        // This captures both explicit federation fees and any overpayment due to
735        // denomination constraints.
736        let fees = {
737            let mut input_total = Amounts::ZERO;
738            for input in partial_transaction.inputs() {
739                input_total
740                    .checked_add_mut(&input.amounts)
741                    .expect("Own transaction amounts don't overflow");
742            }
743            let mut output_total = Amounts::ZERO;
744            for output in partial_transaction.outputs() {
745                output_total
746                    .checked_add_mut(&output.amounts)
747                    .expect("Own transaction amounts don't overflow");
748            }
749            input_total
750                .checked_sub(&output_total)
751                .expect("Inputs >= outputs for own transactions")
752        };
753
754        let (transaction, states) = partial_transaction.build(&self.secp_ctx, thread_rng());
755
756        Ok(FinalizedTransaction {
757            transaction,
758            states,
759            change_range,
760            fees,
761        })
762    }
763
764    /// Computes the fee that finalizing and submitting a transaction with the
765    /// explicit items described by `request` would incur, without submitting
766    /// anything.
767    ///
768    /// This is the read-only twin of `Self::finalize_transaction`: it runs
769    /// the exact same primary-module balancing
770    /// (`create_final_inputs_and_outputs`, including funding selection /
771    /// consolidation / rebalancing) against a non-committable transaction
772    /// that is dropped rather than committed, so the client's funds are
773    /// read but left untouched. The quote is point-in-time: it depends on
774    /// the current inventory and can move as funds change.
775    ///
776    /// The mechanism is module-agnostic — [`FeeQuoteRequest`] only summarizes
777    /// the explicit side (gross amounts + federation fees) of whichever module
778    /// is quoting (mint, lightning, wallet, …), and the change is generated by
779    /// the primary module of each affected unit. The request and the resulting
780    /// breakdown are multi-unit, so an operation spanning several units (e.g.
781    /// Bitcoin plus a custom currency) is quoted in one call, mirroring how
782    /// `finalize_transaction` balances each unit independently.
783    pub async fn fee_quote(
784        &self,
785        operation_id: OperationId,
786        request: FeeQuoteRequest,
787    ) -> anyhow::Result<FeeQuote> {
788        let FeeQuoteRequest {
789            input_amount,
790            output_amount,
791            input_fee,
792            output_fee,
793        } = request;
794
795        // Start the totals from the explicit side; the change the primary
796        // modules add below is a disjoint set of items, accumulated into these
797        // same totals.
798        let mut gross_input = input_amount.clone();
799        let mut gross_output = output_amount.clone();
800        let mut input_fees = input_fee.clone();
801        let mut output_fees = output_fee.clone();
802
803        // The primary modules balance the transaction by minting change (and
804        // possibly pulling in extra inputs). The amounts they must balance
805        // mirror `finalize_transaction`: per unit the input side is the gross
806        // input value, the output side carries the gross output value plus
807        // every explicit fee (fees consume funding on the output side).
808        let balance_input = input_amount;
809        let balance_output = output_amount
810            .checked_add(&input_fee)
811            .and_then(|amounts| amounts.checked_add(&output_fee))
812            .expect("explicit amounts and fees cannot overflow an Amounts");
813
814        // Non-committable: the balancing writes (e.g. removing consolidated
815        // notes) are discarded on drop, so this is a pure dry-run over the real
816        // inventory.
817        let mut dbtx = self.db.begin_transaction_nc().await;
818
819        // Balance each affected unit independently via its own primary module,
820        // exactly as `finalize_transaction` does over the real transaction.
821        for unit in balance_input.units().union(&balance_output.units()) {
822            let balance_input_amount = balance_input.get(unit).copied().unwrap_or_default();
823            let balance_output_amount = balance_output.get(unit).copied().unwrap_or_default();
824            if balance_input_amount == balance_output_amount {
825                continue;
826            }
827
828            let Some((module_id, module)) = self.primary_module_for_unit(*unit) else {
829                bail!("No module to balance a partial transaction (affected unit: {unit:?}");
830            };
831
832            let (change_input, change_output) = module
833                .create_final_inputs_and_outputs(
834                    module_id,
835                    &mut dbtx.to_ref_nc(),
836                    operation_id,
837                    *unit,
838                    balance_input_amount,
839                    balance_output_amount,
840                )
841                .await?;
842
843            // Fold the change into the totals. These are a disjoint set of items
844            // from the explicit ones (the primary module only sees the scalar
845            // amounts to balance, never the explicit items), so this is not
846            // double-counting. Iterate the bundles the way `finalize_transaction`
847            // would, looking each fee up via the module that owns the item.
848            for input in change_input.inputs() {
849                let module = self.get_module(input.input.module_instance_id());
850                let fee = module
851                    .input_fee(&input.amounts, &input.input)
852                    .expect("Primary module must know its own change input fees");
853                gross_input.checked_add_mut(&input.amounts);
854                input_fees.checked_add_mut(&fee);
855            }
856
857            for output in change_output.outputs() {
858                let module = self.get_module(output.output.module_instance_id());
859                let fee = module
860                    .output_fee(&output.amounts, &output.output)
861                    .expect("Primary module must know its own change output fees");
862                gross_output.checked_add_mut(&output.amounts);
863                output_fees.checked_add_mut(&fee);
864            }
865        }
866
867        // Mark the dropped dbtx as intentionally uncommitted so the commit
868        // tracker doesn't warn.
869        dbtx.ignore_uncommitted();
870
871        // Per unit: net wallet gain = produced outputs − consumed inputs. The
872        // total fee is everything the gross input value did not become a net
873        // gain; whatever is left after the input/output fees is
874        // sub-denomination dust.
875        let mut dust = Amounts::ZERO;
876        for unit in gross_input.units().union(&gross_output.units()) {
877            let total = gross_input
878                .get(unit)
879                .copied()
880                .unwrap_or_default()
881                .saturating_sub(gross_output.get(unit).copied().unwrap_or_default());
882            let fees = input_fees.get(unit).copied().unwrap_or_default()
883                + output_fees.get(unit).copied().unwrap_or_default();
884            dust = dust
885                .checked_add_unit(total.saturating_sub(fees), *unit)
886                .expect("dust cannot overflow an Amounts");
887        }
888
889        Ok(FeeQuote {
890            input: input_fees,
891            output: output_fees,
892            dust,
893        })
894    }
895
896    /// Add funding and/or change to the transaction builder as needed, finalize
897    /// the transaction and submit it to the federation.
898    ///
899    /// ## Errors
900    /// The function will return an error if the operation with given ID already
901    /// exists.
902    ///
903    /// ## Panics
904    /// The function will panic if the database transaction collides with
905    /// other and fails with others too often, this should not happen except for
906    /// excessively concurrent scenarios.
907    pub async fn finalize_and_submit_transaction<F, M>(
908        &self,
909        operation_id: OperationId,
910        operation_type: &str,
911        operation_meta_gen: F,
912        tx_builder: TransactionBuilder,
913    ) -> anyhow::Result<OutPointRange>
914    where
915        F: Fn(OutPointRange) -> M + Clone + MaybeSend + MaybeSync,
916        M: serde::Serialize + MaybeSend,
917    {
918        let operation_type = operation_type.to_owned();
919
920        let autocommit_res = self
921            .db
922            .autocommit(
923                |dbtx, _| {
924                    let operation_type = operation_type.clone();
925                    let tx_builder = tx_builder.clone();
926                    let operation_meta_gen = operation_meta_gen.clone();
927                    Box::pin(async move {
928                        self.finalize_and_submit_transaction_dbtx(
929                            dbtx,
930                            operation_id,
931                            &operation_type,
932                            operation_meta_gen,
933                            tx_builder,
934                        )
935                        .await
936                    })
937                },
938                Some(100), // TODO: handle what happens after 100 retries
939            )
940            .await;
941
942        match autocommit_res {
943            Ok(txid) => Ok(txid),
944            Err(AutocommitError::ClosureError { error, .. }) => Err(error),
945            Err(AutocommitError::CommitFailed {
946                attempts,
947                last_error,
948            }) => panic!(
949                "Failed to commit tx submission dbtx after {attempts} attempts: {last_error}"
950            ),
951        }
952    }
953
954    /// See [`Self::finalize_and_submit_transaction`], just inside a database
955    /// transaction.
956    pub async fn finalize_and_submit_transaction_dbtx<F, M>(
957        &self,
958        dbtx: &mut DatabaseTransaction<'_>,
959        operation_id: OperationId,
960        operation_type: &str,
961        operation_meta_gen: F,
962        tx_builder: TransactionBuilder,
963    ) -> anyhow::Result<OutPointRange>
964    where
965        F: FnOnce(OutPointRange) -> M + MaybeSend,
966        M: serde::Serialize + MaybeSend,
967    {
968        if Client::operation_exists_dbtx(dbtx, operation_id).await {
969            bail!("There already exists an operation with id {operation_id:?}")
970        }
971
972        let out_point_range = self
973            .finalize_and_submit_transaction_inner(dbtx, operation_id, tx_builder)
974            .await?;
975
976        self.operation_log()
977            .add_operation_log_entry_dbtx(
978                dbtx,
979                operation_id,
980                operation_type,
981                operation_meta_gen(out_point_range),
982            )
983            .await;
984
985        Ok(out_point_range)
986    }
987
988    async fn finalize_and_submit_transaction_inner(
989        &self,
990        dbtx: &mut DatabaseTransaction<'_>,
991        operation_id: OperationId,
992        tx_builder: TransactionBuilder,
993    ) -> anyhow::Result<OutPointRange> {
994        let FinalizedTransaction {
995            transaction,
996            mut states,
997            change_range,
998            fees,
999        } = self
1000            .finalize_transaction(&mut dbtx.to_ref_nc(), operation_id, tx_builder)
1001            .await?;
1002
1003        if transaction.consensus_encode_to_vec().len() > Transaction::MAX_TX_SIZE {
1004            let inputs = transaction
1005                .inputs
1006                .iter()
1007                .map(DynInput::module_instance_id)
1008                .collect::<Vec<_>>();
1009            let outputs = transaction
1010                .outputs
1011                .iter()
1012                .map(DynOutput::module_instance_id)
1013                .collect::<Vec<_>>();
1014            warn!(
1015                target: LOG_CLIENT_NET_API,
1016                size=%transaction.consensus_encode_to_vec().len(),
1017                ?inputs,
1018                ?outputs,
1019                "Transaction too large",
1020            );
1021            debug!(target: LOG_CLIENT_NET_API, ?transaction, "transaction details");
1022            bail!(
1023                "The generated transaction would be rejected by the federation for being too large."
1024            );
1025        }
1026
1027        let txid = transaction.tx_hash();
1028
1029        debug!(
1030            target: LOG_CLIENT_NET_API,
1031            %txid,
1032            operation_id = %operation_id.fmt_short(),
1033            ?transaction,
1034            "Finalized and submitting transaction",
1035        );
1036
1037        let tx_submission_sm = DynState::from_typed(
1038            TRANSACTION_SUBMISSION_MODULE_INSTANCE,
1039            TxSubmissionStatesSM {
1040                operation_id,
1041                state: TxSubmissionStates::Created(transaction),
1042            },
1043        );
1044        states.push(tx_submission_sm);
1045
1046        self.executor.add_state_machines_dbtx(dbtx, states).await?;
1047
1048        dbtx.insert_new_entry(&TransactionFeesKey(txid), &fees)
1049            .await;
1050
1051        self.log_event_dbtx(dbtx, None, TxCreatedEvent { txid, operation_id })
1052            .await;
1053
1054        Ok(OutPointRange::new(txid, IdxRange::from(change_range)))
1055    }
1056
1057    async fn transaction_update_stream(
1058        &self,
1059        operation_id: OperationId,
1060    ) -> BoxStream<'static, TxSubmissionStatesSM> {
1061        self.executor
1062            .notifier()
1063            .module_notifier::<TxSubmissionStatesSM>(
1064                TRANSACTION_SUBMISSION_MODULE_INSTANCE,
1065                self.final_client.clone(),
1066            )
1067            .subscribe(operation_id)
1068            .await
1069    }
1070
1071    pub async fn operation_exists(&self, operation_id: OperationId) -> bool {
1072        let mut dbtx = self.db().begin_transaction_nc().await;
1073
1074        Client::operation_exists_dbtx(&mut dbtx, operation_id).await
1075    }
1076
1077    pub async fn operation_exists_dbtx(
1078        dbtx: &mut DatabaseTransaction<'_>,
1079        operation_id: OperationId,
1080    ) -> bool {
1081        let active_state_exists = dbtx
1082            .find_by_prefix(&ActiveOperationStateKeyPrefix { operation_id })
1083            .await
1084            .next()
1085            .await
1086            .is_some();
1087
1088        let inactive_state_exists = dbtx
1089            .find_by_prefix(&InactiveOperationStateKeyPrefix { operation_id })
1090            .await
1091            .next()
1092            .await
1093            .is_some();
1094
1095        active_state_exists || inactive_state_exists
1096    }
1097
1098    pub async fn has_active_states(&self, operation_id: OperationId) -> bool {
1099        self.db
1100            .begin_transaction_nc()
1101            .await
1102            .find_by_prefix(&ActiveOperationStateKeyPrefix { operation_id })
1103            .await
1104            .next()
1105            .await
1106            .is_some()
1107    }
1108
1109    /// Calculates the federation fees paid in the course of the operation.
1110    ///
1111    /// Federation fees are fees paid to the federation (e.g. for ecash) and do
1112    /// not include fees paid to service providers like the Lightning gateway.
1113    /// These still have to be separately reported by the module.
1114    ///
1115    /// Fees are calculated by subtracting the total output amount from the
1116    /// total input amount, any difference is either due to direct fees or
1117    /// overpayment to avoid generating uneconomic outputs.
1118    ///
1119    /// The returned amount may still increase while the operation is active;
1120    /// use [`Client::has_active_states`] to check whether it is final.
1121    ///
1122    /// Returns `Ok(None)` if any transaction's fee data is missing (e.g. for
1123    /// operations created before this feature was enabled).
1124    ///
1125    /// # Errors
1126    /// Returns an error if the operation does not exist.
1127    pub async fn get_operation_fees(
1128        &self,
1129        operation_id: OperationId,
1130    ) -> anyhow::Result<Option<Amounts>> {
1131        if !self.operation_exists(operation_id).await {
1132            bail!("Operation does not exist");
1133        }
1134
1135        let (active_states, inactive_states) =
1136            self.executor().get_operation_states(operation_id).await;
1137
1138        let states = active_states
1139            .into_iter()
1140            .map(|(state, _)| state)
1141            .chain(inactive_states.into_iter().map(|(state, _)| state));
1142
1143        let accepted_transactions = states
1144            .filter_map(|state| {
1145                let tx_state = state.as_any().downcast_ref::<TxSubmissionStatesSM>()?;
1146
1147                match &tx_state.state {
1148                    TxSubmissionStates::Accepted(transaction_id) => Some(*transaction_id),
1149                    _ => None,
1150                }
1151            })
1152            .collect::<HashSet<_>>();
1153
1154        // For each accepted transaction, look up its stored fees
1155        let mut dbtx = self.db.begin_transaction_nc().await;
1156        let mut total_fees = Amounts::ZERO;
1157        for txid in &accepted_transactions {
1158            let Some(fees) = dbtx.get_value(&TransactionFeesKey(*txid)).await else {
1159                return Ok(None);
1160            };
1161            total_fees = total_fees
1162                .checked_add(&fees)
1163                .expect("Fee amounts don't overflow in practice");
1164        }
1165
1166        Ok(Some(total_fees))
1167    }
1168
1169    /// Waits for an output from the primary module to reach its final
1170    /// state.
1171    pub async fn await_primary_bitcoin_module_output(
1172        &self,
1173        operation_id: OperationId,
1174        out_point: OutPoint,
1175    ) -> anyhow::Result<()> {
1176        self.primary_module_for_unit(AmountUnit::BITCOIN)
1177            .ok_or_else(|| anyhow!("No primary module available"))?
1178            .1
1179            .await_primary_module_output(operation_id, out_point)
1180            .await
1181    }
1182
1183    /// Returns a reference to a typed module client instance by kind
1184    pub fn get_first_module<M: ClientModule>(
1185        &'_ self,
1186    ) -> anyhow::Result<ClientModuleInstance<'_, M>> {
1187        let module_kind = M::kind();
1188        let id = self
1189            .get_first_instance(&module_kind)
1190            .ok_or_else(|| format_err!("No modules found of kind {module_kind}"))?;
1191        let module: &M = self
1192            .try_get_module(id)
1193            .ok_or_else(|| format_err!("Unknown module instance {id}"))?
1194            .as_any()
1195            .downcast_ref::<M>()
1196            .ok_or_else(|| format_err!("Module is not of type {}", std::any::type_name::<M>()))?;
1197        let (db, _) = self.db().with_prefix_module_id(id);
1198        Ok(ClientModuleInstance {
1199            id,
1200            db,
1201            api: self.api().with_module(id),
1202            module,
1203        })
1204    }
1205
1206    pub fn get_module_client_dyn(
1207        &self,
1208        instance_id: ModuleInstanceId,
1209    ) -> anyhow::Result<&maybe_add_send_sync!(dyn IClientModule)> {
1210        self.try_get_module(instance_id)
1211            .ok_or(anyhow!("Unknown module instance {}", instance_id))
1212    }
1213
1214    pub fn db(&self) -> &Database {
1215        &self.db
1216    }
1217
1218    pub fn endpoints(&self) -> &ConnectorRegistry {
1219        &self.connectors
1220    }
1221
1222    /// Returns a stream of transaction updates for the given operation id that
1223    /// can later be used to watch for a specific transaction being accepted.
1224    pub async fn transaction_updates(&self, operation_id: OperationId) -> TransactionUpdates {
1225        TransactionUpdates {
1226            update_stream: self.transaction_update_stream(operation_id).await,
1227        }
1228    }
1229
1230    /// Returns the instance id of the first module of the given kind.
1231    pub fn get_first_instance(&self, module_kind: &ModuleKind) -> Option<ModuleInstanceId> {
1232        self.modules
1233            .iter_modules()
1234            .find(|(_, kind, _module)| *kind == module_kind)
1235            .map(|(instance_id, _, _)| instance_id)
1236    }
1237
1238    /// Returns the data from which the client's root secret is derived (e.g.
1239    /// BIP39 seed phrase struct).
1240    pub async fn root_secret_encoding<T: Decodable>(&self) -> anyhow::Result<T> {
1241        get_decoded_client_secret::<T>(self.db()).await
1242    }
1243
1244    /// Waits for outputs from the primary module to reach its final
1245    /// state.
1246    pub async fn await_primary_bitcoin_module_outputs(
1247        &self,
1248        operation_id: OperationId,
1249        outputs: Vec<OutPoint>,
1250    ) -> anyhow::Result<()> {
1251        for out_point in outputs {
1252            self.await_primary_bitcoin_module_output(operation_id, out_point)
1253                .await?;
1254        }
1255
1256        Ok(())
1257    }
1258
1259    /// Returns the config of the client in JSON format.
1260    ///
1261    /// Compared to the consensus module format where module configs are binary
1262    /// encoded this format cannot be cryptographically verified but is easier
1263    /// to consume and to some degree human-readable.
1264    pub async fn get_config_json(&self) -> JsonClientConfig {
1265        self.config().await.to_json()
1266    }
1267
1268    // Ideally this would not be in the API, but there's a lot of places where this
1269    // makes it easier.
1270    #[doc(hidden)]
1271    /// Like [`Self::get_balance`] but returns an error if primary module is not
1272    /// available
1273    pub async fn get_balance_for_btc(&self) -> anyhow::Result<Amount> {
1274        self.get_balance_for_unit(AmountUnit::BITCOIN).await
1275    }
1276
1277    pub async fn get_balance_for_unit(&self, unit: AmountUnit) -> anyhow::Result<Amount> {
1278        let (id, module) = self
1279            .primary_module_for_unit(unit)
1280            .ok_or_else(|| anyhow!("Primary module not available"))?;
1281        Ok(module
1282            .get_balance(id, &mut self.db().begin_transaction_nc().await, unit)
1283            .await)
1284    }
1285
1286    /// Returns a stream that yields the current client balance every time it
1287    /// changes.
1288    pub async fn subscribe_balance_changes(&self, unit: AmountUnit) -> BoxStream<'static, Amount> {
1289        let primary_module_things =
1290            if let Some((primary_module_id, primary_module)) = self.primary_module_for_unit(unit) {
1291                let balance_changes = primary_module.subscribe_balance_changes().await;
1292                let initial_balance = self
1293                    .get_balance_for_unit(unit)
1294                    .await
1295                    .expect("Primary is present");
1296
1297                Some((
1298                    primary_module_id,
1299                    primary_module.clone(),
1300                    balance_changes,
1301                    initial_balance,
1302                ))
1303            } else {
1304                None
1305            };
1306        let db = self.db().clone();
1307
1308        Box::pin(async_stream::stream! {
1309            let Some((primary_module_id, primary_module, mut balance_changes, initial_balance)) = primary_module_things else {
1310                // If there is no primary module, there will not be one until client is
1311                // restarted
1312                pending().await
1313            };
1314
1315
1316            yield initial_balance;
1317            let mut prev_balance = initial_balance;
1318            while let Some(()) = balance_changes.next().await {
1319                let mut dbtx = db.begin_transaction_nc().await;
1320                let balance = primary_module
1321                     .get_balance(primary_module_id, &mut dbtx, unit)
1322                    .await;
1323
1324                // Deduplicate in case modules cannot always tell if the balance actually changed
1325                if balance != prev_balance {
1326                    prev_balance = balance;
1327                    yield balance;
1328                }
1329            }
1330        })
1331    }
1332
1333    /// Make a single API version request to a peer after a delay.
1334    ///
1335    /// The delay is here to unify the type of a future both for initial request
1336    /// and possible retries.
1337    async fn make_api_version_request(
1338        delay: Duration,
1339        peer_id: PeerId,
1340        api: &DynGlobalApi,
1341    ) -> (
1342        PeerId,
1343        Result<SupportedApiVersionsSummary, fedimint_connectors::error::ServerError>,
1344    ) {
1345        runtime::sleep(delay).await;
1346        (
1347            peer_id,
1348            api.request_single_peer::<SupportedApiVersionsSummary>(
1349                VERSION_ENDPOINT.to_owned(),
1350                ApiRequestErased::default(),
1351                peer_id,
1352            )
1353            .await,
1354        )
1355    }
1356
1357    /// Create a backoff strategy for API version requests.
1358    ///
1359    /// Keep trying, initially somewhat aggressively, but after a while retry
1360    /// very slowly, because chances for response are getting lower and
1361    /// lower.
1362    fn create_api_version_backoff() -> impl Iterator<Item = Duration> {
1363        custom_backoff(Duration::from_millis(200), Duration::from_secs(600), None)
1364    }
1365
1366    /// Query the federation for API version support and then calculate
1367    /// the best API version to use (supported by most guardians).
1368    pub async fn fetch_common_api_versions_from_all_peers(
1369        num_peers: NumPeers,
1370        api: DynGlobalApi,
1371        db: Database,
1372        num_responses_sender: watch::Sender<usize>,
1373    ) {
1374        let mut backoff = Self::create_api_version_backoff();
1375
1376        // NOTE: `FuturesUnordered` is a footgun, but since we only poll it for result
1377        // and make a single async db write operation, it should be OK.
1378        let mut requests = FuturesUnordered::new();
1379
1380        for peer_id in num_peers.peer_ids() {
1381            requests.push(Self::make_api_version_request(
1382                Duration::ZERO,
1383                peer_id,
1384                &api,
1385            ));
1386        }
1387
1388        let mut num_responses = 0;
1389
1390        while let Some((peer_id, response)) = requests.next().await {
1391            let retry = match response {
1392                Err(err) => {
1393                    let has_previous_response = db
1394                        .begin_transaction_nc()
1395                        .await
1396                        .get_value(&PeerLastApiVersionsSummaryKey(peer_id))
1397                        .await
1398                        .is_some();
1399                    debug!(
1400                        target: LOG_CLIENT,
1401                        %peer_id,
1402                        err = %err.fmt_compact(),
1403                        %has_previous_response,
1404                        "Failed to refresh API versions of a peer"
1405                    );
1406
1407                    !has_previous_response
1408                }
1409                Ok(o) => {
1410                    // Save the response to the database right away, just to
1411                    // not lose it
1412                    let mut dbtx = db.begin_transaction().await;
1413                    dbtx.insert_entry(
1414                        &PeerLastApiVersionsSummaryKey(peer_id),
1415                        &PeerLastApiVersionsSummary(o),
1416                    )
1417                    .await;
1418                    dbtx.commit_tx().await;
1419                    false
1420                }
1421            };
1422
1423            if retry {
1424                requests.push(Self::make_api_version_request(
1425                    backoff.next().expect("Keeps retrying"),
1426                    peer_id,
1427                    &api,
1428                ));
1429            } else {
1430                num_responses += 1;
1431                num_responses_sender.send_replace(num_responses);
1432            }
1433        }
1434    }
1435
1436    /// Fetch API versions from peers, retrying until we get threshold number of
1437    /// successful responses. Returns the successful responses collected
1438    /// from at least `num_peers.threshold()` peers.
1439    pub async fn fetch_peers_api_versions_from_threshold_of_peers(
1440        num_peers: NumPeers,
1441        api: DynGlobalApi,
1442    ) -> BTreeMap<PeerId, SupportedApiVersionsSummary> {
1443        let mut backoff = Self::create_api_version_backoff();
1444
1445        // NOTE: `FuturesUnordered` is a footgun, but since we only poll it for result
1446        // and collect responses, it should be OK.
1447        let mut requests = FuturesUnordered::new();
1448
1449        for peer_id in num_peers.peer_ids() {
1450            requests.push(Self::make_api_version_request(
1451                Duration::ZERO,
1452                peer_id,
1453                &api,
1454            ));
1455        }
1456
1457        let mut successful_responses = BTreeMap::new();
1458
1459        while successful_responses.len() < num_peers.threshold()
1460            && let Some((peer_id, response)) = requests.next().await
1461        {
1462            let retry = match response {
1463                Err(err) => {
1464                    debug!(
1465                        target: LOG_CLIENT,
1466                        %peer_id,
1467                        err = %err.fmt_compact(),
1468                        "Failed to fetch API versions from peer"
1469                    );
1470                    true
1471                }
1472                Ok(response) => {
1473                    successful_responses.insert(peer_id, response);
1474                    false
1475                }
1476            };
1477
1478            if retry {
1479                requests.push(Self::make_api_version_request(
1480                    backoff.next().expect("Keeps retrying"),
1481                    peer_id,
1482                    &api,
1483                ));
1484            }
1485        }
1486
1487        successful_responses
1488    }
1489
1490    /// Fetch API versions from peers and discover common API versions to use.
1491    pub async fn fetch_common_api_versions(
1492        config: &ClientConfig,
1493        api: &DynGlobalApi,
1494    ) -> anyhow::Result<BTreeMap<PeerId, SupportedApiVersionsSummary>> {
1495        debug!(
1496            target: LOG_CLIENT,
1497            "Fetching common api versions"
1498        );
1499
1500        let num_peers = NumPeers::from(config.global.api_endpoints.len());
1501
1502        let peer_api_version_sets =
1503            Self::fetch_peers_api_versions_from_threshold_of_peers(num_peers, api.clone()).await;
1504
1505        Ok(peer_api_version_sets)
1506    }
1507
1508    /// Write API version set to database cache.
1509    /// Used when we have a pre-calculated API version set that should be stored
1510    /// for later use.
1511    pub async fn write_api_version_cache(
1512        dbtx: &mut DatabaseTransaction<'_>,
1513        api_version_set: ApiVersionSet,
1514    ) {
1515        debug!(
1516            target: LOG_CLIENT,
1517            value = ?api_version_set,
1518            "Writing API version set to cache"
1519        );
1520
1521        dbtx.insert_entry(
1522            &CachedApiVersionSetKey,
1523            &CachedApiVersionSet(api_version_set),
1524        )
1525        .await;
1526    }
1527
1528    /// Store prefetched peer API version responses and calculate/store common
1529    /// API version set. This processes the individual peer responses by
1530    /// storing them in the database and calculating the common API version
1531    /// set for caching.
1532    pub async fn store_prefetched_api_versions(
1533        db: &Database,
1534        config: &ClientConfig,
1535        client_module_init: &ClientModuleInitRegistry,
1536        peer_api_versions: &BTreeMap<PeerId, SupportedApiVersionsSummary>,
1537    ) {
1538        debug!(
1539            target: LOG_CLIENT,
1540            "Storing {} prefetched peer API version responses and calculating common version set",
1541            peer_api_versions.len()
1542        );
1543
1544        let mut dbtx = db.begin_transaction().await;
1545        // Calculate common API version set from individual responses
1546        let client_supported_versions =
1547            Self::supported_api_versions_summary_static(config, client_module_init);
1548        match fedimint_client_module::api_version_discovery::discover_common_api_versions_set(
1549            &client_supported_versions,
1550            peer_api_versions,
1551        ) {
1552            Ok(common_api_versions) => {
1553                // Write the calculated common API version set to database cache
1554                Self::write_api_version_cache(&mut dbtx.to_ref_nc(), common_api_versions).await;
1555                debug!(target: LOG_CLIENT, "Calculated and stored common API version set");
1556            }
1557            Err(err) => {
1558                debug!(target: LOG_CLIENT, err = %err.fmt_compact_anyhow(), "Failed to calculate common API versions from prefetched data");
1559            }
1560        }
1561
1562        // Store individual peer responses to database
1563        for (peer_id, peer_api_versions) in peer_api_versions {
1564            dbtx.insert_entry(
1565                &PeerLastApiVersionsSummaryKey(*peer_id),
1566                &PeerLastApiVersionsSummary(peer_api_versions.clone()),
1567            )
1568            .await;
1569        }
1570        dbtx.commit_tx().await;
1571        debug!(target: LOG_CLIENT, "Stored individual peer API version responses");
1572    }
1573
1574    /// [`SupportedApiVersionsSummary`] that the client and its modules support
1575    pub fn supported_api_versions_summary_static(
1576        config: &ClientConfig,
1577        client_module_init: &ClientModuleInitRegistry,
1578    ) -> SupportedApiVersionsSummary {
1579        SupportedApiVersionsSummary {
1580            core: SupportedCoreApiVersions {
1581                core_consensus: config.global.consensus_version,
1582                api: MultiApiVersion::try_from_iter(SUPPORTED_CORE_API_VERSIONS.to_owned())
1583                    .expect("must not have conflicting versions"),
1584            },
1585            modules: config
1586                .modules
1587                .iter()
1588                .filter_map(|(&module_instance_id, module_config)| {
1589                    client_module_init
1590                        .get(module_config.kind())
1591                        .map(|module_init| {
1592                            (
1593                                module_instance_id,
1594                                SupportedModuleApiVersions {
1595                                    core_consensus: config.global.consensus_version,
1596                                    module_consensus: module_config.version,
1597                                    api: module_init.supported_api_versions(),
1598                                },
1599                            )
1600                        })
1601                })
1602                .collect(),
1603        }
1604    }
1605
1606    pub async fn load_and_refresh_common_api_version(&self) -> anyhow::Result<ApiVersionSet> {
1607        Self::load_and_refresh_common_api_version_static(
1608            &self.config().await,
1609            &self.module_inits,
1610            self.connectors.clone(),
1611            &self.api,
1612            &self.db,
1613            &self.task_group,
1614            &self.client_span,
1615        )
1616        .await
1617    }
1618
1619    /// Force refresh API versions from the federation, bypassing the cache.
1620    ///
1621    /// This queries all peers for their supported API versions and calculates
1622    /// the common API version set to use. The result is stored in the database
1623    /// cache for future use.
1624    pub async fn refresh_api_versions(&self) -> anyhow::Result<ApiVersionSet> {
1625        Self::refresh_common_api_version_static(
1626            &self.config().await,
1627            &self.module_inits,
1628            &self.api,
1629            &self.db,
1630            self.task_group.clone(),
1631            &self.client_span,
1632            true,
1633        )
1634        .await
1635    }
1636
1637    /// Load the common api versions to use from cache and start a background
1638    /// process to refresh them.
1639    ///
1640    /// This is a compromise, so we not have to wait for version discovery to
1641    /// complete every time a [`Client`] is being built.
1642    pub(crate) async fn load_and_refresh_common_api_version_static(
1643        config: &ClientConfig,
1644        module_init: &ClientModuleInitRegistry,
1645        connectors: ConnectorRegistry,
1646        api: &DynGlobalApi,
1647        db: &Database,
1648        task_group: &TaskGroup,
1649        client_span: &Span,
1650    ) -> anyhow::Result<ApiVersionSet> {
1651        if let Some(v) = db
1652            .begin_transaction_nc()
1653            .await
1654            .get_value(&CachedApiVersionSetKey)
1655            .await
1656        {
1657            client_span.in_scope(|| {
1658                debug!(
1659                    target: LOG_CLIENT,
1660                    "Found existing cached common api versions"
1661                );
1662            });
1663            let config = config.clone();
1664            let client_module_init = module_init.clone();
1665            let api = api.clone();
1666            let db = db.clone();
1667            let task_group = task_group.clone();
1668            let client_span_owned = client_span.clone();
1669            // Separate task group, because we actually don't want to be waiting for this to
1670            // finish, and it's just best effort.
1671            task_group.clone().spawn_cancellable_with_span(
1672                client_span.clone(),
1673                "refresh_common_api_version_static",
1674                async move {
1675                    connectors.wait_for_initialized_connections().await;
1676
1677                    if let Err(error) = Self::refresh_common_api_version_static(
1678                        &config,
1679                        &client_module_init,
1680                        &api,
1681                        &db,
1682                        task_group,
1683                        &client_span_owned,
1684                        false,
1685                    )
1686                    .await
1687                    {
1688                        warn!(
1689                            target: LOG_CLIENT,
1690                            err = %error.fmt_compact_anyhow(), "Failed to discover common api versions"
1691                        );
1692                    }
1693                },
1694            );
1695
1696            return Ok(v.0);
1697        }
1698
1699        info!(
1700            target: LOG_CLIENT,
1701            "Fetching initial API versions "
1702        );
1703        Self::refresh_common_api_version_static(
1704            config,
1705            module_init,
1706            api,
1707            db,
1708            task_group.clone(),
1709            client_span,
1710            true,
1711        )
1712        .await
1713    }
1714
1715    async fn refresh_common_api_version_static(
1716        config: &ClientConfig,
1717        client_module_init: &ClientModuleInitRegistry,
1718        api: &DynGlobalApi,
1719        db: &Database,
1720        task_group: TaskGroup,
1721        client_span: &Span,
1722        block_until_ok: bool,
1723    ) -> anyhow::Result<ApiVersionSet> {
1724        debug!(
1725            target: LOG_CLIENT,
1726            "Refreshing common api versions"
1727        );
1728
1729        let (num_responses_sender, mut num_responses_receiver) = tokio::sync::watch::channel(0);
1730        let num_peers = NumPeers::from(config.global.api_endpoints.len());
1731
1732        task_group.spawn_cancellable_with_span(
1733            client_span.clone(),
1734            "refresh peers api versions",
1735            Client::fetch_common_api_versions_from_all_peers(
1736                num_peers,
1737                api.clone(),
1738                db.clone(),
1739                num_responses_sender,
1740            ),
1741        );
1742
1743        let common_api_versions = loop {
1744            // Wait to collect enough answers before calculating a set of common api
1745            // versions to use. Note that all peers individual responses from
1746            // previous attempts are still being used, and requests, or even
1747            // retries for response of peers are not actually cancelled, as they
1748            // are happening on a separate task. This is all just to bound the
1749            // time user can be waiting for the join operation to finish, at the
1750            // risk of picking wrong version in very rare circumstances.
1751            let _: Result<_, Elapsed> = runtime::timeout(
1752                Duration::from_secs(30),
1753                num_responses_receiver.wait_for(|num| num_peers.threshold() <= *num),
1754            )
1755            .await;
1756
1757            let peer_api_version_sets = Self::load_peers_last_api_versions(db, num_peers).await;
1758
1759            match fedimint_client_module::api_version_discovery::discover_common_api_versions_set(
1760                &Self::supported_api_versions_summary_static(config, client_module_init),
1761                &peer_api_version_sets,
1762            ) {
1763                Ok(o) => break o,
1764                Err(err) if block_until_ok => {
1765                    warn!(
1766                        target: LOG_CLIENT,
1767                        err = %err.fmt_compact_anyhow(),
1768                        "Failed to discover API version to use. Retrying..."
1769                    );
1770                    continue;
1771                }
1772                Err(e) => return Err(e),
1773            }
1774        };
1775
1776        debug!(
1777            target: LOG_CLIENT,
1778            value = ?common_api_versions,
1779            "Updating the cached common api versions"
1780        );
1781        let mut dbtx = db.begin_transaction().await;
1782        let _ = dbtx
1783            .insert_entry(
1784                &CachedApiVersionSetKey,
1785                &CachedApiVersionSet(common_api_versions.clone()),
1786            )
1787            .await;
1788
1789        dbtx.commit_tx().await;
1790
1791        Ok(common_api_versions)
1792    }
1793
1794    /// Get the client [`Metadata`]
1795    pub async fn get_metadata(&self) -> Metadata {
1796        self.db
1797            .begin_transaction_nc()
1798            .await
1799            .get_value(&ClientMetadataKey)
1800            .await
1801            .unwrap_or_else(|| {
1802                warn!(
1803                    target: LOG_CLIENT,
1804                    "Missing existing metadata. This key should have been set on Client init"
1805                );
1806                Metadata::empty()
1807            })
1808    }
1809
1810    /// Set the client [`Metadata`]
1811    pub async fn set_metadata(&self, metadata: &Metadata) {
1812        self.db
1813            .autocommit::<_, _, anyhow::Error>(
1814                |dbtx, _| {
1815                    Box::pin(async {
1816                        Self::set_metadata_dbtx(dbtx, metadata).await;
1817                        Ok(())
1818                    })
1819                },
1820                None,
1821            )
1822            .await
1823            .expect("Failed to autocommit metadata");
1824    }
1825
1826    pub fn has_pending_recoveries(&self) -> bool {
1827        !self
1828            .client_recovery_progress_receiver
1829            .borrow()
1830            .iter()
1831            .all(|(_id, progress)| progress.is_done())
1832    }
1833
1834    /// Wait for all module recoveries to finish
1835    ///
1836    /// This will block until the recovery task is done with recoveries.
1837    /// Returns success if all recovery tasks are complete (success case),
1838    /// or an error if some modules could not complete the recovery at the time.
1839    ///
1840    /// A bit of a heavy approach.
1841    pub async fn wait_for_all_recoveries(&self) -> anyhow::Result<()> {
1842        let mut recovery_receiver = self.client_recovery_progress_receiver.clone();
1843        recovery_receiver
1844            .wait_for(|in_progress| {
1845                in_progress
1846                    .iter()
1847                    .all(|(_id, progress)| progress.is_done())
1848            })
1849            .await
1850            .context("Recovery task completed and update receiver disconnected, but some modules failed to recover")?;
1851
1852        Ok(())
1853    }
1854
1855    /// Subscribe to recover progress for all the modules.
1856    ///
1857    /// This stream can contain duplicate progress for a module.
1858    /// Don't use this stream for detecting completion of recovery.
1859    pub fn subscribe_to_recovery_progress(
1860        &self,
1861    ) -> impl Stream<Item = (ModuleInstanceId, RecoveryProgress)> + use<> {
1862        WatchStream::new(self.client_recovery_progress_receiver.clone())
1863            .flat_map(futures::stream::iter)
1864    }
1865
1866    pub async fn wait_for_module_kind_recovery(
1867        &self,
1868        module_kind: ModuleKind,
1869    ) -> anyhow::Result<()> {
1870        let mut recovery_receiver = self.client_recovery_progress_receiver.clone();
1871        let config = self.config().await;
1872        recovery_receiver
1873            .wait_for(|in_progress| {
1874                !in_progress
1875                    .iter()
1876                    .filter(|(module_instance_id, _progress)| {
1877                        config.modules[module_instance_id].kind == module_kind
1878                    })
1879                    .any(|(_id, progress)| !progress.is_done())
1880            })
1881            .await
1882            .context("Recovery task completed and update receiver disconnected, but the desired modules are still unavailable or failed to recover")?;
1883
1884        Ok(())
1885    }
1886
1887    pub async fn wait_for_all_active_state_machines(&self) -> anyhow::Result<()> {
1888        loop {
1889            if self.executor.get_active_states().await.is_empty() {
1890                break;
1891            }
1892            sleep(Duration::from_millis(100)).await;
1893        }
1894        Ok(())
1895    }
1896
1897    /// Set the client [`Metadata`]
1898    pub async fn set_metadata_dbtx(dbtx: &mut DatabaseTransaction<'_>, metadata: &Metadata) {
1899        dbtx.insert_new_entry(&ClientMetadataKey, metadata).await;
1900    }
1901
1902    fn spawn_module_recoveries_task(
1903        &self,
1904        recovery_sender: watch::Sender<BTreeMap<ModuleInstanceId, RecoveryProgress>>,
1905        module_recoveries: BTreeMap<
1906            ModuleInstanceId,
1907            Pin<Box<maybe_add_send!(dyn Future<Output = anyhow::Result<()>>)>>,
1908        >,
1909        module_recovery_progress_receivers: BTreeMap<
1910            ModuleInstanceId,
1911            watch::Receiver<RecoveryProgress>,
1912        >,
1913    ) {
1914        let db = self.db.clone();
1915        let log_ordering_wakeup_tx = self.log_ordering_wakeup_tx.clone();
1916        let module_kinds: BTreeMap<ModuleInstanceId, String> = self
1917            .modules
1918            .iter_modules_id_kind()
1919            .map(|(id, kind)| (id, kind.to_string()))
1920            .collect();
1921        self.spawn("module recoveries", |_task_handle| async {
1922            Self::run_module_recoveries_task(
1923                db,
1924                log_ordering_wakeup_tx,
1925                recovery_sender,
1926                module_recoveries,
1927                module_recovery_progress_receivers,
1928                module_kinds,
1929            )
1930            .await;
1931        });
1932    }
1933
1934    async fn run_module_recoveries_task(
1935        db: Database,
1936        log_ordering_wakeup_tx: watch::Sender<()>,
1937        recovery_sender: watch::Sender<BTreeMap<ModuleInstanceId, RecoveryProgress>>,
1938        module_recoveries: BTreeMap<
1939            ModuleInstanceId,
1940            Pin<Box<maybe_add_send!(dyn Future<Output = anyhow::Result<()>>)>>,
1941        >,
1942        module_recovery_progress_receivers: BTreeMap<
1943            ModuleInstanceId,
1944            watch::Receiver<RecoveryProgress>,
1945        >,
1946        module_kinds: BTreeMap<ModuleInstanceId, String>,
1947    ) {
1948        debug!(target: LOG_CLIENT_RECOVERY, num_modules=%module_recovery_progress_receivers.len(), "Staring module recoveries");
1949        let mut completed_stream = Vec::new();
1950        let progress_stream = futures::stream::FuturesUnordered::new();
1951
1952        for (module_instance_id, f) in module_recoveries {
1953            completed_stream.push(futures::stream::once(Box::pin(async move {
1954                match f.await {
1955                    Ok(()) => (module_instance_id, None),
1956                    Err(err) => {
1957                        warn!(
1958                            target: LOG_CLIENT,
1959                            err = %err.fmt_compact_anyhow(), module_instance_id, "Module recovery failed"
1960                        );
1961                        // a module recovery that failed reports and error and
1962                        // just never finishes, so we don't need a separate state
1963                        // for it
1964                        futures::future::pending::<()>().await;
1965                        unreachable!()
1966                    }
1967                }
1968            })));
1969        }
1970
1971        for (module_instance_id, rx) in module_recovery_progress_receivers {
1972            progress_stream.push(
1973                tokio_stream::wrappers::WatchStream::new(rx)
1974                    .fuse()
1975                    .map(move |progress| (module_instance_id, Some(progress))),
1976            );
1977        }
1978
1979        let mut futures = futures::stream::select(
1980            futures::stream::select_all(progress_stream),
1981            futures::stream::select_all(completed_stream),
1982        );
1983
1984        while let Some((module_instance_id, progress)) = futures.next().await {
1985            let mut dbtx = db.begin_transaction().await;
1986
1987            let prev_progress = *recovery_sender
1988                .borrow()
1989                .get(&module_instance_id)
1990                .expect("existing progress must be present");
1991
1992            let progress = if prev_progress.is_done() {
1993                // since updates might be out of order, once done, stick with it
1994                prev_progress
1995            } else if let Some(progress) = progress {
1996                progress
1997            } else {
1998                prev_progress.to_complete()
1999            };
2000
2001            if !prev_progress.is_done() && progress.is_done() {
2002                info!(
2003                    target: LOG_CLIENT,
2004                    module_instance_id,
2005                    progress = format!("{}/{}", progress.complete, progress.total),
2006                    "Recovery complete"
2007                );
2008                dbtx.log_event(
2009                    log_ordering_wakeup_tx.clone(),
2010                    None,
2011                    ModuleRecoveryCompleted {
2012                        module_id: module_instance_id,
2013                    },
2014                )
2015                .await;
2016            } else {
2017                info!(
2018                    target: LOG_CLIENT,
2019                    module_instance_id,
2020                    kind = module_kinds.get(&module_instance_id).map(String::as_str).unwrap_or("unknown"),
2021                    progress = format!("{}/{}", progress.complete, progress.total),
2022                    "Recovery progress"
2023                );
2024            }
2025
2026            dbtx.insert_entry(
2027                &ClientModuleRecovery { module_instance_id },
2028                &ClientModuleRecoveryState { progress },
2029            )
2030            .await;
2031            dbtx.commit_tx().await;
2032
2033            recovery_sender.send_modify(|v| {
2034                v.insert(module_instance_id, progress);
2035            });
2036        }
2037        debug!(target: LOG_CLIENT_RECOVERY, "Recovery executor stopped");
2038    }
2039
2040    async fn load_peers_last_api_versions(
2041        db: &Database,
2042        num_peers: NumPeers,
2043    ) -> BTreeMap<PeerId, SupportedApiVersionsSummary> {
2044        let mut peer_api_version_sets = BTreeMap::new();
2045
2046        let mut dbtx = db.begin_transaction_nc().await;
2047        for peer_id in num_peers.peer_ids() {
2048            if let Some(v) = dbtx
2049                .get_value(&PeerLastApiVersionsSummaryKey(peer_id))
2050                .await
2051            {
2052                peer_api_version_sets.insert(peer_id, v.0);
2053            }
2054        }
2055        drop(dbtx);
2056        peer_api_version_sets
2057    }
2058
2059    /// You likely want to use [`Client::get_peer_urls`]. This function returns
2060    /// only the announcements and doesn't use the config as fallback.
2061    pub async fn get_peer_url_announcements(&self) -> BTreeMap<PeerId, SignedApiAnnouncement> {
2062        self.db()
2063            .begin_transaction_nc()
2064            .await
2065            .find_by_prefix(&ApiAnnouncementPrefix)
2066            .await
2067            .map(|(announcement_key, announcement)| (announcement_key.0, announcement))
2068            .collect()
2069            .await
2070    }
2071
2072    /// Returns guardian metadata stored in the client database
2073    pub async fn get_guardian_metadata(
2074        &self,
2075    ) -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
2076        self.db()
2077            .begin_transaction_nc()
2078            .await
2079            .find_by_prefix(&crate::guardian_metadata::GuardianMetadataPrefix)
2080            .await
2081            .map(|(key, metadata)| (key.0, metadata))
2082            .collect()
2083            .await
2084    }
2085
2086    /// Returns a list of guardian API URLs
2087    pub async fn get_peer_urls(&self) -> BTreeMap<PeerId, SafeUrl> {
2088        get_api_urls(&self.db, &self.config().await).await
2089    }
2090
2091    /// Create an invite code with the api endpoint of the given peer which can
2092    /// be used to download this client config
2093    pub async fn invite_code(&self, peer: PeerId) -> Option<InviteCode> {
2094        self.get_peer_urls()
2095            .await
2096            .into_iter()
2097            .find_map(|(peer_id, url)| (peer == peer_id).then_some(url))
2098            .map(|peer_url| {
2099                InviteCode::new(
2100                    peer_url.clone(),
2101                    peer,
2102                    self.federation_id(),
2103                    self.api_secret.clone(),
2104                )
2105            })
2106    }
2107
2108    /// Blocks till the client has synced the guardian public key set
2109    /// (introduced in version 0.4) and returns it. Once it has been fetched
2110    /// once this function is guaranteed to return immediately.
2111    pub async fn get_guardian_public_keys_blocking(
2112        &self,
2113    ) -> BTreeMap<PeerId, fedimint_core::secp256k1::PublicKey> {
2114        self.db
2115            .autocommit(
2116                |dbtx, _| {
2117                    Box::pin(async move {
2118                        let config = self.config().await;
2119
2120                        let guardian_pub_keys = self
2121                            .get_or_backfill_broadcast_public_keys(dbtx, config)
2122                            .await;
2123
2124                        Result::<_, ()>::Ok(guardian_pub_keys)
2125                    })
2126                },
2127                None,
2128            )
2129            .await
2130            .expect("Will retry forever")
2131    }
2132
2133    async fn get_or_backfill_broadcast_public_keys(
2134        &self,
2135        dbtx: &mut DatabaseTransaction<'_>,
2136        config: ClientConfig,
2137    ) -> BTreeMap<PeerId, PublicKey> {
2138        match config.global.broadcast_public_keys {
2139            Some(guardian_pub_keys) => guardian_pub_keys,
2140            _ => {
2141                let (guardian_pub_keys, new_config) = self.fetch_and_update_config(config).await;
2142
2143                dbtx.insert_entry(&ClientConfigKey, &new_config).await;
2144                *(self.config.write().await) = new_config;
2145                guardian_pub_keys
2146            }
2147        }
2148    }
2149
2150    async fn fetch_session_count(&self) -> FederationResult<u64> {
2151        self.api.session_count().await
2152    }
2153
2154    async fn fetch_and_update_config(
2155        &self,
2156        config: ClientConfig,
2157    ) -> (BTreeMap<PeerId, PublicKey>, ClientConfig) {
2158        let fetched_config = retry(
2159            "Fetching guardian public keys",
2160            backoff_util::background_backoff(),
2161            || async {
2162                Ok(self
2163                    .api
2164                    .request_current_consensus::<ClientConfig>(
2165                        CLIENT_CONFIG_ENDPOINT.to_owned(),
2166                        ApiRequestErased::default(),
2167                    )
2168                    .await?)
2169            },
2170        )
2171        .await
2172        .expect("Will never return on error");
2173
2174        let Some(guardian_pub_keys) = fetched_config.global.broadcast_public_keys else {
2175            warn!(
2176                target: LOG_CLIENT,
2177                "Guardian public keys not found in fetched config, server not updated to 0.4 yet"
2178            );
2179            pending::<()>().await;
2180            unreachable!("Pending will never return");
2181        };
2182
2183        let new_config = ClientConfig {
2184            global: GlobalClientConfig {
2185                broadcast_public_keys: Some(guardian_pub_keys.clone()),
2186                ..config.global
2187            },
2188            modules: config.modules,
2189        };
2190        (guardian_pub_keys, new_config)
2191    }
2192
2193    pub fn handle_global_rpc(
2194        &self,
2195        method: String,
2196        params: serde_json::Value,
2197    ) -> BoxStream<'_, anyhow::Result<serde_json::Value>> {
2198        Box::pin(try_stream! {
2199            match method.as_str() {
2200                "get_balance" => {
2201                    let balance = self.get_balance_for_btc().await.unwrap_or_default();
2202                    yield serde_json::to_value(balance)?;
2203                }
2204                "subscribe_balance_changes" => {
2205                    let req: GetBalanceChangesRequest= serde_json::from_value(params)?;
2206                    let mut stream = self.subscribe_balance_changes(req.unit).await;
2207                    while let Some(balance) = stream.next().await {
2208                        yield serde_json::to_value(balance)?;
2209                    }
2210                }
2211                "get_config" => {
2212                    let config = self.config().await;
2213                    yield serde_json::to_value(config)?;
2214                }
2215                "get_federation_id" => {
2216                    let federation_id = self.federation_id();
2217                    yield serde_json::to_value(federation_id)?;
2218                }
2219                "get_invite_code" => {
2220                    let req: GetInviteCodeRequest = serde_json::from_value(params)?;
2221                    let invite_code = self.invite_code(req.peer).await;
2222                    yield serde_json::to_value(invite_code)?;
2223                }
2224                "get_operation" => {
2225                    let req: GetOperationIdRequest = serde_json::from_value(params)?;
2226                    let operation = self.operation_log().get_operation(req.operation_id).await;
2227                    yield serde_json::to_value(operation)?;
2228                }
2229                "list_operations" => {
2230                    let req: ListOperationsParams = serde_json::from_value(params)?;
2231                    let limit = if req.limit.is_none() && req.last_seen.is_none() {
2232                        usize::MAX
2233                    } else {
2234                        req.limit.unwrap_or(usize::MAX)
2235                    };
2236                    let operations = self.operation_log()
2237                        .paginate_operations_rev(limit, req.last_seen)
2238                        .await;
2239                    yield serde_json::to_value(operations)?;
2240                }
2241                "get_event_log" => {
2242                    let req: GetEventLogRequest = serde_json::from_value(params)?;
2243                    let limit = req
2244                        .limit
2245                        .unwrap_or(DEFAULT_EVENT_LOG_PAGE_SIZE)
2246                        .min(MAX_EVENT_LOG_PAGE_SIZE);
2247                    let events = self.get_event_log(req.pos, limit).await;
2248                    yield serde_json::to_value(events)?;
2249                }
2250                "session_count" => {
2251                    let count = self.fetch_session_count().await?;
2252                    yield serde_json::to_value(count)?;
2253                }
2254                "has_pending_recoveries" => {
2255                    let has_pending = self.has_pending_recoveries();
2256                    yield serde_json::to_value(has_pending)?;
2257                }
2258                "wait_for_all_recoveries" => {
2259                    self.wait_for_all_recoveries().await?;
2260                    yield serde_json::Value::Null;
2261                }
2262                "subscribe_to_recovery_progress" => {
2263                    let mut stream = self.subscribe_to_recovery_progress();
2264                    while let Some((module_id, progress)) = stream.next().await {
2265                        yield serde_json::json!({
2266                            "module_id": module_id,
2267                            "progress": progress
2268                        });
2269                    }
2270                }
2271                #[allow(deprecated)]
2272                "backup_to_federation" => {
2273                    let metadata = if params.is_null() {
2274                        Metadata::from_json_serialized(serde_json::json!({}))
2275                    } else {
2276                        Metadata::from_json_serialized(params)
2277                    };
2278                    self.backup_to_federation(metadata).await?;
2279                    yield serde_json::Value::Null;
2280                }
2281                _ => {
2282                    Err(anyhow::format_err!("Unknown method: {}", method))?;
2283                    unreachable!()
2284                },
2285            }
2286        })
2287    }
2288
2289    pub async fn log_event<E>(&self, module_id: Option<ModuleInstanceId>, event: E)
2290    where
2291        E: Event + Send,
2292    {
2293        let mut dbtx = self.db.begin_transaction().await;
2294        self.log_event_dbtx(&mut dbtx, module_id, event).await;
2295        dbtx.commit_tx().await;
2296    }
2297
2298    pub async fn log_event_dbtx<E, Cap>(
2299        &self,
2300        dbtx: &mut DatabaseTransaction<'_, Cap>,
2301        module_id: Option<ModuleInstanceId>,
2302        event: E,
2303    ) where
2304        E: Event + Send,
2305        Cap: Send,
2306    {
2307        dbtx.log_event(self.log_ordering_wakeup_tx.clone(), module_id, event)
2308            .await;
2309    }
2310
2311    pub async fn log_event_raw_dbtx<Cap>(
2312        &self,
2313        dbtx: &mut DatabaseTransaction<'_, Cap>,
2314        kind: EventKind,
2315        module: Option<(ModuleKind, ModuleInstanceId)>,
2316        payload: Vec<u8>,
2317        persist: EventPersistence,
2318    ) where
2319        Cap: Send,
2320    {
2321        let module_id = module.as_ref().map(|m| m.1);
2322        let module_kind = module.map(|m| m.0);
2323        dbtx.log_event_raw(
2324            self.log_ordering_wakeup_tx.clone(),
2325            kind,
2326            module_kind,
2327            module_id,
2328            payload,
2329            persist,
2330        )
2331        .await;
2332    }
2333
2334    /// Built in event log (trimmable) tracker
2335    ///
2336    /// For the convenience of downstream applications, [`Client`] can store
2337    /// internally event log position for the main application using/driving it.
2338    ///
2339    /// Note that this position is a singleton, so this tracker should not be
2340    /// used for multiple purposes or applications, etc. at the same time.
2341    ///
2342    /// If the application has a need to follow log using multiple trackers, it
2343    /// should implement own [`DynEventLogTrimableTracker`] and store its
2344    /// persient data by itself.
2345    pub fn built_in_application_event_log_tracker(&self) -> DynEventLogTrimableTracker {
2346        struct BuiltInApplicationEventLogTracker;
2347
2348        #[apply(async_trait_maybe_send!)]
2349        impl EventLogTrimableTracker for BuiltInApplicationEventLogTracker {
2350            // Store position in the event log
2351            async fn store(
2352                &mut self,
2353                dbtx: &mut DatabaseTransaction<NonCommittable>,
2354                pos: EventLogTrimableId,
2355            ) -> anyhow::Result<()> {
2356                dbtx.insert_entry(&DefaultApplicationEventLogKey, &pos)
2357                    .await;
2358                Ok(())
2359            }
2360
2361            /// Load the last previous stored position (or None if never stored)
2362            async fn load(
2363                &mut self,
2364                dbtx: &mut DatabaseTransaction<NonCommittable>,
2365            ) -> anyhow::Result<Option<EventLogTrimableId>> {
2366                Ok(dbtx.get_value(&DefaultApplicationEventLogKey).await)
2367            }
2368        }
2369        Box::new(BuiltInApplicationEventLogTracker)
2370    }
2371
2372    /// Like [`Self::handle_events`] but for historical data.
2373    ///
2374    ///
2375    /// This function can be used to process subset of events
2376    /// that is infrequent and important enough to be persisted
2377    /// forever. Most applications should prefer to use [`Self::handle_events`]
2378    /// which emits *all* events.
2379    pub async fn handle_historical_events<F, R>(
2380        &self,
2381        tracker: fedimint_eventlog::DynEventLogTracker,
2382        handler_fn: F,
2383    ) -> anyhow::Result<()>
2384    where
2385        F: Fn(&mut DatabaseTransaction<NonCommittable>, EventLogEntry) -> R,
2386        R: Future<Output = anyhow::Result<()>>,
2387    {
2388        fedimint_eventlog::handle_events(
2389            self.db.clone(),
2390            tracker,
2391            self.log_event_added_rx.clone(),
2392            handler_fn,
2393        )
2394        .await
2395    }
2396
2397    /// Handle events emitted by the client
2398    ///
2399    /// This is a preferred method for reactive & asynchronous
2400    /// processing of events emitted by the client.
2401    ///
2402    /// It needs a `tracker` that will persist the position in the log
2403    /// as it is being handled. You can use the
2404    /// [`Client::built_in_application_event_log_tracker`] if this call is
2405    /// used for the single main application handling this instance of the
2406    /// [`Client`]. Otherwise you should implement your own tracker.
2407    ///
2408    /// This handler will call `handle_fn` with ever event emitted by
2409    /// [`Client`], including transient ones. The caller should atomically
2410    /// handle each event it is interested in and ignore other ones.
2411    ///
2412    /// This method returns only when client is shutting down or on internal
2413    /// error, so typically should be called in a background task dedicated
2414    /// to handling events.
2415    pub async fn handle_events<F, R>(
2416        &self,
2417        tracker: fedimint_eventlog::DynEventLogTrimableTracker,
2418        handler_fn: F,
2419    ) -> anyhow::Result<()>
2420    where
2421        F: Fn(&mut DatabaseTransaction<NonCommittable>, EventLogEntry) -> R,
2422        R: Future<Output = anyhow::Result<()>>,
2423    {
2424        fedimint_eventlog::handle_trimable_events(
2425            self.db.clone(),
2426            tracker,
2427            self.log_event_added_rx.clone(),
2428            handler_fn,
2429        )
2430        .await
2431    }
2432
2433    pub async fn get_event_log(
2434        &self,
2435        pos: Option<EventLogId>,
2436        limit: u64,
2437    ) -> Vec<PersistedLogEntry> {
2438        self.get_event_log_dbtx(&mut self.db.begin_transaction_nc().await, pos, limit)
2439            .await
2440    }
2441
2442    /// Returns the id that the next entry appended to the event log will be
2443    /// assigned, i.e. the position just past the current end of the log.
2444    pub async fn get_next_event_log_id(&self) -> EventLogId {
2445        self.db
2446            .begin_transaction_nc()
2447            .await
2448            .get_next_event_log_id()
2449            .await
2450    }
2451
2452    pub async fn get_event_log_trimable(
2453        &self,
2454        pos: Option<EventLogTrimableId>,
2455        limit: u64,
2456    ) -> Vec<PersistedLogEntry> {
2457        self.get_event_log_trimable_dbtx(&mut self.db.begin_transaction_nc().await, pos, limit)
2458            .await
2459    }
2460
2461    pub async fn get_event_log_dbtx<Cap>(
2462        &self,
2463        dbtx: &mut DatabaseTransaction<'_, Cap>,
2464        pos: Option<EventLogId>,
2465        limit: u64,
2466    ) -> Vec<PersistedLogEntry>
2467    where
2468        Cap: Send,
2469    {
2470        dbtx.get_event_log(pos, limit).await
2471    }
2472
2473    pub async fn get_event_log_trimable_dbtx<Cap>(
2474        &self,
2475        dbtx: &mut DatabaseTransaction<'_, Cap>,
2476        pos: Option<EventLogTrimableId>,
2477        limit: u64,
2478    ) -> Vec<PersistedLogEntry>
2479    where
2480        Cap: Send,
2481    {
2482        dbtx.get_event_log_trimable(pos, limit).await
2483    }
2484
2485    /// Register to receiver all new transient (unpersisted) events
2486    pub fn get_event_log_transient_receiver(&self) -> broadcast::Receiver<EventLogEntry> {
2487        self.log_event_added_transient_tx.subscribe()
2488    }
2489
2490    /// Get a receiver that signals when new events are added to the event log
2491    pub fn log_event_added_rx(&self) -> watch::Receiver<()> {
2492        self.log_event_added_rx.clone()
2493    }
2494
2495    pub fn iroh_enable_dht(&self) -> bool {
2496        self.iroh_enable_dht
2497    }
2498
2499    pub(crate) async fn run_core_migrations(
2500        db_no_decoders: &Database,
2501    ) -> Result<(), anyhow::Error> {
2502        let mut dbtx = db_no_decoders.begin_transaction().await;
2503        apply_migrations_core_client_dbtx(&mut dbtx.to_ref_nc(), "fedimint-client".to_string())
2504            .await?;
2505        if is_running_in_test_env() {
2506            verify_client_db_integrity_dbtx(&mut dbtx.to_ref_nc()).await;
2507        }
2508        dbtx.commit_tx_result().await?;
2509        Ok(())
2510    }
2511
2512    /// Iterator over primary modules for a given `unit`
2513    fn primary_modules_for_unit(
2514        &self,
2515        unit: AmountUnit,
2516    ) -> impl Iterator<Item = (ModuleInstanceId, &DynClientModule)> {
2517        self.primary_modules
2518            .iter()
2519            .flat_map(move |(_prio, candidates)| {
2520                candidates
2521                    .specific
2522                    .get(&unit)
2523                    .into_iter()
2524                    .flatten()
2525                    .copied()
2526                    // within same priority, wildcard matches come last
2527                    .chain(candidates.wildcard.iter().copied())
2528            })
2529            .map(|id| (id, self.modules.get_expect(id)))
2530    }
2531
2532    /// Primary module to use for `unit`
2533    ///
2534    /// Currently, just pick the first (highest priority) match
2535    pub fn primary_module_for_unit(
2536        &self,
2537        unit: AmountUnit,
2538    ) -> Option<(ModuleInstanceId, &DynClientModule)> {
2539        self.primary_modules_for_unit(unit).next()
2540    }
2541
2542    /// [`Self::primary_module_for_unit`] for Bitcoin
2543    pub fn primary_module_for_btc(&self) -> (ModuleInstanceId, &DynClientModule) {
2544        self.primary_module_for_unit(AmountUnit::BITCOIN)
2545            .expect("No primary module for Bitcoin")
2546    }
2547}
2548
2549#[apply(async_trait_maybe_send!)]
2550impl ClientContextIface for Client {
2551    fn get_module(&self, instance: ModuleInstanceId) -> &maybe_add_send_sync!(dyn IClientModule) {
2552        Client::get_module(self, instance)
2553    }
2554
2555    fn api_clone(&self) -> DynGlobalApi {
2556        Client::api_clone(self)
2557    }
2558    fn decoders(&self) -> &ModuleDecoderRegistry {
2559        Client::decoders(self)
2560    }
2561
2562    async fn finalize_and_submit_transaction(
2563        &self,
2564        operation_id: OperationId,
2565        operation_type: &str,
2566        operation_meta_gen: Box<maybe_add_send_sync!(dyn Fn(OutPointRange) -> serde_json::Value)>,
2567        tx_builder: TransactionBuilder,
2568    ) -> anyhow::Result<OutPointRange> {
2569        Client::finalize_and_submit_transaction(
2570            self,
2571            operation_id,
2572            operation_type,
2573            // |out_point_range| operation_meta_gen(out_point_range),
2574            &operation_meta_gen,
2575            tx_builder,
2576        )
2577        .await
2578    }
2579
2580    async fn finalize_and_submit_transaction_dbtx(
2581        &self,
2582        dbtx: &mut DatabaseTransaction<'_>,
2583        operation_id: OperationId,
2584        operation_type: &str,
2585        operation_meta_gen: Box<maybe_add_send_sync!(dyn Fn(OutPointRange) -> serde_json::Value)>,
2586        tx_builder: TransactionBuilder,
2587    ) -> anyhow::Result<OutPointRange> {
2588        Client::finalize_and_submit_transaction_dbtx(
2589            self,
2590            dbtx,
2591            operation_id,
2592            operation_type,
2593            &operation_meta_gen,
2594            tx_builder,
2595        )
2596        .await
2597    }
2598
2599    async fn finalize_and_submit_transaction_inner(
2600        &self,
2601        dbtx: &mut DatabaseTransaction<'_>,
2602        operation_id: OperationId,
2603        tx_builder: TransactionBuilder,
2604    ) -> anyhow::Result<OutPointRange> {
2605        Client::finalize_and_submit_transaction_inner(self, dbtx, operation_id, tx_builder).await
2606    }
2607
2608    async fn fee_quote(
2609        &self,
2610        operation_id: OperationId,
2611        request: FeeQuoteRequest,
2612    ) -> anyhow::Result<FeeQuote> {
2613        Client::fee_quote(self, operation_id, request).await
2614    }
2615
2616    async fn transaction_updates(&self, operation_id: OperationId) -> TransactionUpdates {
2617        Client::transaction_updates(self, operation_id).await
2618    }
2619
2620    async fn await_primary_module_outputs(
2621        &self,
2622        operation_id: OperationId,
2623        // TODO: make `impl Iterator<Item = ...>`
2624        outputs: Vec<OutPoint>,
2625    ) -> anyhow::Result<()> {
2626        Client::await_primary_bitcoin_module_outputs(self, operation_id, outputs).await
2627    }
2628
2629    fn operation_log(&self) -> &dyn IOperationLog {
2630        Client::operation_log(self)
2631    }
2632
2633    async fn has_active_states(&self, operation_id: OperationId) -> bool {
2634        Client::has_active_states(self, operation_id).await
2635    }
2636
2637    async fn operation_exists(&self, operation_id: OperationId) -> bool {
2638        Client::operation_exists(self, operation_id).await
2639    }
2640
2641    async fn config(&self) -> ClientConfig {
2642        Client::config(self).await
2643    }
2644
2645    fn db(&self) -> &Database {
2646        Client::db(self)
2647    }
2648
2649    fn executor(&self) -> &(maybe_add_send_sync!(dyn IExecutor + 'static)) {
2650        Client::executor(self)
2651    }
2652
2653    async fn invite_code(&self, peer: PeerId) -> Option<InviteCode> {
2654        Client::invite_code(self, peer).await
2655    }
2656
2657    fn get_internal_payment_markers(&self) -> anyhow::Result<(PublicKey, u64)> {
2658        Client::get_internal_payment_markers(self)
2659    }
2660
2661    async fn log_event_json(
2662        &self,
2663        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
2664        module_kind: Option<ModuleKind>,
2665        module_id: ModuleInstanceId,
2666        kind: EventKind,
2667        payload: serde_json::Value,
2668        persist: EventPersistence,
2669    ) {
2670        dbtx.ensure_global()
2671            .expect("Must be called with global dbtx");
2672        self.log_event_raw_dbtx(
2673            dbtx,
2674            kind,
2675            module_kind.map(|kind| (kind, module_id)),
2676            serde_json::to_vec(&payload).expect("Serialization can't fail"),
2677            persist,
2678        )
2679        .await;
2680    }
2681
2682    async fn read_operation_active_states<'dbtx>(
2683        &self,
2684        operation_id: OperationId,
2685        module_id: ModuleInstanceId,
2686        dbtx: &'dbtx mut DatabaseTransaction<'_>,
2687    ) -> Pin<Box<maybe_add_send!(dyn Stream<Item = (ActiveStateKey, ActiveStateMeta)> + 'dbtx)>>
2688    {
2689        Box::pin(
2690            dbtx.find_by_prefix(&ActiveModuleOperationStateKeyPrefix {
2691                operation_id,
2692                module_instance: module_id,
2693            })
2694            .await
2695            .map(move |(k, v)| (k.0, v)),
2696        )
2697    }
2698    async fn read_operation_inactive_states<'dbtx>(
2699        &self,
2700        operation_id: OperationId,
2701        module_id: ModuleInstanceId,
2702        dbtx: &'dbtx mut DatabaseTransaction<'_>,
2703    ) -> Pin<Box<maybe_add_send!(dyn Stream<Item = (InactiveStateKey, InactiveStateMeta)> + 'dbtx)>>
2704    {
2705        Box::pin(
2706            dbtx.find_by_prefix(&InactiveModuleOperationStateKeyPrefix {
2707                operation_id,
2708                module_instance: module_id,
2709            })
2710            .await
2711            .map(move |(k, v)| (k.0, v)),
2712        )
2713    }
2714}
2715
2716// TODO: impl `Debug` for `Client` and derive here
2717impl fmt::Debug for Client {
2718    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2719        write!(f, "Client")
2720    }
2721}
2722
2723pub fn client_decoders<'a>(
2724    registry: &ModuleInitRegistry<DynClientModuleInit>,
2725    module_kinds: impl Iterator<Item = (ModuleInstanceId, &'a ModuleKind)>,
2726) -> ModuleDecoderRegistry {
2727    let mut modules = BTreeMap::new();
2728    for (id, kind) in module_kinds {
2729        let Some(init) = registry.get(kind) else {
2730            debug!("Detected configuration for unsupported module id: {id}, kind: {kind}");
2731            continue;
2732        };
2733
2734        modules.insert(
2735            id,
2736            (
2737                kind.clone(),
2738                IClientModuleInit::decoder(AsRef::<dyn IClientModuleInit + 'static>::as_ref(init)),
2739            ),
2740        );
2741    }
2742    ModuleDecoderRegistry::from(modules)
2743}