Skip to main content

fedimint_wallet_client/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7
8#[cfg(feature = "uniffi")]
9::uniffi::setup_scaffolding!();
10
11pub mod api;
12#[cfg(feature = "cli")]
13mod cli;
14
15mod backup;
16
17pub mod client_db;
18/// Legacy, state-machine based peg-ins, replaced by `pegin_monitor`
19/// but retained for time being to ensure existing peg-ins complete.
20mod deposit;
21pub mod events;
22use events::SendPaymentEvent;
23#[cfg(feature = "uniffi")]
24pub mod ffi;
25/// Peg-in monitor: a task monitoring deposit addresses for peg-ins.
26mod pegin_monitor;
27mod withdraw;
28
29use std::collections::{BTreeMap, BTreeSet};
30use std::future;
31use std::sync::Arc;
32use std::time::{Duration, SystemTime};
33
34use anyhow::{Context as AnyhowContext, anyhow, bail, ensure};
35use async_stream::{stream, try_stream};
36use backup::WalletModuleBackup;
37use bitcoin::address::NetworkUnchecked;
38use bitcoin::secp256k1::{All, SECP256K1, Secp256k1};
39use bitcoin::{Address, Network, ScriptBuf};
40use client_db::{DbKeyPrefix, PegInTweakIndexKey, SupportsSafeDepositKey, TweakIdx};
41use fedimint_api_client::api::{DynModuleApi, FederationResult};
42use fedimint_bitcoind::{BitcoindTracked, DynBitcoindRpc, IBitcoindRpc, create_esplora_rpc};
43use fedimint_client_module::module::init::{
44    ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs,
45};
46use fedimint_client_module::module::recovery::RecoveryProgress;
47use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
48use fedimint_client_module::oplog::UpdateStreamOrOutcome;
49use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
50use fedimint_client_module::transaction::{
51    ClientOutput, ClientOutputBundle, ClientOutputSM, FeeQuote, FeeQuoteRequest, TransactionBuilder,
52};
53use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
54use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
55use fedimint_core::db::{
56    AutocommitError, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
57};
58use fedimint_core::encoding::{Decodable, Encodable};
59use fedimint_core::envs::{BitcoinRpcConfig, is_running_in_test_env};
60use fedimint_core::module::{
61    Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleConsensusVersion,
62    ModuleInit, MultiApiVersion,
63};
64use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup, sleep};
65use fedimint_core::util::backoff_util::background_backoff;
66use fedimint_core::util::{BoxStream, backoff_util, retry};
67use fedimint_core::{
68    BitcoinHash, OutPoint, TransactionId, apply, async_trait_maybe_send, push_db_pair_items,
69    runtime, secp256k1,
70};
71use fedimint_derive_secret::{ChildId, DerivableSecret};
72use fedimint_logging::LOG_CLIENT_MODULE_WALLET;
73pub use fedimint_wallet_common as common;
74use fedimint_wallet_common::config::{FeeConsensus, WalletClientConfig};
75use fedimint_wallet_common::tweakable::Tweakable;
76pub use fedimint_wallet_common::*;
77use futures::{Stream, StreamExt};
78use rand::{Rng, thread_rng};
79use secp256k1::Keypair;
80use serde::{Deserialize, Serialize};
81use strum::IntoEnumIterator;
82use tokio::sync::watch;
83use tracing::{debug, instrument};
84
85use crate::api::WalletFederationApi;
86use crate::backup::{FEDERATION_RECOVER_MAX_GAP, RecoveryStateV2, WalletRecovery};
87use crate::client_db::{
88    ClaimedPegInData, ClaimedPegInKey, ClaimedPegInPrefix, NextPegInTweakIndexKey,
89    PegInPoolCursorKey, PegInTweakIndexData, PegInTweakIndexPrefix, RecoveryFinalizedKey,
90    RecoveryStateKey, SupportsSafeDepositPrefix,
91};
92use crate::deposit::DepositStateMachine;
93use crate::withdraw::{CreatedWithdrawState, WithdrawStateMachine, WithdrawStates};
94
95const WALLET_TWEAK_CHILD_ID: ChildId = ChildId(0);
96
97#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
98pub struct BitcoinTransactionData {
99    /// The bitcoin transaction is saved as soon as we see it so the transaction
100    /// can be re-transmitted if it's evicted from the mempool.
101    pub btc_transaction: bitcoin::Transaction,
102    /// Index of the deposit output
103    pub out_idx: u32,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
107pub enum DepositStateV1 {
108    WaitingForTransaction,
109    WaitingForConfirmation(BitcoinTransactionData),
110    Confirmed(BitcoinTransactionData),
111    Claimed(BitcoinTransactionData),
112    Failed(String),
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
116#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
117pub enum DepositStateV2 {
118    WaitingForTransaction,
119    WaitingForConfirmation {
120        #[serde(with = "bitcoin::amount::serde::as_sat")]
121        btc_deposited: bitcoin::Amount,
122        btc_out_point: bitcoin::OutPoint,
123    },
124    Confirmed {
125        #[serde(with = "bitcoin::amount::serde::as_sat")]
126        btc_deposited: bitcoin::Amount,
127        btc_out_point: bitcoin::OutPoint,
128    },
129    Claimed {
130        #[serde(with = "bitcoin::amount::serde::as_sat")]
131        btc_deposited: bitcoin::Amount,
132        btc_out_point: bitcoin::OutPoint,
133    },
134    Failed(String),
135}
136
137/// Deposit address allocated by this client.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct DepositAddressInfo {
140    pub operation_id: OperationId,
141    pub address: Address,
142    pub tweak_idx: TweakIdx,
143}
144
145/// Result of [`WalletClientModule::allocate_deposit_address_pooled_stateless`].
146///
147/// Callers that need a custom address-picking strategy can use
148/// [`MaybeNewAddress::TooManyUnusedAddresses`] to see all currently reusable
149/// addresses instead of relying on this crate's round-robin policy.
150#[allow(clippy::enum_variant_names)]
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum MaybeNewAddress {
153    /// A new tweak/operation was created on this call.
154    NewAddress(DepositAddressInfo),
155    /// The unused-address gap is full. No new address was allocated.
156    ///
157    /// Reusable unused addresses are ordered by `creation_time` ascending.
158    TooManyUnusedAddresses(Vec<DepositAddressInfo>),
159}
160
161/// Outcome of [`WalletClientModule::allocate_deposit_address_pooled`], so
162/// callers can decide whether to perform per-operation initialization (notes,
163/// fee bookkeeping, metadata writes) — for `Reused` returns, that work was
164/// already done at the time of the original `Fresh` allocation and must not be
165/// repeated.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum AllocateDepositOutcome {
168    /// A new tweak/operation was created on this call.
169    Fresh,
170    /// An existing unused address was returned. The carried `TweakIdx` is the
171    /// same as the one returned in the tuple and refers to the original
172    /// allocation; it's exposed here as well for explicit diagnostic use.
173    Reused { original_tweak_idx: TweakIdx },
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
177#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
178pub enum WithdrawState {
179    Created,
180    Succeeded(bitcoin::Txid),
181    Failed(String),
182    // TODO: track refund
183    // Refunded,
184    // RefundFailed(String),
185}
186
187async fn next_withdraw_state<S>(stream: &mut S) -> Option<WithdrawStates>
188where
189    S: Stream<Item = WalletClientStates> + Unpin,
190{
191    loop {
192        if let WalletClientStates::Withdraw(ds) = stream.next().await? {
193            return Some(ds.state);
194        }
195        tokio::task::yield_now().await;
196    }
197}
198
199#[derive(Debug, Clone, Default)]
200// TODO: should probably move to DB
201pub struct WalletClientInit(pub Option<DynBitcoindRpc>);
202
203const SLICE_SIZE: u64 = 1000;
204
205impl WalletClientInit {
206    pub fn new(rpc: DynBitcoindRpc) -> Self {
207        Self(Some(rpc))
208    }
209
210    async fn recover_from_slices(
211        &self,
212        args: &ClientModuleRecoverArgs<Self>,
213    ) -> anyhow::Result<Option<fedimint_core::Amount>> {
214        let data = WalletClientModuleData {
215            cfg: args.cfg().clone(),
216            module_root_secret: args.module_root_secret().clone(),
217        };
218
219        let total_items = args.module_api().fetch_recovery_count().await?;
220
221        let mut state = RecoveryStateV2::new();
222
223        state.refill_pending_pool_up_to(&data, TweakIdx(FEDERATION_RECOVER_MAX_GAP));
224
225        for start in (0..total_items).step_by(SLICE_SIZE as usize) {
226            let end = std::cmp::min(start + SLICE_SIZE, total_items);
227
228            let items = args.module_api().fetch_recovery_slice(start, end).await?;
229
230            for item in &items {
231                match item {
232                    RecoveryItem::Input { outpoint, script } => {
233                        state.handle_item(*outpoint, script, &data);
234                    }
235                }
236            }
237
238            args.update_recovery_progress(RecoveryProgress {
239                complete: end.try_into().unwrap_or(u32::MAX),
240                total: total_items.try_into().unwrap_or(u32::MAX),
241            });
242        }
243
244        let mut dbtx = args.db().begin_transaction().await;
245
246        for tweak_idx in 0..state.new_start_idx().0 {
247            let operation_id = data.derive_peg_in_script(TweakIdx(tweak_idx)).3;
248
249            let claimed = state
250                .claimed_outpoints
251                .get(&TweakIdx(tweak_idx))
252                .cloned()
253                .unwrap_or_default();
254
255            dbtx.insert_new_entry(
256                &PegInTweakIndexKey(TweakIdx(tweak_idx)),
257                &PegInTweakIndexData {
258                    operation_id,
259                    creation_time: fedimint_core::time::now(),
260                    last_check_time: None,
261                    next_check_time: Some(fedimint_core::time::now()),
262                    claimed,
263                },
264            )
265            .await;
266        }
267
268        dbtx.insert_new_entry(&NextPegInTweakIndexKey, &state.new_start_idx())
269            .await;
270
271        dbtx.commit_tx().await;
272
273        // The wallet only discovers which on-chain outputs belonged to the
274        // client during recovery; their value isn't known until the deposits
275        // are later claimed, so no amount is reported here.
276        Ok(None)
277    }
278}
279
280impl ModuleInit for WalletClientInit {
281    type Common = WalletCommonInit;
282
283    async fn dump_database(
284        &self,
285        dbtx: &mut DatabaseTransaction<'_>,
286        prefix_names: Vec<String>,
287    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
288        let mut wallet_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
289            BTreeMap::new();
290        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
291            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
292        });
293
294        for table in filtered_prefixes {
295            match table {
296                DbKeyPrefix::NextPegInTweakIndex => {
297                    if let Some(index) = dbtx.get_value(&NextPegInTweakIndexKey).await {
298                        wallet_client_items
299                            .insert("NextPegInTweakIndex".to_string(), Box::new(index));
300                    }
301                }
302                DbKeyPrefix::PegInTweakIndex => {
303                    push_db_pair_items!(
304                        dbtx,
305                        PegInTweakIndexPrefix,
306                        PegInTweakIndexKey,
307                        PegInTweakIndexData,
308                        wallet_client_items,
309                        "Peg-In Tweak Index"
310                    );
311                }
312                DbKeyPrefix::ClaimedPegIn => {
313                    push_db_pair_items!(
314                        dbtx,
315                        ClaimedPegInPrefix,
316                        ClaimedPegInKey,
317                        ClaimedPegInData,
318                        wallet_client_items,
319                        "Claimed Peg-In"
320                    );
321                }
322                DbKeyPrefix::RecoveryFinalized => {
323                    if let Some(val) = dbtx.get_value(&RecoveryFinalizedKey).await {
324                        wallet_client_items.insert("RecoveryFinalized".to_string(), Box::new(val));
325                    }
326                }
327                DbKeyPrefix::SupportsSafeDeposit => {
328                    push_db_pair_items!(
329                        dbtx,
330                        SupportsSafeDepositPrefix,
331                        SupportsSafeDepositKey,
332                        (),
333                        wallet_client_items,
334                        "Supports Safe Deposit"
335                    );
336                }
337                DbKeyPrefix::PegInPoolCursor => {
338                    if let Some(cursor) = dbtx.get_value(&PegInPoolCursorKey).await {
339                        wallet_client_items.insert("PegInPoolCursor".to_string(), Box::new(cursor));
340                    }
341                }
342                DbKeyPrefix::RecoveryState
343                | DbKeyPrefix::ExternalReservedStart
344                | DbKeyPrefix::CoreInternalReservedStart
345                | DbKeyPrefix::CoreInternalReservedEnd => {}
346            }
347        }
348
349        Box::new(wallet_client_items.into_iter())
350    }
351}
352
353#[apply(async_trait_maybe_send!)]
354impl ClientModuleInit for WalletClientInit {
355    type Module = WalletClientModule;
356
357    fn supported_api_versions(&self) -> MultiApiVersion {
358        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
359            .expect("no version conflicts")
360    }
361
362    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
363        let data = WalletClientModuleData {
364            cfg: args.cfg().clone(),
365            module_root_secret: args.module_root_secret().clone(),
366        };
367
368        let db = args.db().clone();
369
370        let rpc_config = WalletClientModule::get_rpc_config(args.cfg());
371
372        // Priority:
373        // 1. user-provided bitcoind RPC from ClientBuilder::with_bitcoind_rpc
374        // 2. user-provided no-chain-id factory from
375        //    ClientBuilder::with_bitcoind_rpc_no_chain_id
376        // 3. WalletClientInit constructor
377        // 4. create from config (esplora)
378        let btc_rpc = if let Some(user_rpc) = args.user_bitcoind_rpc() {
379            user_rpc.clone()
380        } else if let Some(factory) = args.user_bitcoind_rpc_no_chain_id() {
381            if let Some(rpc) = factory(rpc_config.url.clone()).await {
382                rpc
383            } else {
384                self.0
385                    .clone()
386                    .unwrap_or(create_esplora_rpc(&rpc_config.url)?)
387            }
388        } else {
389            self.0
390                .clone()
391                .unwrap_or(create_esplora_rpc(&rpc_config.url)?)
392        };
393        let btc_rpc = BitcoindTracked::new(btc_rpc, "wallet-client").into_dyn();
394
395        let module_api = args.module_api().clone();
396
397        let (pegin_claimed_sender, pegin_claimed_receiver) = watch::channel(());
398        let (pegin_monitor_wakeup_sender, pegin_monitor_wakeup_receiver) = watch::channel(());
399
400        Ok(WalletClientModule {
401            db,
402            data,
403            module_api,
404            notifier: args.notifier().clone(),
405            rpc: btc_rpc,
406            client_ctx: args.context(),
407            pegin_monitor_wakeup_sender,
408            pegin_monitor_wakeup_receiver,
409            pegin_claimed_receiver,
410            pegin_claimed_sender,
411            task_group: args.task_group().clone(),
412            client_span: args.client_span().clone(),
413            admin_auth: args.admin_auth().cloned(),
414        })
415    }
416
417    /// Wallet recovery
418    ///
419    /// Uses slice-based recovery if supported by the federation, otherwise
420    /// falls back to session-based history recovery.
421    async fn recover(
422        &self,
423        args: &ClientModuleRecoverArgs<Self>,
424        snapshot: Option<&<Self::Module as ClientModule>::Backup>,
425    ) -> anyhow::Result<Option<fedimint_core::Amount>> {
426        // Check if V1 (session-based) recovery state exists (resuming interrupted
427        // recovery)
428        if args
429            .db()
430            .begin_transaction_nc()
431            .await
432            .get_value(&RecoveryStateKey)
433            .await
434            .is_some()
435        {
436            return args
437                .recover_from_history::<WalletRecovery>(self, snapshot)
438                .await;
439        }
440
441        // Determine which method to use based on endpoint availability
442        if args.module_api().fetch_recovery_count().await.is_ok() {
443            self.recover_from_slices(args).await
444        } else {
445            args.recover_from_history::<WalletRecovery>(self, snapshot)
446                .await
447        }
448    }
449
450    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
451        Some(
452            DbKeyPrefix::iter()
453                .map(|p| p as u8)
454                .chain(
455                    DbKeyPrefix::ExternalReservedStart as u8
456                        ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
457                )
458                .collect(),
459        )
460    }
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct WalletOperationMeta {
465    pub variant: WalletOperationMetaVariant,
466    pub extra_meta: serde_json::Value,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
470#[serde(rename_all = "snake_case")]
471pub enum WalletOperationMetaVariant {
472    Deposit {
473        address: Address<NetworkUnchecked>,
474        /// Added in 0.4.2, can be `None` for old deposits or `Some` for ones
475        /// using the pegin monitor. The value is the child index of the key
476        /// used to generate the address, so we can re-generate the secret key
477        /// from our root secret.
478        #[serde(default)]
479        tweak_idx: Option<TweakIdx>,
480        #[serde(default, skip_serializing_if = "Option::is_none")]
481        expires_at: Option<SystemTime>,
482    },
483    Withdraw {
484        address: Address<NetworkUnchecked>,
485        #[serde(with = "bitcoin::amount::serde::as_sat")]
486        amount: bitcoin::Amount,
487        fee: PegOutFees,
488        change: Vec<OutPoint>,
489    },
490
491    RbfWithdraw {
492        rbf: Rbf,
493        change: Vec<OutPoint>,
494    },
495}
496
497/// The non-resource, just plain-data parts of [`WalletClientModule`]
498#[derive(Debug, Clone)]
499pub struct WalletClientModuleData {
500    cfg: WalletClientConfig,
501    module_root_secret: DerivableSecret,
502}
503
504impl WalletClientModuleData {
505    fn derive_deposit_address(
506        &self,
507        idx: TweakIdx,
508    ) -> (Keypair, secp256k1::PublicKey, Address, OperationId) {
509        let idx = ChildId(idx.0);
510
511        let secret_tweak_key = self
512            .module_root_secret
513            .child_key(WALLET_TWEAK_CHILD_ID)
514            .child_key(idx)
515            .to_secp_key(fedimint_core::secp256k1::SECP256K1);
516
517        let public_tweak_key = secret_tweak_key.public_key();
518
519        let address = self
520            .cfg
521            .peg_in_descriptor
522            .tweak(&public_tweak_key, bitcoin::secp256k1::SECP256K1)
523            .address(self.cfg.network.0)
524            .unwrap();
525
526        // TODO: make hash?
527        let operation_id = OperationId(public_tweak_key.x_only_public_key().0.serialize());
528
529        (secret_tweak_key, public_tweak_key, address, operation_id)
530    }
531
532    fn derive_peg_in_script(
533        &self,
534        idx: TweakIdx,
535    ) -> (ScriptBuf, bitcoin::Address, Keypair, OperationId) {
536        let (secret_tweak_key, _, address, operation_id) = self.derive_deposit_address(idx);
537
538        (
539            self.cfg
540                .peg_in_descriptor
541                .tweak(&secret_tweak_key.public_key(), SECP256K1)
542                .script_pubkey(),
543            address,
544            secret_tweak_key,
545            operation_id,
546        )
547    }
548}
549
550#[derive(Debug)]
551#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
552pub struct WalletClientModule {
553    data: WalletClientModuleData,
554    db: Database,
555    module_api: DynModuleApi,
556    notifier: ModuleNotifier<WalletClientStates>,
557    rpc: DynBitcoindRpc,
558    client_ctx: ClientContext<Self>,
559    /// Updated to wake up pegin monitor
560    pegin_monitor_wakeup_sender: watch::Sender<()>,
561    pegin_monitor_wakeup_receiver: watch::Receiver<()>,
562    /// Called every time a peg-in was claimed
563    pegin_claimed_sender: watch::Sender<()>,
564    pegin_claimed_receiver: watch::Receiver<()>,
565    task_group: TaskGroup,
566    client_span: tracing::Span,
567    admin_auth: Option<ApiAuth>,
568}
569
570#[apply(async_trait_maybe_send!)]
571impl ClientModule for WalletClientModule {
572    type Init = WalletClientInit;
573    type Common = WalletModuleTypes;
574    type Backup = WalletModuleBackup;
575    type ModuleStateMachineContext = WalletClientContext;
576    type States = WalletClientStates;
577
578    fn context(&self) -> Self::ModuleStateMachineContext {
579        WalletClientContext {
580            rpc: self.rpc.clone(),
581            wallet_descriptor: self.cfg().peg_in_descriptor.clone(),
582            wallet_decoder: self.decoder(),
583            secp: Secp256k1::default(),
584            client_ctx: self.client_ctx.clone(),
585        }
586    }
587
588    async fn start(&self) {
589        self.task_group
590            .spawn_cancellable_with_span(self.client_span.clone(), "peg-in monitor", {
591                let client_ctx = self.client_ctx.clone();
592                let db = self.db.clone();
593                let btc_rpc = self.rpc.clone();
594                let module_api = self.module_api.clone();
595                let data = self.data.clone();
596                let pegin_claimed_sender = self.pegin_claimed_sender.clone();
597                let pegin_monitor_wakeup_receiver = self.pegin_monitor_wakeup_receiver.clone();
598                pegin_monitor::run_peg_in_monitor(
599                    client_ctx,
600                    db,
601                    btc_rpc,
602                    module_api,
603                    data,
604                    pegin_claimed_sender,
605                    pegin_monitor_wakeup_receiver,
606                )
607            });
608
609        self.task_group.spawn_cancellable_with_span(
610            self.client_span.clone(),
611            "supports-safe-deposit-version",
612            {
613                let db = self.db.clone();
614                let module_api = self.module_api.clone();
615
616                poll_supports_safe_deposit_version(db, module_api)
617            },
618        );
619    }
620
621    fn supports_backup(&self) -> bool {
622        true
623    }
624
625    async fn backup(&self) -> anyhow::Result<backup::WalletModuleBackup> {
626        // fetch consensus height first
627        let session_count = self.client_ctx.global_api().session_count().await?;
628
629        let mut dbtx = self.db.begin_transaction_nc().await;
630        let next_pegin_tweak_idx = dbtx
631            .get_value(&NextPegInTweakIndexKey)
632            .await
633            .unwrap_or_default();
634        let claimed = dbtx
635            .find_by_prefix(&PegInTweakIndexPrefix)
636            .await
637            .filter_map(|(k, v)| async move {
638                if v.claimed.is_empty() {
639                    None
640                } else {
641                    Some(k.0)
642                }
643            })
644            .collect()
645            .await;
646        Ok(backup::WalletModuleBackup::new_v1(
647            session_count,
648            next_pegin_tweak_idx,
649            claimed,
650        ))
651    }
652
653    fn input_fee(
654        &self,
655        _amount: &Amounts,
656        _input: &<Self::Common as ModuleCommon>::Input,
657    ) -> Option<Amounts> {
658        Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_in_abs))
659    }
660
661    fn output_fee(
662        &self,
663        _amount: &Amounts,
664        _output: &<Self::Common as ModuleCommon>::Output,
665    ) -> Option<Amounts> {
666        Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs))
667    }
668
669    async fn handle_rpc(
670        &self,
671        method: String,
672        request: serde_json::Value,
673    ) -> BoxStream<'_, anyhow::Result<serde_json::Value>> {
674        Box::pin(try_stream! {
675            match method.as_str() {
676                "get_wallet_summary" => {
677                    let _req: WalletSummaryRequest = serde_json::from_value(request)?;
678                    let wallet_summary = self.get_wallet_summary()
679                        .await
680                        .expect("Failed to fetch wallet summary");
681                    let result = serde_json::to_value(&wallet_summary)
682                        .expect("Serialization error");
683                    yield result;
684                }
685                "get_block_count_local" => {
686                    let block_count = self.get_block_count_local().await
687                        .expect("Failed to fetch block count");
688                    yield serde_json::to_value(block_count)?;
689                }
690                "peg_in" => {
691                    let req: PegInRequest = serde_json::from_value(request)?;
692                    let response = self.peg_in(req)
693                        .await
694                        .map_err(|e| anyhow::anyhow!("peg_in failed: {e}"))?;
695                    let result = serde_json::to_value(&response)?;
696                    yield result;
697                },
698                "peg_out" => {
699                    let req: PegOutRequest = serde_json::from_value(request)?;
700                    let response = self.peg_out(req)
701                        .await
702                        .map_err(|e| anyhow::anyhow!("peg_out failed: {e}"))?;
703                    let result = serde_json::to_value(&response)?;
704                    yield result;
705                },
706                "subscribe_deposit" => {
707                    let req: SubscribeDepositRequest = serde_json::from_value(request)?;
708                    for await state in self.subscribe_deposit(req.operation_id).await?.into_stream() {
709                        yield serde_json::to_value(state)?;
710                    }
711                },
712                "subscribe_withdraw" => {
713                    let req: SubscribeWithdrawRequest = serde_json::from_value(request)?;
714                    for await state in self.subscribe_withdraw_updates(req.operation_id).await?.into_stream(){
715                        yield serde_json::to_value(state)?;
716                    }
717                }
718                _ => {
719                    Err(anyhow::format_err!("Unknown method: {method}"))?;
720                }
721            }
722        })
723    }
724
725    #[cfg(feature = "cli")]
726    async fn handle_cli_command(
727        &self,
728        args: &[std::ffi::OsString],
729    ) -> anyhow::Result<serde_json::Value> {
730        cli::handle_cli_command(self, args).await
731    }
732}
733
734#[derive(Deserialize)]
735struct WalletSummaryRequest {}
736
737#[derive(Debug, Clone)]
738pub struct WalletClientContext {
739    rpc: DynBitcoindRpc,
740    wallet_descriptor: PegInDescriptor,
741    wallet_decoder: Decoder,
742    secp: Secp256k1<All>,
743    pub client_ctx: ClientContext<WalletClientModule>,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct PegInRequest {
748    pub extra_meta: serde_json::Value,
749}
750
751#[cfg(feature = "uniffi")]
752uniffi::custom_type!(PegInRequest, String, {
753    lower: |v| serde_json::to_string(&v).expect("PegInRequest serialization cannot fail"),
754    try_lift: |s| serde_json::from_str::<PegInRequest>(&s).map_err(|e| anyhow!("Failed to parse PegInRequest: {e}")),
755});
756
757#[derive(Deserialize)]
758struct SubscribeDepositRequest {
759    operation_id: OperationId,
760}
761
762#[derive(Deserialize)]
763struct SubscribeWithdrawRequest {
764    operation_id: OperationId,
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize)]
768#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
769pub struct PegInResponse {
770    pub deposit_address: Address<NetworkUnchecked>,
771    pub operation_id: OperationId,
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize)]
775#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
776pub struct PegOutRequest {
777    pub amount_sat: u64,
778    pub destination_address: Address<NetworkUnchecked>,
779    pub extra_meta: serde_json::Value,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize)]
783#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
784pub struct PegOutResponse {
785    pub operation_id: OperationId,
786}
787
788impl Context for WalletClientContext {
789    const KIND: Option<ModuleKind> = Some(KIND);
790}
791
792impl WalletClientModule {
793    fn cfg(&self) -> &WalletClientConfig {
794        &self.data.cfg
795    }
796
797    fn get_rpc_config(cfg: &WalletClientConfig) -> BitcoinRpcConfig {
798        match BitcoinRpcConfig::get_defaults_from_env_vars() {
799            Ok(rpc_config) => {
800                // TODO: Wallet client cannot support bitcoind RPC until the bitcoin dep is
801                // updated to 0.30
802                if rpc_config.kind == "bitcoind" {
803                    cfg.default_bitcoin_rpc.clone()
804                } else {
805                    rpc_config
806                }
807            }
808            _ => cfg.default_bitcoin_rpc.clone(),
809        }
810    }
811
812    pub fn get_network(&self) -> Network {
813        self.cfg().network.0
814    }
815
816    pub fn get_finality_delay(&self) -> u32 {
817        self.cfg().finality_delay
818    }
819
820    pub fn get_fee_consensus(&self) -> FeeConsensus {
821        self.cfg().fee_consensus
822    }
823
824    async fn allocate_deposit_address_inner(
825        &self,
826        dbtx: &mut DatabaseTransaction<'_>,
827    ) -> DepositAddressInfo {
828        dbtx.ensure_isolated().expect("Must be isolated db");
829
830        let tweak_idx = get_next_peg_in_tweak_child_id(dbtx).await;
831        let (_secret_tweak_key, _, address, operation_id) =
832            self.data.derive_deposit_address(tweak_idx);
833
834        let now = fedimint_core::time::now();
835
836        dbtx.insert_new_entry(
837            &PegInTweakIndexKey(tweak_idx),
838            &PegInTweakIndexData {
839                creation_time: now,
840                next_check_time: Some(now),
841                last_check_time: None,
842                operation_id,
843                claimed: vec![],
844            },
845        )
846        .await;
847
848        DepositAddressInfo {
849            operation_id,
850            address,
851            tweak_idx,
852        }
853    }
854
855    /// Fetches the fees that would need to be paid to make the withdraw request
856    /// using [`Self::withdraw`] work *right now*.
857    ///
858    /// Note that we do not receive a guarantee that these fees will be valid in
859    /// the future, thus even the next second using these fees *may* fail.
860    /// The caller should be prepared to retry with a new fee estimate.
861    pub async fn get_withdraw_fees(
862        &self,
863        address: &bitcoin::Address,
864        amount: bitcoin::Amount,
865    ) -> anyhow::Result<PegOutFees> {
866        self.module_api
867            .fetch_peg_out_fees(address, amount)
868            .await?
869            .context("Federation didn't return peg-out fees")
870    }
871
872    /// Computes the federation fee a peg-out of an on-chain output worth
873    /// `amount` (the withdrawal amount plus the on-chain miner fee) would
874    /// incur, without submitting anything.
875    ///
876    /// A peg-out submits a single wallet output worth `amount`; the primary
877    /// module balances it by spending ecash to fund the output and minting any
878    /// change. This quotes the fee of that transaction — the wallet (peg-out)
879    /// output fee, the mint input fees on the funding notes, any mint change
880    /// output fees, and sub-denomination dust — via the shared, module-agnostic
881    /// fee quote.
882    ///
883    /// The on-chain Bitcoin miner fee is deliberately excluded: it is part of
884    /// the output `amount` (see [`Self::get_withdraw_fees`]), not the
885    /// on-federation transaction fee.
886    pub async fn send_fee_quote(&self, amount: bitcoin::Amount) -> anyhow::Result<FeeQuote> {
887        let amount = fedimint_core::Amount::from_sats(amount.to_sat());
888        self.client_ctx
889            .fee_quote(
890                OperationId::new_random(),
891                FeeQuoteRequest {
892                    input_amount: Amounts::ZERO,
893                    output_amount: Amounts::new_bitcoin(amount),
894                    input_fee: Amounts::ZERO,
895                    output_fee: Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs),
896                },
897            )
898            .await
899    }
900
901    /// Returns a summary of the wallet's coins
902    pub async fn get_wallet_summary(&self) -> anyhow::Result<WalletSummary> {
903        Ok(self.module_api.fetch_wallet_summary().await?)
904    }
905
906    pub async fn get_block_count_local(&self) -> anyhow::Result<u32> {
907        Ok(self.module_api.fetch_block_count_local().await?)
908    }
909
910    pub fn create_withdraw_output(
911        &self,
912        operation_id: OperationId,
913        address: bitcoin::Address,
914        amount: bitcoin::Amount,
915        fees: PegOutFees,
916    ) -> anyhow::Result<ClientOutputBundle<WalletOutput, WalletClientStates>> {
917        let output = WalletOutput::new_v0_peg_out(address, amount, fees);
918
919        let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
920
921        let sm_gen = move |out_point_range: OutPointRange| {
922            assert_eq!(out_point_range.count(), 1);
923            let out_idx = out_point_range.start_idx();
924            vec![WalletClientStates::Withdraw(WithdrawStateMachine {
925                operation_id,
926                state: WithdrawStates::Created(CreatedWithdrawState {
927                    fm_outpoint: OutPoint {
928                        txid: out_point_range.txid(),
929                        out_idx,
930                    },
931                }),
932            })]
933        };
934
935        Ok(ClientOutputBundle::new(
936            vec![ClientOutput::<WalletOutput> {
937                output,
938                amounts: Amounts::new_bitcoin(amount),
939            }],
940            vec![ClientOutputSM::<WalletClientStates> {
941                state_machines: Arc::new(sm_gen),
942            }],
943        ))
944    }
945
946    pub async fn peg_in(&self, req: PegInRequest) -> anyhow::Result<PegInResponse> {
947        let deposit_address = self.safe_allocate_deposit_address(req.extra_meta).await?;
948
949        Ok(PegInResponse {
950            deposit_address: Address::from_script(
951                &deposit_address.address.script_pubkey(),
952                self.get_network(),
953            )?
954            .as_unchecked()
955            .clone(),
956            operation_id: deposit_address.operation_id,
957        })
958    }
959
960    pub async fn peg_out(&self, req: PegOutRequest) -> anyhow::Result<PegOutResponse> {
961        let amount = bitcoin::Amount::from_sat(req.amount_sat);
962        let destination = req
963            .destination_address
964            .require_network(self.get_network())?;
965
966        let fees = self.get_withdraw_fees(&destination, amount).await?;
967        let operation_id = self
968            .withdraw(&destination, amount, fees, req.extra_meta)
969            .await
970            .context("Failed to initiate withdraw")?;
971
972        Ok(PegOutResponse { operation_id })
973    }
974
975    pub fn create_rbf_withdraw_output(
976        &self,
977        operation_id: OperationId,
978        rbf: &Rbf,
979    ) -> anyhow::Result<ClientOutputBundle<WalletOutput, WalletClientStates>> {
980        let output = WalletOutput::new_v0_rbf(rbf.fees, rbf.txid);
981
982        let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
983
984        let sm_gen = move |out_point_range: OutPointRange| {
985            assert_eq!(out_point_range.count(), 1);
986            let out_idx = out_point_range.start_idx();
987            vec![WalletClientStates::Withdraw(WithdrawStateMachine {
988                operation_id,
989                state: WithdrawStates::Created(CreatedWithdrawState {
990                    fm_outpoint: OutPoint {
991                        txid: out_point_range.txid(),
992                        out_idx,
993                    },
994                }),
995            })]
996        };
997
998        Ok(ClientOutputBundle::new(
999            vec![ClientOutput::<WalletOutput> {
1000                output,
1001                amounts: Amounts::new_bitcoin(amount),
1002            }],
1003            vec![ClientOutputSM::<WalletClientStates> {
1004                state_machines: Arc::new(sm_gen),
1005            }],
1006        ))
1007    }
1008
1009    pub async fn btc_tx_has_no_size_limit(&self) -> FederationResult<bool> {
1010        Ok(self.module_api.module_consensus_version().await? >= ModuleConsensusVersion::new(2, 2))
1011    }
1012
1013    /// Returns true if the federation's wallet module consensus version
1014    /// supports processing all deposits.
1015    ///
1016    /// This method is safe to call offline, since it first attempts to read a
1017    /// key from the db that represents the client has previously been able to
1018    /// verify the wallet module consensus version. If the client has not
1019    /// verified the version, it must be online to fetch the latest wallet
1020    /// module consensus version.
1021    pub async fn supports_safe_deposit(&self) -> bool {
1022        let mut dbtx = self.db.begin_transaction().await;
1023
1024        let already_verified_supports_safe_deposit =
1025            dbtx.get_value(&SupportsSafeDepositKey).await.is_some();
1026
1027        already_verified_supports_safe_deposit || {
1028            match self.module_api.module_consensus_version().await {
1029                Ok(module_consensus_version) => {
1030                    let supported_version =
1031                        SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION <= module_consensus_version;
1032
1033                    if supported_version {
1034                        dbtx.insert_new_entry(&SupportsSafeDepositKey, &()).await;
1035                        dbtx.commit_tx().await;
1036                    }
1037
1038                    supported_version
1039                }
1040                Err(_) => false,
1041            }
1042        }
1043    }
1044
1045    /// Allocates a deposit address controlled by the federation, guaranteeing
1046    /// safe handling of all deposits, including on-chain transactions exceeding
1047    /// `ALEPH_BFT_UNIT_BYTE_LIMIT`.
1048    ///
1049    /// Returns an error if the client has never been online to verify the
1050    /// federation's wallet module consensus version supports processing all
1051    /// deposits.
1052    pub async fn safe_allocate_deposit_address<M>(
1053        &self,
1054        extra_meta: M,
1055    ) -> anyhow::Result<DepositAddressInfo>
1056    where
1057        M: Serialize + MaybeSend + MaybeSync,
1058    {
1059        ensure!(
1060            self.supports_safe_deposit().await,
1061            "Wallet module consensus version doesn't support safe deposits",
1062        );
1063
1064        self.allocate_deposit_address_expert_only(extra_meta).await
1065    }
1066
1067    /// Allocates a deposit address that is controlled by the federation.
1068    ///
1069    /// This is an EXPERT ONLY method intended for power users such as Lightning
1070    /// gateways allocating liquidity, and we discourage exposing peg-in
1071    /// functionality to everyday users of a Fedimint wallet due to the
1072    /// following two limitations:
1073    ///
1074    /// The transaction sending to this address needs to be smaller than 40KB in
1075    /// order for the peg-in to be claimable. If the transaction is too large,
1076    /// funds will be lost.
1077    ///
1078    /// In the future, federations will also enforce a minimum peg-in amount to
1079    /// prevent accumulation of dust UTXOs. Peg-ins under this minimum cannot be
1080    /// claimed and funds will be lost.
1081    ///
1082    /// Everyday users should rely on Lightning to move funds into the
1083    /// federation.
1084    pub async fn allocate_deposit_address_expert_only<M>(
1085        &self,
1086        extra_meta: M,
1087    ) -> anyhow::Result<DepositAddressInfo>
1088    where
1089        M: Serialize + MaybeSend + MaybeSync,
1090    {
1091        let extra_meta_value =
1092            serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1093        let deposit_address = self
1094            .db
1095            .autocommit(
1096                move |dbtx, _| {
1097                    let extra_meta_value_inner = extra_meta_value.clone();
1098                    Box::pin(async move {
1099                        let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1100
1101                        self.client_ctx
1102                            .manual_operation_start_dbtx(
1103                                dbtx,
1104                                deposit_address.operation_id,
1105                                WalletCommonInit::KIND.as_str(),
1106                                WalletOperationMeta {
1107                                    variant: WalletOperationMetaVariant::Deposit {
1108                                        address: deposit_address.address.clone().into_unchecked(),
1109                                        tweak_idx: Some(deposit_address.tweak_idx),
1110                                        expires_at: None,
1111                                    },
1112                                    extra_meta: extra_meta_value_inner,
1113                                },
1114                                vec![],
1115                            )
1116                            .await?;
1117
1118                        debug!(
1119                            target: LOG_CLIENT_MODULE_WALLET,
1120                            tweak_idx = %deposit_address.tweak_idx,
1121                            address = %deposit_address.address,
1122                            "Derived a new deposit address"
1123                        );
1124
1125                        // Begin watching the script address
1126                        self.rpc
1127                            .watch_script_history(&deposit_address.address.script_pubkey())
1128                            .await?;
1129
1130                        let sender = self.pegin_monitor_wakeup_sender.clone();
1131                        dbtx.on_commit(move || {
1132                            sender.send_replace(());
1133                        });
1134
1135                        Ok(deposit_address)
1136                    })
1137                },
1138                Some(100),
1139            )
1140            .await
1141            .map_err(|e| match e {
1142                AutocommitError::CommitFailed {
1143                    last_error,
1144                    attempts,
1145                } => anyhow!("Failed to commit after {attempts} attempts: {last_error}"),
1146                AutocommitError::ClosureError { error, .. } => error,
1147            })?;
1148
1149        Ok(deposit_address)
1150    }
1151
1152    /// Allocate a deposit address, bounding the gap of consecutive unused
1153    /// addresses past the last-used one, without selecting a reusable address.
1154    ///
1155    /// "Unused" here means `PegInTweakIndexData::claimed.is_empty()` — the
1156    /// `pegin_monitor` has not yet successfully claimed any deposit on this
1157    /// address. There is a small race window where a deposit has been
1158    /// observed in the mempool / awaiting confirmations but not yet claimed,
1159    /// in which case the address still looks unused. Reusing such an address
1160    /// is benign in practice: `pegin_monitor` claims any number of deposits
1161    /// per address.
1162    ///
1163    /// Let `gap` be the count of unused tweak indices strictly greater than
1164    /// the highest used index (or the count of all allocated tweaks if none
1165    /// have been used yet). Semantics:
1166    ///   - If `gap >= max_gap_size` and there is at least one unused address,
1167    ///     returns all those addresses with
1168    ///     [`MaybeNewAddress::TooManyUnusedAddresses`].
1169    ///   - Otherwise, allocates a new address (equivalent to
1170    ///     [`Self::allocate_deposit_address_expert_only`]) and returns it with
1171    ///     [`MaybeNewAddress::NewAddress`].
1172    ///
1173    /// `max_gap_size == 0` therefore means "only allocate fresh when the
1174    /// previous address was already used". The very first call still
1175    /// allocates fresh because there are no addresses to reuse.
1176    ///
1177    /// `max_gap_size == usize::MAX` makes this method behave like
1178    /// [`Self::allocate_deposit_address_expert_only`] — always allocating a new
1179    /// address.
1180    ///
1181    /// Reusable addresses are ordered by `creation_time` ascending. This lets
1182    /// callers that need a custom selection strategy choose from all available
1183    /// candidates.
1184    ///
1185    /// Caveats inherited from
1186    /// [`Self::allocate_deposit_address_expert_only`] (40KB tx limit, future
1187    /// minimum peg-in amount) apply equally to newly allocated and reused
1188    /// addresses.
1189    pub async fn allocate_deposit_address_pooled_stateless(
1190        &self,
1191        max_gap_size: usize,
1192    ) -> anyhow::Result<MaybeNewAddress> {
1193        let max_gap_size_u64 = u64::try_from(max_gap_size).unwrap_or(u64::MAX);
1194        let extra_meta_value = serde_json::Value::Null;
1195        let result = self
1196            .db
1197            .autocommit(
1198                move |dbtx, _| {
1199                    let extra_meta_value_inner = extra_meta_value.clone();
1200                    Box::pin(async move {
1201                        let unused = self.unused_pooled_deposit_addresses(dbtx).await;
1202
1203                        if max_gap_size_u64 <= unused.len() as u64 && !unused.is_empty() {
1204                            let addresses = unused
1205                                .into_iter()
1206                                .map(|(tweak_idx, data)| {
1207                                    let (_script, address, _key, operation_id) =
1208                                        self.data.derive_peg_in_script(tweak_idx);
1209
1210                                    debug_assert_eq!(operation_id, data.operation_id);
1211
1212                                    DepositAddressInfo {
1213                                        operation_id,
1214                                        address,
1215                                        tweak_idx,
1216                                    }
1217                                })
1218                                .collect();
1219
1220                            return Ok::<_, anyhow::Error>(
1221                                MaybeNewAddress::TooManyUnusedAddresses(addresses),
1222                            );
1223                        }
1224
1225                        let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1226
1227                        self.client_ctx
1228                            .manual_operation_start_dbtx(
1229                                dbtx,
1230                                deposit_address.operation_id,
1231                                WalletCommonInit::KIND.as_str(),
1232                                WalletOperationMeta {
1233                                    variant: WalletOperationMetaVariant::Deposit {
1234                                        address: deposit_address.address.clone().into_unchecked(),
1235                                        tweak_idx: Some(deposit_address.tweak_idx),
1236                                        expires_at: None,
1237                                    },
1238                                    extra_meta: extra_meta_value_inner,
1239                                },
1240                                vec![],
1241                            )
1242                            .await?;
1243
1244                        debug!(
1245                            target: LOG_CLIENT_MODULE_WALLET,
1246                            tweak_idx = %deposit_address.tweak_idx,
1247                            address = %deposit_address.address,
1248                            "Derived a new pooled deposit address"
1249                        );
1250
1251                        self.rpc
1252                            .watch_script_history(&deposit_address.address.script_pubkey())
1253                            .await?;
1254
1255                        let sender = self.pegin_monitor_wakeup_sender.clone();
1256                        dbtx.on_commit(move || {
1257                            sender.send_replace(());
1258                        });
1259
1260                        Ok(MaybeNewAddress::NewAddress(deposit_address))
1261                    })
1262                },
1263                Some(100),
1264            )
1265            .await
1266            .map_err(|e| match e {
1267                AutocommitError::CommitFailed {
1268                    last_error,
1269                    attempts,
1270                } => anyhow!("Failed to commit after {attempts} attempts: {last_error}"),
1271                AutocommitError::ClosureError { error, .. } => error,
1272            })?;
1273
1274        Ok(result)
1275    }
1276
1277    async fn unused_pooled_deposit_addresses(
1278        &self,
1279        dbtx: &mut DatabaseTransaction<'_>,
1280    ) -> Vec<(TweakIdx, PegInTweakIndexData)> {
1281        // Walk peg-in tweaks in descending order, taking unused ones
1282        // (`claimed.is_empty()`) until we hit a used one. That gives us exactly
1283        // the trailing gap — `gap + 1` reads regardless of total tweak count.
1284        // No RPC and no separately-maintained `last_used` index needed.
1285        let mut unused: Vec<(TweakIdx, PegInTweakIndexData)> = dbtx
1286            .find_by_prefix_sorted_descending(&PegInTweakIndexPrefix)
1287            .await
1288            .take_while(|(_, d)| std::future::ready(d.claimed.is_empty()))
1289            .map(|(k, v)| (k.0, v))
1290            .collect()
1291            .await;
1292
1293        // Order by `creation_time` ascending for caller selection and stable
1294        // wraparound after stateful reuse mutates `creation_time`.
1295        unused.sort_by_key(|(t, d)| (d.creation_time, *t));
1296        unused
1297    }
1298
1299    /// Allocate a deposit address, bounding the gap of consecutive unused
1300    /// addresses past the last-used one.
1301    ///
1302    /// This is a stateful wrapper over
1303    /// [`Self::allocate_deposit_address_pooled_stateless`]. If the stateless
1304    /// method returns [`MaybeNewAddress::TooManyUnusedAddresses`], this method
1305    /// picks one of those addresses with a persisted round-robin cursor and
1306    /// returns it with [`AllocateDepositOutcome::Reused`].
1307    ///
1308    /// Selection policy when reusing: round-robin over unused addresses
1309    /// ordered by `creation_time` ascending, with the cursor persisted
1310    /// per-client in the wallet module's DB. The cursor stores the next
1311    /// tweak index to consider; after picking idx X it advances to X+1, and
1312    /// when no candidate has `tweak_idx >= cursor` it wraps to the
1313    /// oldest-by-`creation_time` candidate. This gives the caller the
1314    /// predictable "cycle through the unused addresses" behavior.
1315    ///
1316    /// On reuse the `creation_time` and check-schedule fields of the
1317    /// underlying tweak entry are reset to "now", so `pegin_monitor`'s
1318    /// age-proportional polling delay restarts from zero. Without this an
1319    /// old entry would keep its long stale next-check delay and a user
1320    /// sending to it could wait a long time before the client noticed.
1321    #[allow(clippy::too_many_lines)]
1322    pub async fn allocate_deposit_address_pooled(
1323        &self,
1324        max_gap_size: usize,
1325    ) -> anyhow::Result<(DepositAddressInfo, AllocateDepositOutcome)> {
1326        let stateless = self
1327            .allocate_deposit_address_pooled_stateless(max_gap_size)
1328            .await?;
1329
1330        let reused_addresses = match stateless {
1331            MaybeNewAddress::NewAddress(deposit_address) => {
1332                return Ok((deposit_address, AllocateDepositOutcome::Fresh));
1333            }
1334            MaybeNewAddress::TooManyUnusedAddresses(addresses) => addresses,
1335        };
1336
1337        let result = self
1338            .db
1339            .autocommit(
1340                move |dbtx, _| {
1341                    let reused_addresses = reused_addresses.clone();
1342                    Box::pin(async move {
1343                        let cursor = dbtx
1344                            .get_value(&PegInPoolCursorKey)
1345                            .await
1346                            .unwrap_or(TweakIdx(0));
1347
1348                        let pick_pos = reused_addresses
1349                            .iter()
1350                            .position(|a| cursor <= a.tweak_idx)
1351                            .unwrap_or(0);
1352                        let reused_address = reused_addresses[pick_pos].clone();
1353
1354                        let existing_tweak_idx = reused_address.tweak_idx;
1355                        let existing = dbtx
1356                            .get_value(&PegInTweakIndexKey(reused_address.tweak_idx))
1357                            .await
1358                            .with_context(|| {
1359                                format!(
1360                                    "Pooled address disappeared while reusing {}",
1361                                    reused_address.tweak_idx
1362                                )
1363                            })?;
1364
1365                        ensure!(
1366                            existing.claimed.is_empty(),
1367                            "Pooled address was used while reusing {}",
1368                            reused_address.tweak_idx
1369                        );
1370
1371                        dbtx.insert_entry(&PegInPoolCursorKey, &reused_address.tweak_idx.next())
1372                            .await;
1373
1374                        // Reset the monitoring schedule so the reused address
1375                        // gets checked as aggressively as a freshly-allocated
1376                        // one. Without this, an old entry would retain its
1377                        // `age/10` next-check delay from `pegin_monitor`,
1378                        // meaning a user who sends to a long-idle pool address
1379                        // could wait hours before the client polls for the
1380                        // deposit.
1381                        let now = fedimint_core::time::now();
1382                        dbtx.insert_entry(
1383                            &PegInTweakIndexKey(reused_address.tweak_idx),
1384                            &PegInTweakIndexData {
1385                                creation_time: now,
1386                                last_check_time: None,
1387                                next_check_time: Some(now),
1388                                operation_id: existing.operation_id,
1389                                claimed: existing.claimed,
1390                            },
1391                        )
1392                        .await;
1393
1394                        let sender = self.pegin_monitor_wakeup_sender.clone();
1395                        dbtx.on_commit(move || {
1396                            sender.send_replace(());
1397                        });
1398
1399                        Ok::<_, anyhow::Error>((
1400                            reused_address,
1401                            AllocateDepositOutcome::Reused {
1402                                original_tweak_idx: existing_tweak_idx,
1403                            },
1404                        ))
1405                    })
1406                },
1407                Some(100),
1408            )
1409            .await
1410            .map_err(|e| match e {
1411                AutocommitError::CommitFailed {
1412                    last_error,
1413                    attempts,
1414                } => anyhow!("Failed to commit after {attempts} attempts: {last_error}"),
1415                AutocommitError::ClosureError { error, .. } => error,
1416            })?;
1417
1418        Ok(result)
1419    }
1420
1421    /// Returns a stream of updates about an ongoing deposit operation created
1422    /// with [`WalletClientModule::allocate_deposit_address_expert_only`].
1423    /// Returns an error for old deposit operations created prior to the 0.4
1424    /// release and not driven to completion yet. This should be rare enough
1425    /// that an indeterminate state is ok here.
1426    pub async fn subscribe_deposit(
1427        &self,
1428        operation_id: OperationId,
1429    ) -> anyhow::Result<UpdateStreamOrOutcome<DepositStateV2>> {
1430        let operation = self
1431            .client_ctx
1432            .get_operation(operation_id)
1433            .await
1434            .with_context(|| anyhow!("Operation not found: {}", operation_id.fmt_short()))?;
1435
1436        if operation.operation_module_kind() != WalletCommonInit::KIND.as_str() {
1437            bail!("Operation is not a wallet operation");
1438        }
1439
1440        let operation_meta = operation.meta::<WalletOperationMeta>();
1441
1442        let WalletOperationMetaVariant::Deposit {
1443            address, tweak_idx, ..
1444        } = operation_meta.variant
1445        else {
1446            bail!("Operation is not a deposit operation");
1447        };
1448
1449        let address = address.require_network(self.cfg().network.0)?;
1450
1451        // The old deposit operations don't have tweak_idx set
1452        let Some(tweak_idx) = tweak_idx else {
1453            // In case we are dealing with an old deposit that still uses state machines we
1454            // don't have the logic here anymore to subscribe to updates. We can still read
1455            // the final state though if it reached any.
1456            let outcome_v1 = operation
1457                .outcome::<DepositStateV1>()
1458                .context("Old pending deposit, can't subscribe to updates")?;
1459
1460            let outcome_v2 = match outcome_v1 {
1461                DepositStateV1::Claimed(tx_info) => DepositStateV2::Claimed {
1462                    btc_deposited: tx_info.btc_transaction.output[tx_info.out_idx as usize].value,
1463                    btc_out_point: bitcoin::OutPoint {
1464                        txid: tx_info.btc_transaction.compute_txid(),
1465                        vout: tx_info.out_idx,
1466                    },
1467                },
1468                DepositStateV1::Failed(error) => DepositStateV2::Failed(error),
1469                _ => bail!("Non-final outcome in operation log"),
1470            };
1471
1472            return Ok(UpdateStreamOrOutcome::Outcome(outcome_v2));
1473        };
1474
1475        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, {
1476            let stream_rpc = self.rpc.clone();
1477            let stream_client_ctx = self.client_ctx.clone();
1478            let stream_script_pub_key = address.script_pubkey();
1479            move || {
1480
1481            stream! {
1482                yield DepositStateV2::WaitingForTransaction;
1483
1484                retry(
1485                    "subscribe script history",
1486                    background_backoff(),
1487                    || stream_rpc.watch_script_history(&stream_script_pub_key)
1488                ).await.expect("Will never give up");
1489                let (btc_out_point, btc_deposited) = retry(
1490                    "fetch history",
1491                    background_backoff(),
1492                    || async {
1493                        let history = stream_rpc.get_script_history(&stream_script_pub_key).await?;
1494                        history.first().and_then(|tx| {
1495                            let (out_idx, amount) = tx.output
1496                                .iter()
1497                                .enumerate()
1498                                .find_map(|(idx, output)| (output.script_pubkey == stream_script_pub_key).then_some((idx, output.value)))?;
1499                            let txid = tx.compute_txid();
1500
1501                            Some((
1502                                bitcoin::OutPoint {
1503                                    txid,
1504                                    vout: out_idx as u32,
1505                                },
1506                                amount
1507                            ))
1508                        }).context("No deposit transaction found")
1509                    }
1510                ).await.expect("Will never give up");
1511
1512                yield DepositStateV2::WaitingForConfirmation {
1513                    btc_deposited,
1514                    btc_out_point
1515                };
1516
1517                let claim_data = stream_client_ctx.module_db().wait_key_exists(&ClaimedPegInKey {
1518                    peg_in_index: tweak_idx,
1519                    btc_out_point,
1520                }).await;
1521
1522                yield DepositStateV2::Confirmed {
1523                    btc_deposited,
1524                    btc_out_point
1525                };
1526
1527                match stream_client_ctx.await_primary_module_outputs(operation_id, claim_data.change).await {
1528                    Ok(()) => yield DepositStateV2::Claimed {
1529                        btc_deposited,
1530                        btc_out_point
1531                    },
1532                    Err(e) => yield DepositStateV2::Failed(e.to_string())
1533                }
1534            }
1535        }}))
1536    }
1537
1538    pub async fn list_peg_in_tweak_idxes(&self) -> BTreeMap<TweakIdx, PegInTweakIndexData> {
1539        self.client_ctx
1540            .module_db()
1541            .clone()
1542            .begin_transaction_nc()
1543            .await
1544            .find_by_prefix(&PegInTweakIndexPrefix)
1545            .await
1546            .map(|(key, data)| (key.0, data))
1547            .collect()
1548            .await
1549    }
1550
1551    pub async fn find_tweak_idx_by_address(
1552        &self,
1553        address: bitcoin::Address<NetworkUnchecked>,
1554    ) -> anyhow::Result<TweakIdx> {
1555        let data = self.data.clone();
1556        let Some((tweak_idx, _)) = self
1557            .db
1558            .begin_transaction_nc()
1559            .await
1560            .find_by_prefix(&PegInTweakIndexPrefix)
1561            .await
1562            .filter(|(k, _)| {
1563                let (_, derived_address, _tweak_key, _) = data.derive_peg_in_script(k.0);
1564                future::ready(derived_address.into_unchecked() == address)
1565            })
1566            .next()
1567            .await
1568        else {
1569            bail!("Address not found in the list of derived keys");
1570        };
1571
1572        Ok(tweak_idx.0)
1573    }
1574    pub async fn find_tweak_idx_by_operation_id(
1575        &self,
1576        operation_id: OperationId,
1577    ) -> anyhow::Result<TweakIdx> {
1578        Ok(self
1579            .client_ctx
1580            .module_db()
1581            .clone()
1582            .begin_transaction_nc()
1583            .await
1584            .find_by_prefix(&PegInTweakIndexPrefix)
1585            .await
1586            .filter(|(_k, v)| future::ready(v.operation_id == operation_id))
1587            .next()
1588            .await
1589            .ok_or_else(|| anyhow::format_err!("OperationId not found"))?
1590            .0
1591            .0)
1592    }
1593
1594    pub async fn get_pegin_tweak_idx(
1595        &self,
1596        tweak_idx: TweakIdx,
1597    ) -> anyhow::Result<PegInTweakIndexData> {
1598        self.client_ctx
1599            .module_db()
1600            .clone()
1601            .begin_transaction_nc()
1602            .await
1603            .get_value(&PegInTweakIndexKey(tweak_idx))
1604            .await
1605            .ok_or_else(|| anyhow::format_err!("TweakIdx not found"))
1606    }
1607
1608    pub async fn get_claimed_pegins(
1609        &self,
1610        dbtx: &mut DatabaseTransaction<'_>,
1611        tweak_idx: TweakIdx,
1612    ) -> Vec<(
1613        bitcoin::OutPoint,
1614        TransactionId,
1615        Vec<fedimint_core::OutPoint>,
1616    )> {
1617        let outpoints = dbtx
1618            .get_value(&PegInTweakIndexKey(tweak_idx))
1619            .await
1620            .map(|v| v.claimed)
1621            .unwrap_or_default();
1622
1623        let mut res = vec![];
1624
1625        for outpoint in outpoints {
1626            let claimed_peg_in_data = dbtx
1627                .get_value(&ClaimedPegInKey {
1628                    peg_in_index: tweak_idx,
1629                    btc_out_point: outpoint,
1630                })
1631                .await
1632                .expect("Must have a corresponding claim record");
1633            res.push((
1634                outpoint,
1635                claimed_peg_in_data.claim_txid,
1636                claimed_peg_in_data.change,
1637            ));
1638        }
1639
1640        res
1641    }
1642
1643    /// Like [`Self::recheck_pegin_address`] but by `operation_id`
1644    pub async fn recheck_pegin_address_by_op_id(
1645        &self,
1646        operation_id: OperationId,
1647    ) -> anyhow::Result<()> {
1648        let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1649
1650        self.recheck_pegin_address(tweak_idx).await
1651    }
1652
1653    /// Schedule given address for immediate re-check for deposits
1654    pub async fn recheck_pegin_address_by_address(
1655        &self,
1656        address: bitcoin::Address<NetworkUnchecked>,
1657    ) -> anyhow::Result<()> {
1658        self.recheck_pegin_address(self.find_tweak_idx_by_address(address).await?)
1659            .await
1660    }
1661
1662    /// Schedule given address for immediate re-check for deposits
1663    pub async fn recheck_pegin_address(&self, tweak_idx: TweakIdx) -> anyhow::Result<()> {
1664        self.db
1665            .autocommit(
1666                |dbtx, _| {
1667                    Box::pin(async {
1668                        let db_key = PegInTweakIndexKey(tweak_idx);
1669                        let db_val = dbtx
1670                            .get_value(&db_key)
1671                            .await
1672                            .ok_or_else(|| anyhow::format_err!("DBKey not found"))?;
1673
1674                        dbtx.insert_entry(
1675                            &db_key,
1676                            &PegInTweakIndexData {
1677                                next_check_time: Some(fedimint_core::time::now()),
1678                                ..db_val
1679                            },
1680                        )
1681                        .await;
1682
1683                        let sender = self.pegin_monitor_wakeup_sender.clone();
1684                        dbtx.on_commit(move || {
1685                            sender.send_replace(());
1686                        });
1687
1688                        Ok::<_, anyhow::Error>(())
1689                    })
1690                },
1691                Some(100),
1692            )
1693            .await?;
1694
1695        Ok(())
1696    }
1697
1698    /// Await for num deposit by [`OperationId`]
1699    pub async fn await_num_deposits_by_operation_id(
1700        &self,
1701        operation_id: OperationId,
1702        num_deposits: usize,
1703    ) -> anyhow::Result<()> {
1704        let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1705        self.await_num_deposits(tweak_idx, num_deposits).await
1706    }
1707
1708    pub async fn await_num_deposits_by_address(
1709        &self,
1710        address: bitcoin::Address<NetworkUnchecked>,
1711        num_deposits: usize,
1712    ) -> anyhow::Result<()> {
1713        self.await_num_deposits(self.find_tweak_idx_by_address(address).await?, num_deposits)
1714            .await
1715    }
1716
1717    #[instrument(target = LOG_CLIENT_MODULE_WALLET, skip_all, fields(tweak_idx=?tweak_idx, num_deposists=num_deposits))]
1718    pub async fn await_num_deposits(
1719        &self,
1720        tweak_idx: TweakIdx,
1721        num_deposits: usize,
1722    ) -> anyhow::Result<()> {
1723        let operation_id = self.get_pegin_tweak_idx(tweak_idx).await?.operation_id;
1724
1725        let mut receiver = self.pegin_claimed_receiver.clone();
1726        let mut backoff = backoff_util::aggressive_backoff();
1727
1728        loop {
1729            let pegins = self
1730                .get_claimed_pegins(
1731                    &mut self.client_ctx.module_db().begin_transaction_nc().await,
1732                    tweak_idx,
1733                )
1734                .await;
1735
1736            if pegins.len() < num_deposits {
1737                debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Not enough deposits");
1738                self.recheck_pegin_address(tweak_idx).await?;
1739                runtime::sleep(backoff.next().unwrap_or_default()).await;
1740                receiver.changed().await?;
1741                continue;
1742            }
1743
1744            debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Enough deposits detected");
1745
1746            for (_outpoint, transaction_id, change) in pegins {
1747                if transaction_id == TransactionId::from_byte_array([0; 32]) && change.is_empty() {
1748                    debug!(target: LOG_CLIENT_MODULE_WALLET, "Deposited amount was too low, skipping");
1749                    continue;
1750                }
1751
1752                debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring deposists claimed");
1753                let tx_subscriber = self.client_ctx.transaction_updates(operation_id).await;
1754
1755                if let Err(e) = tx_subscriber.await_tx_accepted(transaction_id).await {
1756                    bail!("{e}");
1757                }
1758
1759                debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring outputs claimed");
1760                self.client_ctx
1761                    .await_primary_module_outputs(operation_id, change)
1762                    .await
1763                    .expect("Cannot fail if tx was accepted and federation is honest");
1764            }
1765
1766            return Ok(());
1767        }
1768    }
1769
1770    /// Attempt to withdraw a given `amount` of Bitcoin to a destination
1771    /// `address`. The caller has to supply the fee rate to be used which can be
1772    /// fetched using [`Self::get_withdraw_fees`] and should be
1773    /// acknowledged by the user since it can be unexpectedly high.
1774    pub async fn withdraw<M: Serialize + MaybeSend + MaybeSync>(
1775        &self,
1776        address: &bitcoin::Address,
1777        amount: bitcoin::Amount,
1778        fee: PegOutFees,
1779        extra_meta: M,
1780    ) -> anyhow::Result<OperationId> {
1781        {
1782            let operation_id = OperationId(thread_rng().r#gen());
1783
1784            let withdraw_output =
1785                self.create_withdraw_output(operation_id, address.clone(), amount, fee)?;
1786            let tx_builder = TransactionBuilder::new()
1787                .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1788
1789            let extra_meta =
1790                serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1791            self.client_ctx
1792                .finalize_and_submit_transaction(
1793                    operation_id,
1794                    WalletCommonInit::KIND.as_str(),
1795                    {
1796                        let address = address.clone();
1797                        move |change_range: OutPointRange| WalletOperationMeta {
1798                            variant: WalletOperationMetaVariant::Withdraw {
1799                                address: address.clone().into_unchecked(),
1800                                amount,
1801                                fee,
1802                                change: change_range.into_iter().collect(),
1803                            },
1804                            extra_meta: extra_meta.clone(),
1805                        }
1806                    },
1807                    tx_builder,
1808                )
1809                .await?;
1810
1811            let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1812
1813            self.client_ctx
1814                .log_event(
1815                    &mut dbtx,
1816                    SendPaymentEvent {
1817                        operation_id,
1818                        amount: amount + fee.amount(),
1819                        fee: fee.amount(),
1820                    },
1821                )
1822                .await;
1823
1824            dbtx.commit_tx().await;
1825
1826            Ok(operation_id)
1827        }
1828    }
1829
1830    /// Attempt to increase the fee of a onchain withdraw transaction using
1831    /// replace by fee (RBF).
1832    /// This can prevent transactions from getting stuck
1833    /// in the mempool
1834    #[deprecated(
1835        since = "0.4.0",
1836        note = "RBF withdrawals are rejected by the federation"
1837    )]
1838    pub async fn rbf_withdraw<M: Serialize + MaybeSync + MaybeSend>(
1839        &self,
1840        rbf: Rbf,
1841        extra_meta: M,
1842    ) -> anyhow::Result<OperationId> {
1843        let operation_id = OperationId(thread_rng().r#gen());
1844
1845        let withdraw_output = self.create_rbf_withdraw_output(operation_id, &rbf)?;
1846        let tx_builder = TransactionBuilder::new()
1847            .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1848
1849        let extra_meta = serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1850        self.client_ctx
1851            .finalize_and_submit_transaction(
1852                operation_id,
1853                WalletCommonInit::KIND.as_str(),
1854                move |change_range: OutPointRange| WalletOperationMeta {
1855                    variant: WalletOperationMetaVariant::RbfWithdraw {
1856                        rbf: rbf.clone(),
1857                        change: change_range.into_iter().collect(),
1858                    },
1859                    extra_meta: extra_meta.clone(),
1860                },
1861                tx_builder,
1862            )
1863            .await?;
1864
1865        Ok(operation_id)
1866    }
1867
1868    pub async fn subscribe_withdraw_updates(
1869        &self,
1870        operation_id: OperationId,
1871    ) -> anyhow::Result<UpdateStreamOrOutcome<WithdrawState>> {
1872        let operation = self
1873            .client_ctx
1874            .get_operation(operation_id)
1875            .await
1876            .with_context(|| anyhow!("Operation not found: {}", operation_id.fmt_short()))?;
1877
1878        if operation.operation_module_kind() != WalletCommonInit::KIND.as_str() {
1879            bail!("Operation is not a wallet operation");
1880        }
1881
1882        let operation_meta = operation.meta::<WalletOperationMeta>();
1883
1884        let (WalletOperationMetaVariant::Withdraw { change, .. }
1885        | WalletOperationMetaVariant::RbfWithdraw { change, .. }) = operation_meta.variant
1886        else {
1887            bail!("Operation is not a withdraw operation");
1888        };
1889
1890        let mut operation_stream = self.notifier.subscribe(operation_id).await;
1891        let client_ctx = self.client_ctx.clone();
1892
1893        Ok(self
1894            .client_ctx
1895            .outcome_or_updates(operation, operation_id, move || {
1896                stream! {
1897                    match next_withdraw_state(&mut operation_stream).await {
1898                        Some(WithdrawStates::Created(_)) => {
1899                            yield WithdrawState::Created;
1900                        },
1901                        Some(s) => {
1902                            panic!("Unexpected state {s:?}")
1903                        },
1904                        None => return,
1905                    }
1906
1907                    // TODO: get rid of awaiting change here, there has to be a better way to make tests deterministic
1908
1909                        // Swallowing potential errors since the transaction failing  is handled by
1910                        // output outcome fetching already
1911                        let _ = client_ctx
1912                            .await_primary_module_outputs(operation_id, change)
1913                            .await;
1914
1915
1916                    match next_withdraw_state(&mut operation_stream).await {
1917                        Some(WithdrawStates::Aborted(inner)) => {
1918                            yield WithdrawState::Failed(inner.error);
1919                        },
1920                        Some(WithdrawStates::Success(inner)) => {
1921                            yield WithdrawState::Succeeded(inner.txid);
1922                        },
1923                        Some(s) => {
1924                            panic!("Unexpected state {s:?}")
1925                        },
1926                        None => {},
1927                    }
1928                }
1929            }))
1930    }
1931
1932    fn admin_auth(&self) -> anyhow::Result<ApiAuth> {
1933        self.admin_auth
1934            .clone()
1935            .ok_or_else(|| anyhow::format_err!("Admin auth not set"))
1936    }
1937
1938    pub async fn activate_consensus_version_voting(&self) -> anyhow::Result<()> {
1939        self.module_api
1940            .activate_consensus_version_voting(self.admin_auth()?)
1941            .await?;
1942
1943        Ok(())
1944    }
1945}
1946
1947/// Polls the federation checking if the activated module consensus version
1948/// supports safe deposits, saving the result in the db once it does.
1949async fn poll_supports_safe_deposit_version(db: Database, module_api: DynModuleApi) {
1950    loop {
1951        let mut dbtx = db.begin_transaction().await;
1952
1953        if dbtx.get_value(&SupportsSafeDepositKey).await.is_some() {
1954            break;
1955        }
1956
1957        module_api.wait_for_initialized_connections().await;
1958
1959        if let Ok(module_consensus_version) = module_api.module_consensus_version().await
1960            && SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION <= module_consensus_version
1961        {
1962            dbtx.insert_new_entry(&SupportsSafeDepositKey, &()).await;
1963            dbtx.commit_tx().await;
1964            break;
1965        }
1966
1967        drop(dbtx);
1968
1969        if is_running_in_test_env() {
1970            // Even in tests we don't want to spam the federation with requests about it
1971            sleep(Duration::from_secs(10)).await;
1972        } else {
1973            sleep(Duration::from_hours(1)).await;
1974        }
1975    }
1976}
1977
1978/// Returns the child index to derive the next peg-in tweak key from.
1979async fn get_next_peg_in_tweak_child_id(dbtx: &mut DatabaseTransaction<'_>) -> TweakIdx {
1980    let index = dbtx
1981        .get_value(&NextPegInTweakIndexKey)
1982        .await
1983        .unwrap_or_default();
1984    dbtx.insert_entry(&NextPegInTweakIndexKey, &(index.next()))
1985        .await;
1986    index
1987}
1988
1989#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1990pub enum WalletClientStates {
1991    Deposit(DepositStateMachine),
1992    Withdraw(WithdrawStateMachine),
1993}
1994
1995impl IntoDynInstance for WalletClientStates {
1996    type DynType = DynState;
1997
1998    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1999        DynState::from_typed(instance_id, self)
2000    }
2001}
2002
2003impl State for WalletClientStates {
2004    type ModuleContext = WalletClientContext;
2005
2006    fn transitions(
2007        &self,
2008        context: &Self::ModuleContext,
2009        global_context: &DynGlobalClientContext,
2010    ) -> Vec<StateTransition<Self>> {
2011        match self {
2012            WalletClientStates::Deposit(sm) => {
2013                sm_enum_variant_translation!(
2014                    sm.transitions(context, global_context),
2015                    WalletClientStates::Deposit
2016                )
2017            }
2018            WalletClientStates::Withdraw(sm) => {
2019                sm_enum_variant_translation!(
2020                    sm.transitions(context, global_context),
2021                    WalletClientStates::Withdraw
2022                )
2023            }
2024        }
2025    }
2026
2027    fn operation_id(&self) -> OperationId {
2028        match self {
2029            WalletClientStates::Deposit(sm) => sm.operation_id(),
2030            WalletClientStates::Withdraw(sm) => sm.operation_id(),
2031        }
2032    }
2033}
2034
2035#[cfg(all(test, not(target_family = "wasm")))]
2036mod tests {
2037    use std::collections::BTreeSet;
2038    use std::sync::atomic::{AtomicBool, Ordering};
2039
2040    use super::*;
2041    use crate::backup::{
2042        RECOVER_NUM_IDX_ADD_TO_LAST_USED, RecoverScanOutcome, recover_scan_idxes_for_activity,
2043    };
2044
2045    #[allow(clippy::too_many_lines)] // shut-up clippy, it's a test
2046    #[tokio::test(flavor = "multi_thread")]
2047    async fn sanity_test_recover_inner() {
2048        {
2049            let last_checked = AtomicBool::new(false);
2050            let last_checked = &last_checked;
2051            assert_eq!(
2052                recover_scan_idxes_for_activity(
2053                    TweakIdx(0),
2054                    &BTreeSet::new(),
2055                    |cur_idx| async move {
2056                        Ok(match cur_idx {
2057                            TweakIdx(9) => {
2058                                last_checked.store(true, Ordering::SeqCst);
2059                                vec![]
2060                            }
2061                            TweakIdx(10) => panic!("Shouldn't happen"),
2062                            TweakIdx(11) => {
2063                                vec![0usize] /* just for type inference */
2064                            }
2065                            _ => vec![],
2066                        })
2067                    }
2068                )
2069                .await
2070                .unwrap(),
2071                RecoverScanOutcome {
2072                    last_used_idx: None,
2073                    new_start_idx: TweakIdx(RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2074                    tweak_idxes_with_pegins: BTreeSet::from([])
2075                }
2076            );
2077            assert!(last_checked.load(Ordering::SeqCst));
2078        }
2079
2080        {
2081            let last_checked = AtomicBool::new(false);
2082            let last_checked = &last_checked;
2083            assert_eq!(
2084                recover_scan_idxes_for_activity(
2085                    TweakIdx(0),
2086                    &BTreeSet::from([TweakIdx(1), TweakIdx(2)]),
2087                    |cur_idx| async move {
2088                        Ok(match cur_idx {
2089                            TweakIdx(1) => panic!("Shouldn't happen: already used (1)"),
2090                            TweakIdx(2) => panic!("Shouldn't happen: already used (2)"),
2091                            TweakIdx(11) => {
2092                                last_checked.store(true, Ordering::SeqCst);
2093                                vec![]
2094                            }
2095                            TweakIdx(12) => panic!("Shouldn't happen"),
2096                            TweakIdx(13) => {
2097                                vec![0usize] /* just for type inference */
2098                            }
2099                            _ => vec![],
2100                        })
2101                    }
2102                )
2103                .await
2104                .unwrap(),
2105                RecoverScanOutcome {
2106                    last_used_idx: Some(TweakIdx(2)),
2107                    new_start_idx: TweakIdx(2 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2108                    tweak_idxes_with_pegins: BTreeSet::from([])
2109                }
2110            );
2111            assert!(last_checked.load(Ordering::SeqCst));
2112        }
2113
2114        {
2115            let last_checked = AtomicBool::new(false);
2116            let last_checked = &last_checked;
2117            assert_eq!(
2118                recover_scan_idxes_for_activity(
2119                    TweakIdx(10),
2120                    &BTreeSet::new(),
2121                    |cur_idx| async move {
2122                        Ok(match cur_idx {
2123                            TweakIdx(10) => vec![()],
2124                            TweakIdx(19) => {
2125                                last_checked.store(true, Ordering::SeqCst);
2126                                vec![]
2127                            }
2128                            TweakIdx(20) => panic!("Shouldn't happen"),
2129                            _ => vec![],
2130                        })
2131                    }
2132                )
2133                .await
2134                .unwrap(),
2135                RecoverScanOutcome {
2136                    last_used_idx: Some(TweakIdx(10)),
2137                    new_start_idx: TweakIdx(10 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2138                    tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(10)])
2139                }
2140            );
2141            assert!(last_checked.load(Ordering::SeqCst));
2142        }
2143
2144        assert_eq!(
2145            recover_scan_idxes_for_activity(TweakIdx(0), &BTreeSet::new(), |cur_idx| async move {
2146                Ok(match cur_idx {
2147                    TweakIdx(6 | 15) => vec![()],
2148                    _ => vec![],
2149                })
2150            })
2151            .await
2152            .unwrap(),
2153            RecoverScanOutcome {
2154                last_used_idx: Some(TweakIdx(15)),
2155                new_start_idx: TweakIdx(15 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2156                tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(6), TweakIdx(15)])
2157            }
2158        );
2159        assert_eq!(
2160            recover_scan_idxes_for_activity(TweakIdx(10), &BTreeSet::new(), |cur_idx| async move {
2161                Ok(match cur_idx {
2162                    TweakIdx(8) => {
2163                        vec![()] /* for type inference only */
2164                    }
2165                    TweakIdx(9) => {
2166                        panic!("Shouldn't happen")
2167                    }
2168                    _ => vec![],
2169                })
2170            })
2171            .await
2172            .unwrap(),
2173            RecoverScanOutcome {
2174                last_used_idx: None,
2175                new_start_idx: TweakIdx(9 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2176                tweak_idxes_with_pegins: BTreeSet::from([])
2177            }
2178        );
2179        assert_eq!(
2180            recover_scan_idxes_for_activity(TweakIdx(10), &BTreeSet::new(), |cur_idx| async move {
2181                Ok(match cur_idx {
2182                    TweakIdx(9) => panic!("Shouldn't happen"),
2183                    TweakIdx(15) => vec![()],
2184                    _ => vec![],
2185                })
2186            })
2187            .await
2188            .unwrap(),
2189            RecoverScanOutcome {
2190                last_used_idx: Some(TweakIdx(15)),
2191                new_start_idx: TweakIdx(15 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2192                tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(15)])
2193            }
2194        );
2195    }
2196}