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