Skip to main content

fedimint_walletv2_client/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::must_use_candidate)]
5#![allow(clippy::module_name_repetitions)]
6
7pub use fedimint_walletv2_common as common;
8pub use fedimint_walletv2_common::MempoolVisibility;
9
10mod api;
11#[cfg(feature = "cli")]
12mod cli;
13pub mod db;
14pub mod events;
15mod receive_sm;
16mod send_sm;
17
18use std::collections::{BTreeMap, BTreeSet};
19use std::sync::Arc;
20use std::time::Duration;
21
22use api::WalletFederationApi;
23use bitcoin::address::NetworkUnchecked;
24use bitcoin::{Address, ScriptBuf};
25use db::{NextOutputIndexKey, ValidAddressIndexKey, ValidAddressIndexPrefix};
26use events::{ReceivePaymentEvent, SendPaymentEvent};
27use fedimint_api_client::api::{DynModuleApi, FederationError, FederationResult};
28use fedimint_client::DynGlobalClientContext;
29use fedimint_client::transaction::{
30    ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
31    ClientOutputSM, FeeQuote, FeeQuoteRequest, TransactionBuilder, max_affordable_send_amount,
32};
33use fedimint_client_module::db::ClientModuleMigrationFn;
34use fedimint_client_module::error::{
35    ClientModuleError, OperationLookupError, TransactionSubmitError,
36};
37use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
38use fedimint_client_module::module::recovery::NoModuleBackup;
39use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
40use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
41use fedimint_client_module::sm_enum_variant_translation;
42use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
43use fedimint_core::db::{
44    AutocommitError, Database, DatabaseError, DatabaseTransaction, DatabaseVersion,
45    IDatabaseTransactionOpsCoreTyped,
46};
47use fedimint_core::encoding::{Decodable, Encodable};
48use fedimint_core::module::{
49    AmountUnit, Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
50};
51use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup, TaskHandle, sleep};
52use fedimint_core::{Amount, OutPoint, TransactionId, apply, async_trait_maybe_send};
53use fedimint_derive_secret::{ChildId, DerivableSecret};
54use fedimint_eventlog::{Event, EventLogId};
55use fedimint_logging::LOG_CLIENT_MODULE_WALLETV2;
56use fedimint_walletv2_common::config::WalletClientConfig;
57use fedimint_walletv2_common::{
58    BLOCK_COUNT_VOTE_DELAY, KIND, OutputInfo, PendingOutput, StandardScript, TxInfo,
59    WalletCommonInit, WalletInput, WalletInputV0, WalletModuleTypes, WalletOutput, WalletOutputV0,
60    descriptor, is_potential_receive,
61};
62use futures::StreamExt;
63use receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
64use secp256k1::Keypair;
65use send_sm::{SendSMCommon, SendSMState, SendStateMachine};
66use serde::{Deserialize, Serialize};
67use strum::IntoEnumIterator as _;
68use thiserror::Error;
69use tracing::{debug, warn};
70
71/// Number of output info entries to scan per batch.
72const SLICE_SIZE: u64 = 1000;
73
74/// Number of event log entries to read per batch.
75const EVENT_LOG_PAGE_SIZE: u64 = 1000;
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub enum WalletOperationMeta {
79    Send(SendMeta),
80    Receive(ReceiveMeta),
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SendMeta {
85    pub change_outpoint_range: OutPointRange,
86    pub address: Address<NetworkUnchecked>,
87    pub value: bitcoin::Amount,
88    pub fee: bitcoin::Amount,
89    #[serde(default)]
90    pub custom_meta: serde_json::Value,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ReceiveMeta {
95    pub change_outpoint_range: OutPointRange,
96    pub value: bitcoin::Amount,
97    pub fee: bitcoin::Amount,
98    pub address: Option<Address<NetworkUnchecked>>,
99    pub outpoint: Option<bitcoin::OutPoint>,
100}
101
102/// The final state of an operation sending bitcoin onchain.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub enum FinalSendOperationState {
105    /// The transaction was successful.
106    Success(bitcoin::Txid),
107    /// The funding transaction was aborted.
108    Aborted,
109    /// A programming error has occurred or the federation is malicious.
110    Failure,
111}
112
113/// The final state of an operation receiving bitcoin onchain.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub enum FinalReceiveOperationState {
116    /// The federation accepted the claiming transaction.
117    Success,
118    /// The federation rejected the claiming transaction.
119    Aborted,
120}
121
122/// Progress of a single incoming peg-in.
123///
124/// Each variant describes one deposit, identified by the outpoint it is looked
125/// up or paired with. An address that has been paid more than once has one of
126/// these per payment rather than a combined view, so nothing is aggregated
127/// away.
128///
129/// There is no "nothing has arrived" variant: a deposit no guardian has seen
130/// has no outpoint to describe it, so absence is expressed by
131/// [`WalletClientModule::address_receive_progress`] reporting no deposits.
132///
133/// Every variant is derived from advisory, non-consensus data reported by
134/// individual guardians, so none of them is authoritative and progress may move
135/// backwards: a reorg or an eviction can take a deposit back to being unseen
136/// entirely. No variant means money has been received - a deposit is only
137/// actually claimed once [`WalletClientModule::await_receive`] returns, and an
138/// output is deliberately still reported here for a short while after that.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub enum ReceiveProgress {
141    /// The deposit is in a guardian's mempool but is not mined.
142    ///
143    /// The weakest signal there is: an unmined transaction can be replaced or
144    /// evicted, and a deposit in this state can disappear again. Treat it as
145    /// "we have seen your payment", never as money received.
146    ///
147    /// Only guardians whose bitcoin backend can enumerate a mempool report
148    /// these, so on a federation without one a deposit stays unseen until it is
149    /// mined.
150    Mempool { value: bitcoin::Amount },
151    /// The deposit has been mined but is not yet deep enough for the federation
152    /// to act on it.
153    Confirming {
154        value: bitcoin::Amount,
155        /// Height of the block that mined the deposit.
156        ///
157        /// Reported as the guardians saw it, so a reorg can move a deposit to a
158        /// different height or back to being unseen entirely.
159        height: u64,
160        /// Confirmations counting the block that mined the transaction as the
161        /// first, matching what a block explorer displays.
162        confirmations: u64,
163        /// Confirmations needed before the federation records the output.
164        required: u64,
165    },
166}
167
168/// What the federation currently reports about the deposits to one address.
169///
170/// Returned by [`WalletClientModule::address_receive_progress`]. The two fields
171/// are only meaningful together: they come from a single query, so the
172/// visibility flag describes exactly the view the deposits were read from.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct AddressReceiveProgress {
175    /// One entry per deposit the guardians can see, ordered by outpoint.
176    pub deposits: Vec<(bitcoin::OutPoint, ReceiveProgress)>,
177    /// The best mempool view any guardian had for this query.
178    ///
179    /// With [`MempoolVisibility::Complete`], empty `deposits` mean no
180    /// matching deposit was observed in the reported snapshot. That is not
181    /// proof that no payment was sent: a guardian only inspects its own
182    /// mempool, which a freshly broadcast transaction may not have reached
183    /// yet, and a deposit mined long enough ago has left the reported block
184    /// window. With [`MempoolVisibility::Partial`] the unmined deposits
185    /// reported are real, but one may also be missing because a guardian
186    /// failed to fetch its transaction or has not reached it yet while still
187    /// working through a large mempool, which is normal progress rather than
188    /// a backend fault; with
189    /// [`MempoolVisibility::Unavailable`] the federation is blind to a
190    /// payment until it is mined. One guardian that inspected its whole
191    /// mempool is enough to make this `Complete` for everyone.
192    ///
193    /// A wallet showing a receive screen wants this to choose between "we have
194    /// not seen your payment yet" and "waiting for the first confirmation",
195    /// which are very different things to tell a user who has just paid.
196    pub mempool_visibility: MempoolVisibility,
197}
198
199#[derive(Debug, Clone)]
200pub struct WalletClientModule {
201    root_secret: DerivableSecret,
202    cfg: WalletClientConfig,
203    notifier: ModuleNotifier<WalletClientStateMachines>,
204    client_ctx: ClientContext<Self>,
205    db: Database,
206    module_api: DynModuleApi,
207}
208
209#[derive(Debug, Clone)]
210pub struct WalletClientContext {
211    pub client_ctx: ClientContext<WalletClientModule>,
212}
213
214impl Context for WalletClientContext {
215    const KIND: Option<ModuleKind> = Some(KIND);
216}
217
218#[apply(async_trait_maybe_send!)]
219impl ClientModule for WalletClientModule {
220    type Init = WalletClientInit;
221    type Common = WalletModuleTypes;
222    type Backup = NoModuleBackup;
223    type ModuleStateMachineContext = WalletClientContext;
224    type States = WalletClientStateMachines;
225
226    fn context(&self) -> Self::ModuleStateMachineContext {
227        WalletClientContext {
228            client_ctx: self.client_ctx.clone(),
229        }
230    }
231
232    fn input_fee(
233        &self,
234        amount: &Amounts,
235        _input: &<Self::Common as ModuleCommon>::Input,
236    ) -> Option<Amounts> {
237        amount
238            .get(&AmountUnit::BITCOIN)
239            .map(|a| Amounts::new_bitcoin(self.cfg.fee_consensus.fee(*a)))
240    }
241
242    fn output_fee(
243        &self,
244        amount: &Amounts,
245        _output: &<Self::Common as ModuleCommon>::Output,
246    ) -> Option<Amounts> {
247        amount
248            .get(&AmountUnit::BITCOIN)
249            .map(|a| Amounts::new_bitcoin(self.cfg.fee_consensus.fee(*a)))
250    }
251
252    #[cfg(feature = "cli")]
253    async fn handle_cli_command(
254        &self,
255        args: &[std::ffi::OsString],
256    ) -> Result<serde_json::Value, ClientModuleError> {
257        cli::handle_cli_command(self, args)
258            .await
259            .map_err(ClientModuleError::other)
260    }
261}
262
263#[derive(Debug, Clone, Default)]
264pub struct WalletClientInit;
265
266impl ModuleInit for WalletClientInit {
267    type Common = WalletCommonInit;
268
269    async fn dump_database(
270        &self,
271        _dbtx: &mut DatabaseTransaction<'_>,
272        _prefix_names: Vec<String>,
273    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
274        Box::new(BTreeMap::new().into_iter())
275    }
276}
277
278#[apply(async_trait_maybe_send!)]
279impl ClientModuleInit for WalletClientInit {
280    type Module = WalletClientModule;
281
282    fn supported_api_versions(&self) -> MultiApiVersion {
283        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
284            .expect("no version conflicts")
285    }
286
287    async fn init(
288        &self,
289        args: &ClientModuleInitArgs<Self>,
290    ) -> Result<Self::Module, ClientModuleError> {
291        let module = WalletClientModule {
292            root_secret: args.module_root_secret().clone(),
293            cfg: args.cfg().clone(),
294            notifier: args.notifier().clone(),
295            client_ctx: args.context(),
296            db: args.db().clone(),
297            module_api: args.module_api().clone(),
298        };
299
300        module.spawn_output_scanner(args.task_group(), args.client_span());
301
302        Ok(module)
303    }
304
305    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
306        BTreeMap::new()
307    }
308
309    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
310        Some(db::DbKeyPrefix::iter().map(|p| p as u8).collect())
311    }
312}
313
314impl WalletClientModule {
315    /// Returns the Bitcoin network for this federation.
316    pub fn get_network(&self) -> bitcoin::Network {
317        self.cfg.network
318    }
319
320    /// Fetch the total value of bitcoin controlled by the federation.
321    pub async fn total_value(&self) -> FederationResult<bitcoin::Amount> {
322        self.module_api
323            .federation_wallet()
324            .await
325            .map(|tx_out| tx_out.map_or(bitcoin::Amount::ZERO, |tx_out| tx_out.value))
326    }
327
328    /// Fetch the consensus block count of the federation.
329    pub async fn block_count(&self) -> FederationResult<u64> {
330        self.module_api.consensus_block_count().await
331    }
332
333    /// Fetch the current consensus feerate.
334    pub async fn feerate(&self) -> FederationResult<Option<u64>> {
335        self.module_api.consensus_feerate().await
336    }
337
338    /// Fetch information on the chain of pending bitcoin transactions.
339    pub async fn pending_tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
340        self.module_api.pending_tx_chain().await
341    }
342
343    /// Display log of bitcoin transactions.
344    pub async fn tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
345        self.module_api.tx_chain().await
346    }
347
348    /// Fetch the current fee required to send an onchain payment.
349    pub async fn send_fee(&self) -> Result<bitcoin::Amount, SendError> {
350        self.module_api
351            .send_fee()
352            .await
353            .map_err(|e| SendError::Federation(Box::new(e)))?
354            .ok_or(SendError::NoConsensusFeerateAvailable)
355    }
356
357    /// Computes the federation fee an onchain send of an output worth `amount`
358    /// (the payment amount plus the on-chain miner fee) would incur, without
359    /// submitting anything.
360    ///
361    /// A send submits a single wallet output worth `amount`; the primary module
362    /// balances it by spending ecash to fund the output and minting any change.
363    /// This quotes the fee of that transaction — the wallet output fee, the
364    /// mint input fees on the funding notes, any mint change output fees,
365    /// and sub-denomination dust — via the shared, module-agnostic fee
366    /// quote.
367    ///
368    /// The on-chain Bitcoin miner fee is deliberately excluded: it is part of
369    /// the output `amount` (see [`Self::send_fee`]), not the on-federation
370    /// transaction fee.
371    pub async fn send_fee_quote(
372        &self,
373        amount: bitcoin::Amount,
374    ) -> Result<FeeQuote, TransactionSubmitError> {
375        let amount = Amount::from_sats(amount.to_sat());
376        self.client_ctx
377            .fee_quote(
378                OperationId::new_random(),
379                FeeQuoteRequest {
380                    input_amount: Amounts::ZERO,
381                    output_amount: Amounts::new_bitcoin(amount),
382                    input_fee: Amounts::ZERO,
383                    output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
384                },
385            )
386            .await
387    }
388
389    /// Finds the largest value that can be sent on chain in full out of
390    /// `balance` — the amount a "send everything" sweep should use.
391    ///
392    /// Sending `value` costs `value + fee` (the on-chain miner fee is carried
393    /// inside the wallet output, see [`Self::send`]) *plus* the federation fee
394    /// of funding that output — the wallet output fee, the mint input fees on
395    /// the funding notes, any mint change output fees and sub-denomination
396    /// dust — as quoted by [`Self::send_fee_quote`]. This returns the largest
397    /// `value` satisfying
398    ///
399    /// ```text
400    /// value + fee + send_fee_quote(value + fee).total() <= balance
401    /// ```
402    ///
403    /// `balance` is the client's current Bitcoin balance (e.g. from
404    /// `Client::get_balance_for_btc`). `fee` is the on-chain fee from
405    /// [`Self::send_fee`]; pass the *same* value on to [`Self::send`], since
406    /// the required feerate rises with each pending federation transaction and
407    /// a value computed against a stale fee would be rejected.
408    ///
409    /// The maximum is found by binary search over the real fee quote (see
410    /// [`max_affordable_send_amount`]) rather than by subtracting a single
411    /// quote: the federation fee is charged per note, so note selection,
412    /// denomination rounding, change and dust move it in steps as the value
413    /// crosses thresholds, and a quote taken at the full balance would fail
414    /// outright — funding it is the very thing that is unaffordable.
415    ///
416    /// The quote is point-in-time and moves with the balance; [`Self::send`]
417    /// remains the source of truth. Note that it cannot account for the
418    /// federation's own on-chain constraints — a send whose change UTXO would
419    /// fall below the dust limit is still rejected by the guardians.
420    ///
421    /// Returns [`SendError::InsufficientFunds`] if the balance cannot cover
422    /// the dust limit plus fees, or [`SendError::Failed`] if the fee probe
423    /// itself failed.
424    pub async fn max_sendable_amount(
425        &self,
426        balance: Amount,
427        fee: bitcoin::Amount,
428    ) -> Result<bitcoin::Amount, SendError> {
429        let fee_msats = Amount::from_sats(fee.to_sat());
430
431        let max = max_affordable_send_amount(
432            balance,
433            Amount::from_sats(self.cfg.dust_limit.to_sat()),
434            balance,
435            // The solver searches millisatoshis, but a send funds a whole
436            // number of satoshis. Rounding the probe up to the next satoshi
437            // keeps the predicate conservative and makes the value handed to
438            // the quote below an exact satoshi multiple.
439            |value: Amount| Amount::from_sats(value.msats.div_ceil(1000)) + fee_msats,
440            |funded: Amount| self.send_fee_quote(bitcoin::Amount::from_sat(funded.msats / 1000)),
441        )
442        .await
443        .map_err(SendError::Failed)?
444        .ok_or(SendError::InsufficientFunds)?;
445
446        // `gross_up` rounded up to whole satoshis, so the largest affordable
447        // amount already sits on a satoshi boundary; no value is lost here.
448        Ok(bitcoin::Amount::from_sat(max.msats.div_ceil(1000)))
449    }
450
451    /// Fetch the current fee required to claim an onchain deposit (peg-in).
452    pub async fn receive_fee(&self) -> Result<bitcoin::Amount, ReceiveError> {
453        self.module_api
454            .receive_fee()
455            .await
456            .map_err(|e| ReceiveError::Federation(Box::new(e)))?
457            .ok_or(ReceiveError::NoConsensusFeerateAvailable)
458    }
459
460    /// Send an onchain payment with the given fee.
461    pub async fn send(
462        &self,
463        address: Address<NetworkUnchecked>,
464        value: bitcoin::Amount,
465        fee: Option<bitcoin::Amount>,
466        custom_meta: serde_json::Value,
467    ) -> Result<OperationId, SendError> {
468        if !address.is_valid_for_network(self.cfg.network) {
469            return Err(SendError::WrongNetwork);
470        }
471
472        if value < self.cfg.dust_limit {
473            return Err(SendError::DustValue);
474        }
475
476        let fee = match fee {
477            Some(value) => value,
478            None => self
479                .module_api
480                .send_fee()
481                .await
482                .map_err(|e| SendError::Federation(Box::new(e)))?
483                .ok_or(SendError::NoConsensusFeerateAvailable)?,
484        };
485
486        let operation_id = OperationId::new_random();
487
488        let destination = StandardScript::from_address(&address.clone().assume_checked())
489            .ok_or(SendError::UnsupportedAddress)?;
490
491        let client_output = ClientOutput::<WalletOutput> {
492            output: WalletOutput::V0(WalletOutputV0 {
493                destination,
494                value,
495                fee,
496            }),
497            amounts: Amounts::new_bitcoin(Amount::from_sats((value + fee).to_sat())),
498        };
499
500        let client_output_sm = ClientOutputSM::<WalletClientStateMachines> {
501            state_machines: Arc::new(move |range: OutPointRange| {
502                vec![WalletClientStateMachines::Send(SendStateMachine {
503                    common: SendSMCommon {
504                        operation_id,
505                        outpoint: OutPoint {
506                            txid: range.txid(),
507                            out_idx: 0,
508                        },
509                        value,
510                        fee,
511                    },
512                    state: SendSMState::Funding,
513                })]
514            }),
515        };
516
517        let client_output_bundle = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
518            vec![client_output],
519            vec![client_output_sm],
520        ));
521
522        let address_clone = address.clone();
523
524        self.submit_with_event(
525            operation_id,
526            move |change_outpoint_range| {
527                WalletOperationMeta::Send(SendMeta {
528                    change_outpoint_range,
529                    address: address_clone.clone(),
530                    value,
531                    fee,
532                    custom_meta: custom_meta.clone(),
533                })
534            },
535            TransactionBuilder::new().with_outputs(client_output_bundle),
536            SendPaymentEvent {
537                operation_id,
538                address,
539                value,
540                fee,
541            },
542        )
543        .await
544        .map_err(|error| match error {
545            TransactionSubmitError::InsufficientFunds(_) => SendError::InsufficientFunds,
546            error => SendError::Failed(error),
547        })?;
548
549        Ok(operation_id)
550    }
551
552    /// Await the final state of the send operation.
553    pub async fn await_final_send_operation_state(
554        &self,
555        operation_id: OperationId,
556    ) -> Result<FinalSendOperationState, OperationLookupError> {
557        let operation = self.client_ctx.get_operation(operation_id).await?;
558        let mut stream = self.notifier.subscribe(operation_id).await;
559
560        let mut stream = self
561            .client_ctx
562            .outcome_or_updates(&operation, operation_id, |_| true, move || {
563                async_stream::stream! {
564                    loop {
565                        if let Some(WalletClientStateMachines::Send(state)) = stream.next().await {
566                            match state.state {
567                                SendSMState::Funding => {}
568                                SendSMState::Success(txid) => {
569                                    yield FinalSendOperationState::Success(txid);
570                                    return;
571                                }
572                                SendSMState::Aborted(..) => {
573                                    yield FinalSendOperationState::Aborted;
574                                    return;
575                                }
576                                SendSMState::Failure => {
577                                    yield FinalSendOperationState::Failure;
578                                    return;
579                                }
580                            }
581                        }
582                    }
583                }
584            })
585            .into_stream();
586
587        let mut final_state = None;
588
589        while let Some(state) = stream.next().await {
590            final_state = Some(state);
591        }
592
593        Ok(final_state.expect("Stream contains one final state"))
594    }
595
596    /// Await the final state of the receive operation.
597    pub async fn await_final_receive_operation_state(
598        &self,
599        operation_id: OperationId,
600    ) -> Result<FinalReceiveOperationState, OperationLookupError> {
601        let operation = self.client_ctx.get_operation(operation_id).await?;
602        let mut stream = self.notifier.subscribe(operation_id).await;
603
604        let mut stream = self
605            .client_ctx
606            .outcome_or_updates(&operation, operation_id, |_| true, move || {
607                async_stream::stream! {
608                    loop {
609                        if let Some(WalletClientStateMachines::Receive(state)) = stream.next().await {
610                            match state.state {
611                                ReceiveSMState::Funding => {}
612                                ReceiveSMState::Success => {
613                                    yield FinalReceiveOperationState::Success;
614                                    return;
615                                }
616                                ReceiveSMState::Aborted(..) => {
617                                    yield FinalReceiveOperationState::Aborted;
618                                    return;
619                                }
620                            }
621                        }
622                    }
623                }
624            })
625            .into_stream();
626
627        let mut final_state = None;
628
629        while let Some(state) = stream.next().await {
630            final_state = Some(state);
631        }
632
633        Ok(final_state.expect("Stream contains one final state"))
634    }
635
636    /// Resolves one of our receive addresses back to the index it was derived
637    /// at.
638    ///
639    /// Only addresses handed out by [`Self::receive`] resolve; the set of valid
640    /// indices is small, so this is a short scan rather than a stored reverse
641    /// index.
642    async fn address_index(&self, address: &Address) -> Option<u64> {
643        let script = address.script_pubkey();
644
645        let indices: Vec<u64> = self
646            .db
647            .begin_transaction_nc()
648            .await
649            .find_by_prefix(&ValidAddressIndexPrefix)
650            .await
651            .map(|entry| entry.0.0)
652            .collect()
653            .await;
654
655        indices
656            .into_iter()
657            .find(|index| self.derive_address(*index).script_pubkey() == script)
658    }
659
660    /// The current progress of the deposit at `outpoint`.
661    ///
662    /// Reads only the guardians' pending view, which is advisory and may move
663    /// backwards, and which cannot tell a claimed deposit from a reorged one.
664    /// Use [`Self::await_receive`] to establish that a deposit has actually
665    /// been claimed.
666    ///
667    /// Each call queries the federation, so poll it sparingly.
668    ///
669    /// Errors if no deposit at `outpoint` is known to this client, which
670    /// includes one that has been reorged out or evicted since it was last
671    /// seen, and one paying an address this client did not derive.
672    pub async fn receive_progress(
673        &self,
674        outpoint: bitcoin::OutPoint,
675    ) -> Result<ReceiveProgress, ReceiveProgressError> {
676        let pending = self.module_api.pending_outputs().await;
677
678        // Guardians report every output passing the receive filter, most of
679        // which belong to other clients, so ownership is established by matching
680        // the script against our own addresses.
681        let ours = self.address_map().await;
682
683        pending
684            .outputs
685            .iter()
686            .find(|output| output.outpoint == outpoint && ours.contains_key(&output.script))
687            .map(|output| Self::pending_progress(output, pending.block_count))
688            .ok_or(ReceiveProgressError::UnknownDeposit)
689    }
690
691    /// The progress of every deposit to `address` the federation has seen,
692    /// ordered by outpoint.
693    ///
694    /// Empty deposits mean no guardian has reported a payment to the address
695    /// yet. Even when [`AddressReceiveProgress::mempool_visibility`] is
696    /// [`MempoolVisibility::Complete`], that only means no matching deposit
697    /// was observed in the reported snapshot, never proof that no payment was
698    /// sent: a broadcast transaction may not have reached the guardians'
699    /// mempools yet. Without `Complete` an unmined deposit may be invisible
700    /// to the federation altogether.
701    ///
702    /// Errors if `address` was not handed out by [`Self::receive`].
703    pub async fn address_receive_progress(
704        &self,
705        address: &Address,
706    ) -> Result<AddressReceiveProgress, ReceiveProgressError> {
707        self.address_index(address)
708            .await
709            .ok_or(ReceiveProgressError::UnknownAddress)?;
710
711        Ok(self.deposits_paying(&address.script_pubkey()).await)
712    }
713
714    /// Every deposit the federation currently reports against `script`.
715    async fn deposits_paying(&self, script: &ScriptBuf) -> AddressReceiveProgress {
716        let pending = self.module_api.pending_outputs().await;
717
718        let mut deposits: Vec<(bitcoin::OutPoint, ReceiveProgress)> = pending
719            .outputs
720            .iter()
721            .filter(|output| output.script == *script)
722            .map(|output| {
723                (
724                    output.outpoint,
725                    Self::pending_progress(output, pending.block_count),
726                )
727            })
728            .collect();
729
730        deposits.sort_by_key(|(outpoint, _)| *outpoint);
731
732        AddressReceiveProgress {
733            deposits,
734            mempool_visibility: pending.mempool_visibility,
735        }
736    }
737
738    /// The scripts of every receive address derived so far, by index.
739    async fn address_map(&self) -> BTreeMap<ScriptBuf, u64> {
740        self.db
741            .begin_transaction_nc()
742            .await
743            .find_by_prefix(&ValidAddressIndexPrefix)
744            .await
745            .map(|entry| entry.0.0)
746            .map(|index| (self.derive_address(index).script_pubkey(), index))
747            .collect()
748            .await
749    }
750
751    fn pending_progress(output: &PendingOutput, block_count: u64) -> ReceiveProgress {
752        // Block count votes trail the chain tip by the vote delay, and an output
753        // is only recorded once the consensus block count is past the block that
754        // mined it, so a peg-in is claimable one standard confirmation later than
755        // the vote delay itself.
756        let required = BLOCK_COUNT_VOTE_DELAY + 1;
757
758        match output.height {
759            Some(height) => ReceiveProgress::Confirming {
760                value: output.value,
761                height,
762                // Confirmations count the block that mined the deposit as the
763                // first, matching what a block explorer displays.
764                //
765                // Guardians keep reporting an output for a while after it turns
766                // final, so that progress does not blank out before the claim
767                // begins. Clamp so a progress display cannot run past its own
768                // target during that overlap.
769                confirmations: block_count.saturating_sub(height).min(required),
770                required,
771            },
772            None => ReceiveProgress::Mempool {
773                value: output.value,
774            },
775        }
776    }
777
778    /// Returns the highest valid receive address index that the background
779    /// scanner has derived so far, or `None` if it has not derived one yet.
780    async fn valid_index(&self) -> Option<u64> {
781        self.db
782            .begin_transaction_nc()
783            .await
784            .find_by_prefix_sorted_descending(&ValidAddressIndexPrefix)
785            .await
786            .next()
787            .await
788            .map(|entry| entry.0.0)
789    }
790
791    /// Returns the next unused receive address.
792    ///
793    /// To wait for a payment to this address race-free, read the client's
794    /// current event log position (via the global `get_next_event_log_id`)
795    /// *before* calling this, then pass that position to
796    /// [`Self::await_receive`]; it will only consider payments received
797    /// after that position.
798    ///
799    /// If the background scanner has already derived a valid address index this
800    /// returns immediately. Otherwise it blocks, letting the scanner grind
801    /// until it finds the next valid index, and returns once one is
802    /// available.
803    pub async fn receive(&self) -> Address {
804        loop {
805            if let Some(index) = self.valid_index().await {
806                return self.derive_address(index);
807            }
808
809            sleep(Duration::from_secs(1)).await;
810        }
811    }
812
813    /// Block until the next on-chain payment recorded at or after `position` is
814    /// received and successfully claimed by the federation.
815    ///
816    /// Returns the peg-in's final state together with the event log position
817    /// just past it, so that a subsequent call can resume from there to wait
818    /// for the following receive.
819    ///
820    /// A peg-in attempt may be aborted (rejected by the federation), in which
821    /// case the still-unspent output is reprocessed into a new receive
822    /// operation; this keeps waiting until one succeeds.
823    pub async fn await_receive(
824        &self,
825        position: EventLogId,
826    ) -> Result<(FinalReceiveOperationState, EventLogId), AwaitReceiveError> {
827        let mut position = position;
828
829        loop {
830            let (operation_id, next_position) = self.next_receive_operation(position).await;
831
832            position = next_position;
833
834            let state = self
835                .await_final_receive_operation_state(operation_id)
836                .await?;
837
838            // A successful peg-in is terminal; an aborted one is retried as a
839            // new receive operation, so keep waiting.
840            if state == FinalReceiveOperationState::Success {
841                // Reaching `Success` only means the peg-in claim transaction was
842                // accepted into consensus. The ecash it mints is issued
843                // asynchronously by the primary module, so wait for those
844                // outputs before returning; otherwise the freshly claimed funds
845                // may not yet be reflected in the client's balance.
846                let operation = self.client_ctx.get_operation(operation_id).await?;
847
848                if let WalletOperationMeta::Receive(ReceiveMeta {
849                    change_outpoint_range,
850                    ..
851                }) = operation.meta::<WalletOperationMeta>()
852                {
853                    self.client_ctx
854                        .await_primary_module_outputs(
855                            operation_id,
856                            change_outpoint_range.into_iter().collect(),
857                        )
858                        .await?;
859                }
860
861                return Ok((state, position));
862            }
863        }
864    }
865
866    /// Scan the event log from `position` for the next [`ReceivePaymentEvent`],
867    /// blocking until one is found, and return its operation id together with
868    /// the event log position just past it.
869    async fn next_receive_operation(&self, position: EventLogId) -> (OperationId, EventLogId) {
870        let mut position = position;
871
872        loop {
873            let events = self
874                .client_ctx
875                .get_event_log(Some(position), EVENT_LOG_PAGE_SIZE)
876                .await;
877
878            for entry in &events {
879                position = entry.id().saturating_add(1);
880
881                if entry.module_kind() == Some(&KIND)
882                    && entry.kind == ReceivePaymentEvent::KIND
883                    && let Some(event) = entry.to_event::<ReceivePaymentEvent>()
884                {
885                    return (event.operation_id, position);
886                }
887            }
888
889            if events.is_empty() {
890                // Caught up with the log; wait for new events to be written.
891                sleep(Duration::from_secs(1)).await;
892            }
893        }
894    }
895
896    fn derive_address(&self, index: u64) -> Address {
897        descriptor(
898            &self.cfg.bitcoin_pks,
899            &self.derive_tweak(index).public_key().consensus_hash(),
900        )
901        .address(self.cfg.network)
902    }
903
904    fn derive_tweak(&self, index: u64) -> Keypair {
905        self.root_secret
906            .child_key(ChildId(index))
907            .to_secp_key(secp256k1::SECP256K1)
908    }
909
910    /// Find the next valid index starting from (and including) `start_index`.
911    ///
912    /// Only ~1/65536 indices are valid, so the search is CPU-bound and may scan
913    /// many indices before finding one. The scan runs in bounded batches and
914    /// yields to the executor between them, so it does not stall the runtime —
915    /// important on wasm, which is single-threaded. It stops and returns `None`
916    /// once the task group begins shutting down.
917    async fn next_valid_index(&self, start_index: u64, handle: &TaskHandle) -> Option<u64> {
918        /// Indices to scan per batch before yielding to the executor.
919        const SCAN_BATCH: u64 = 256;
920
921        let pks_hash = self.cfg.bitcoin_pks.consensus_hash();
922
923        let mut index = start_index;
924
925        while !handle.is_shutting_down() {
926            for _ in 0..SCAN_BATCH {
927                if is_potential_receive(&self.derive_address(index).script_pubkey(), &pks_hash) {
928                    return Some(index);
929                }
930
931                index += 1;
932            }
933
934            // Hand control back to the executor between batches.
935            sleep(Duration::ZERO).await;
936        }
937
938        None
939    }
940
941    /// Issue ecash for an unspent output with a given fee.
942    ///
943    /// Returns `None` if the output value cannot cover the fee, or if the
944    /// remainder is too small to fund the claim transaction's fees.
945    async fn receive_output(
946        &self,
947        output_index: u64,
948        value: bitcoin::Amount,
949        address_index: u64,
950        fee: bitcoin::Amount,
951        outpoint: Option<bitcoin::OutPoint>,
952    ) -> Option<(OperationId, TransactionId)> {
953        let operation_id = OperationId::new_random();
954
955        let client_input = ClientInput::<WalletInput> {
956            input: WalletInput::V0(WalletInputV0 {
957                output_index,
958                fee,
959                tweak: self.derive_tweak(address_index).public_key(),
960            }),
961            keys: vec![self.derive_tweak(address_index)],
962            amounts: Amounts::new_bitcoin(Amount::from_sats(value.checked_sub(fee)?.to_sat())),
963        };
964
965        let client_input_sm = ClientInputSM::<WalletClientStateMachines> {
966            state_machines: Arc::new(move |range: OutPointRange| {
967                vec![WalletClientStateMachines::Receive(ReceiveStateMachine {
968                    common: ReceiveSMCommon {
969                        operation_id,
970                        txid: range.txid(),
971                        value,
972                        fee,
973                    },
974                    state: ReceiveSMState::Funding,
975                })]
976            }),
977        };
978
979        let client_input_bundle = self.client_ctx.make_client_inputs(ClientInputBundle::new(
980            vec![client_input],
981            vec![client_input_sm],
982        ));
983
984        let address = self.derive_address(address_index).as_unchecked().clone();
985
986        let meta_address = address.clone();
987        let range = self
988            .submit_with_event(
989                operation_id,
990                move |change_outpoint_range| {
991                    WalletOperationMeta::Receive(ReceiveMeta {
992                        change_outpoint_range,
993                        value,
994                        fee,
995                        address: Some(meta_address.clone()),
996                        outpoint,
997                    })
998                },
999                TransactionBuilder::new().with_inputs(client_input_bundle),
1000                ReceivePaymentEvent {
1001                    operation_id,
1002                    value,
1003                    fee,
1004                    address,
1005                    outpoint,
1006                },
1007            )
1008            .await
1009            .ok()?;
1010
1011        Some((operation_id, range.txid()))
1012    }
1013
1014    /// Submits a transaction as a new operation and logs the event that
1015    /// announces the operation, in one database transaction.
1016    ///
1017    /// Committing the two together is what lets a reader of the event log rely
1018    /// on it: an operation that exists has its event, also after the client
1019    /// stopped at any point in between.
1020    async fn submit_with_event<F, E>(
1021        &self,
1022        operation_id: OperationId,
1023        operation_meta_gen: F,
1024        tx_builder: TransactionBuilder,
1025        event: E,
1026    ) -> Result<OutPointRange, TransactionSubmitError>
1027    where
1028        F: Fn(OutPointRange) -> WalletOperationMeta + Clone + MaybeSend + MaybeSync + 'static,
1029        E: Event + Clone + Send + MaybeSync,
1030    {
1031        self.client_ctx
1032            .module_db()
1033            .autocommit(
1034                |dbtx, _| {
1035                    let operation_meta_gen = operation_meta_gen.clone();
1036                    let tx_builder = tx_builder.clone();
1037                    let event = event.clone();
1038                    Box::pin(async move {
1039                        let range = self
1040                            .client_ctx
1041                            .finalize_and_submit_transaction_dbtx(
1042                                dbtx,
1043                                operation_id,
1044                                WalletCommonInit::KIND.as_str(),
1045                                operation_meta_gen,
1046                                tx_builder,
1047                            )
1048                            .await?;
1049
1050                        self.client_ctx.log_event(dbtx, event).await;
1051
1052                        Ok::<_, TransactionSubmitError>(range)
1053                    })
1054                },
1055                Some(100),
1056            )
1057            .await
1058            .map_err(|error| match error {
1059                AutocommitError::ClosureError { error, .. } => error,
1060                AutocommitError::CommitFailed { last_error, .. } => {
1061                    TransactionSubmitError::Database(last_error)
1062                }
1063            })
1064    }
1065
1066    fn spawn_output_scanner(&self, task_group: &TaskGroup, client_span: &tracing::Span) {
1067        let module = self.clone();
1068        let handle = task_group.make_handle();
1069
1070        task_group.spawn_cancellable_with_span(client_span.clone(), "output-scanner", async move {
1071            // Read the stored index with a short-lived transaction, runs the search
1072            // with no transaction open, and inserts the result in its own short
1073            // transaction. This avoids holding a transaction snapshot across a
1074            // long-running CPU-bound search that might outlive RocksDB's retained
1075            // write history, causing a SnapshotTooOld error and panic on commit.
1076            // See: https://github.com/fedimint/fedimint/issues/9202
1077            let address_index_exists = {
1078                let mut dbtx = module.db.begin_transaction().await;
1079                dbtx.find_by_prefix(&ValidAddressIndexPrefix)
1080                    .await
1081                    .next()
1082                    .await
1083                    .is_some()
1084            };
1085
1086            if !address_index_exists {
1087                let Some(index) = module.next_valid_index(0, &handle).await else {
1088                    return;
1089                };
1090
1091                let mut dbtx = module.db.begin_transaction().await;
1092                dbtx.insert_new_entry(&ValidAddressIndexKey(index), &())
1093                    .await;
1094                dbtx.commit_tx().await;
1095            }
1096
1097            loop {
1098                match module.check_outputs(&handle).await {
1099                    Ok(skip_wait) => {
1100                        if skip_wait {
1101                            continue;
1102                        }
1103                    }
1104                    Err(e) => {
1105                        warn!(target: LOG_CLIENT_MODULE_WALLETV2, "Failed to fetch outputs: {e}");
1106                    }
1107                }
1108
1109                sleep(fedimint_walletv2_common::sleep_duration()).await;
1110            }
1111        });
1112    }
1113
1114    async fn check_outputs(&self, handle: &TaskHandle) -> Result<bool, CheckOutputsError> {
1115        let mut dbtx = self.db.begin_transaction_nc().await;
1116
1117        let next_output_index = dbtx.get_value(&NextOutputIndexKey).await.unwrap_or(0);
1118
1119        let mut valid_indices: Vec<u64> = dbtx
1120            .find_by_prefix(&ValidAddressIndexPrefix)
1121            .await
1122            .map(|entry| entry.0.0)
1123            .collect()
1124            .await;
1125
1126        let mut address_map: BTreeMap<ScriptBuf, u64> = valid_indices
1127            .iter()
1128            .map(|&i| (self.derive_address(i).script_pubkey(), i))
1129            .collect();
1130
1131        let outputs = self
1132            .module_api
1133            .output_info_slice(next_output_index, next_output_index + SLICE_SIZE)
1134            .await?;
1135
1136        let returned_num = outputs.len();
1137        let mut matched_num: usize = 0;
1138
1139        for output in &outputs {
1140            if let Some(&address_index) = address_map.get(&output.script) {
1141                matched_num += 1;
1142
1143                // Claim before extending the valid index list: the index search
1144                // below is CPU-bound and can take longer than a short-lived
1145                // client process (e.g. a cli invocation) lives. The claim is
1146                // quick and the extension can be retried on the next scan.
1147                if !output.spent && !self.process_unspent_output(output, address_index).await? {
1148                    return Ok(false);
1149                }
1150
1151                let next_address_index = valid_indices
1152                    .last()
1153                    .copied()
1154                    .expect("we have at least one address index");
1155
1156                // If we used the highest valid index, add the next valid one
1157                if address_index == next_address_index {
1158                    let Some(index) = self.next_valid_index(next_address_index + 1, handle).await
1159                    else {
1160                        return Ok(false);
1161                    };
1162
1163                    let mut dbtx = self.db.begin_transaction().await;
1164
1165                    dbtx.insert_entry(&ValidAddressIndexKey(index), &()).await;
1166
1167                    dbtx.commit_tx_result().await?;
1168
1169                    valid_indices.push(index);
1170
1171                    address_map.insert(self.derive_address(index).script_pubkey(), index);
1172                }
1173            }
1174
1175            let mut dbtx = self.db.begin_transaction().await;
1176
1177            dbtx.insert_entry(&NextOutputIndexKey, &(output.index + 1))
1178                .await;
1179
1180            dbtx.commit_tx_result().await?;
1181        }
1182
1183        debug!(
1184            target: LOG_CLIENT_MODULE_WALLETV2,
1185            next_output_index,
1186            returned_num,
1187            matched_num,
1188            valid_indices_num = valid_indices.len(),
1189            "Scanning for outputs"
1190        );
1191
1192        Ok(!outputs.is_empty())
1193    }
1194
1195    async fn process_unspent_output(
1196        &self,
1197        output: &OutputInfo,
1198        address_index: u64,
1199    ) -> Result<bool, ProcessOutputError> {
1200        debug!(
1201            target: LOG_CLIENT_MODULE_WALLETV2,
1202            output_index = output.index,
1203            value_sat = output.value.to_sat(),
1204            address_index,
1205            outpoint = ?output.outpoint,
1206            "Discovered unspent walletv2 receive output"
1207        );
1208
1209        // In order to not overpay on fees we choose to wait,
1210        // the congestion will clear up within a few blocks.
1211        let pending_tx_chain_len = self.module_api.pending_tx_chain().await?.len();
1212        if 3 <= pending_tx_chain_len {
1213            debug!(
1214                target: LOG_CLIENT_MODULE_WALLETV2,
1215                output_index = output.index,
1216                pending_tx_chain_len,
1217                "Delaying walletv2 receive claim because pending transaction chain is full"
1218            );
1219            return Ok(false);
1220        }
1221
1222        let receive_fee = self
1223            .module_api
1224            .receive_fee()
1225            .await?
1226            .ok_or(ProcessOutputError::NoFeerate)?;
1227
1228        if let Some((operation_id, txid)) = self
1229            .receive_output(
1230                output.index,
1231                output.value,
1232                address_index,
1233                receive_fee,
1234                output.outpoint,
1235            )
1236            .await
1237        {
1238            debug!(
1239                target: LOG_CLIENT_MODULE_WALLETV2,
1240                output_index = output.index,
1241                ?operation_id,
1242                %txid,
1243                "Waiting for walletv2 receive claim acceptance"
1244            );
1245            self.client_ctx
1246                .transaction_updates(operation_id)
1247                .await
1248                .await_tx_accepted(txid)
1249                .await
1250                .map_err(ProcessOutputError::ClaimRejected)?;
1251            debug!(
1252                target: LOG_CLIENT_MODULE_WALLETV2,
1253                output_index = output.index,
1254                ?operation_id,
1255                %txid,
1256                "Walletv2 receive claim accepted"
1257            );
1258        } else {
1259            debug!(
1260                target: LOG_CLIENT_MODULE_WALLETV2,
1261                output_index = output.index,
1262                value_sat = output.value.to_sat(),
1263                fee_sat = receive_fee.to_sat(),
1264                "Skipping walletv2 receive claim; value cannot cover the claim fees"
1265            );
1266        }
1267
1268        Ok(true)
1269    }
1270}
1271
1272/// A failure to send an on-chain payment.
1273#[derive(Error, Debug)]
1274#[non_exhaustive]
1275pub enum SendError {
1276    /// The destination address is not valid on the federation's network.
1277    #[error("Address is from a different network than the federation")]
1278    WrongNetwork,
1279
1280    /// The value to send is below the federation's dust limit.
1281    #[error("The value is too small")]
1282    DustValue,
1283
1284    /// The federation could not be asked for the current on-chain fee.
1285    #[error("The federation returned an error")]
1286    Federation(#[source] Box<FederationError>),
1287
1288    /// The guardians have not agreed a feerate yet, so no on-chain fee can be
1289    /// quoted.
1290    #[error("No consensus feerate is available at this time")]
1291    NoConsensusFeerateAvailable,
1292
1293    /// The client cannot fund the wallet output this send needs.
1294    #[error("The client does not have sufficient funds to send the payment")]
1295    InsufficientFunds,
1296
1297    /// The destination is not an address type the federation can pay.
1298    #[error("Unsupported address type")]
1299    UnsupportedAddress,
1300
1301    /// The send transaction could not be submitted for a reason that is not
1302    /// about funding.
1303    #[error("The send transaction could not be submitted")]
1304    Failed(#[source] TransactionSubmitError),
1305}
1306
1307/// A failure to quote the fee for claiming an on-chain deposit.
1308#[derive(Error, Debug)]
1309#[non_exhaustive]
1310pub enum ReceiveError {
1311    /// The federation could not be asked for the current claim fee.
1312    #[error("The federation returned an error")]
1313    Federation(#[source] Box<FederationError>),
1314
1315    /// The guardians have not agreed a feerate yet, so no claim fee can be
1316    /// quoted.
1317    #[error("No consensus feerate is available at this time")]
1318    NoConsensusFeerateAvailable,
1319}
1320
1321/// A failure to wait for the next on-chain payment to be received.
1322#[derive(Debug, Error)]
1323#[non_exhaustive]
1324pub enum AwaitReceiveError {
1325    /// The receive operation could not be looked up.
1326    #[error("The receive operation could not be looked up")]
1327    Operation(#[from] OperationLookupError),
1328
1329    /// The deposit was claimed, but the ecash it mints was never issued.
1330    #[error("The ecash for the claimed deposit could not be issued")]
1331    Issuance(#[from] TransactionSubmitError),
1332}
1333
1334/// A failure to look up the progress of an incoming peg-in.
1335#[derive(Debug, Error)]
1336#[non_exhaustive]
1337pub enum ReceiveProgressError {
1338    /// No guardian reports a deposit at this outpoint to one of our addresses.
1339    /// This includes a deposit that was reorged out or evicted since it was
1340    /// last seen.
1341    #[error("No deposit of ours at this outpoint")]
1342    UnknownDeposit,
1343
1344    /// The address was not handed out by [`WalletClientModule::receive`].
1345    #[error("Address was not derived by this client")]
1346    UnknownAddress,
1347}
1348
1349/// A failure while scanning the federation's outputs for payments to this
1350/// client.
1351#[derive(Debug, Error)]
1352enum CheckOutputsError {
1353    /// The federation did not serve the scan.
1354    #[error(transparent)]
1355    Federation(#[from] FederationError),
1356
1357    /// The scan's progress could not be committed.
1358    #[error(transparent)]
1359    Database(#[from] DatabaseError),
1360
1361    /// An unspent output paid to this client could not be claimed.
1362    #[error(transparent)]
1363    ProcessOutput(#[from] ProcessOutputError),
1364}
1365
1366/// A failure to claim an unspent output paid to this client.
1367#[derive(Debug, Error)]
1368enum ProcessOutputError {
1369    /// The federation could not report its pending transactions or the
1370    /// receive fee.
1371    #[error(transparent)]
1372    Federation(#[from] FederationError),
1373
1374    /// The federation has no consensus feerate to price the claim with.
1375    #[error("No consensus feerate is available")]
1376    NoFeerate,
1377
1378    /// The claim transaction was rejected.
1379    #[error("Claim transaction was rejected: {0}")]
1380    ClaimRejected(String),
1381}
1382
1383#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1384pub enum WalletClientStateMachines {
1385    Send(send_sm::SendStateMachine),
1386    Receive(receive_sm::ReceiveStateMachine),
1387}
1388
1389impl State for WalletClientStateMachines {
1390    type ModuleContext = WalletClientContext;
1391
1392    fn transitions(
1393        &self,
1394        context: &Self::ModuleContext,
1395        global_context: &DynGlobalClientContext,
1396    ) -> Vec<StateTransition<Self>> {
1397        match self {
1398            WalletClientStateMachines::Send(sm) => sm_enum_variant_translation!(
1399                sm.transitions(context, global_context),
1400                WalletClientStateMachines::Send
1401            ),
1402            WalletClientStateMachines::Receive(sm) => sm_enum_variant_translation!(
1403                sm.transitions(context, global_context),
1404                WalletClientStateMachines::Receive
1405            ),
1406        }
1407    }
1408
1409    fn operation_id(&self) -> OperationId {
1410        match self {
1411            WalletClientStateMachines::Send(sm) => sm.operation_id(),
1412            WalletClientStateMachines::Receive(sm) => sm.operation_id(),
1413        }
1414    }
1415}
1416
1417impl IntoDynInstance for WalletClientStateMachines {
1418    type DynType = DynState;
1419
1420    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1421        DynState::from_typed(instance_id, self)
1422    }
1423}