Skip to main content

fedimint_mintv2_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#![allow(clippy::return_self_not_must_use)]
8#![allow(clippy::too_many_lines)]
9
10pub use fedimint_mintv2_common as common;
11
12mod api;
13#[cfg(feature = "cli")]
14mod cli;
15pub mod client_db;
16mod ecash;
17mod events;
18mod input;
19pub mod issuance;
20mod output;
21mod receive;
22
23use std::collections::{BTreeMap, BTreeSet};
24use std::convert::Infallible;
25use std::sync::Arc;
26use std::time::Duration;
27
28use anyhow::{Context as _, anyhow};
29use bitcoin_hashes::sha256;
30use client_db::{RecoveryState, RecoveryStateKey, SpendableNoteAmountPrefix, SpendableNotePrefix};
31pub use events::*;
32use fedimint_api_client::api::DynModuleApi;
33use fedimint_client::module::ClientModule;
34use fedimint_client::transaction::{
35    ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
36    ClientOutputSM, FeeQuote, FeeQuoteRequest, TransactionBuilder,
37};
38use fedimint_client_module::db::ClientModuleMigrationFn;
39use fedimint_client_module::error::{
40    InsufficientBalanceError, OperationLookupError, TransactionSubmitError,
41};
42use fedimint_client_module::module::init::{
43    ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs,
44    ClientModuleRecoveryPrepareArgs, RecoveryMode,
45};
46use fedimint_client_module::module::recovery::{NoModuleBackup, RecoveryProgress};
47use fedimint_client_module::module::{
48    ClientContext, OutPointRange, PrimaryModulePriority, PrimaryModuleSupport,
49};
50use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
51use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
52use fedimint_core::base32::{self, FEDIMINT_PREFIX};
53use fedimint_core::config::FederationId;
54use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
55use fedimint_core::db::{DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped};
56use fedimint_core::encoding::{Decodable, Encodable};
57use fedimint_core::module::{
58    AmountUnit, Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
59};
60use fedimint_core::secp256k1::rand::{Rng, thread_rng};
61use fedimint_core::secp256k1::{Keypair, PublicKey};
62use fedimint_core::util::backoff_util::custom_backoff;
63use fedimint_core::util::{BoxStream, NextOrPending};
64use fedimint_core::{Amount, OutPoint, PeerId, apply, async_trait_maybe_send};
65use fedimint_derive_secret::DerivableSecret;
66use fedimint_mintv2_common::config::{FeeConsensus, MintClientConfig, client_denominations};
67use fedimint_mintv2_common::{
68    Denomination, KIND, MintCommonInit, MintInput, MintModuleTypes, MintOutput, Note, RecoveryItem,
69};
70use futures::{StreamExt, pin_mut};
71use itertools::Itertools;
72use serde::{Deserialize, Serialize};
73use serde_json::Value;
74use tbs::AggregatePublicKey;
75use thiserror::Error;
76
77use crate::api::MintV2ModuleApi;
78use crate::client_db::SpendableNoteKey;
79pub use crate::ecash::ECash;
80use crate::input::{InputSMCommon, InputSMState, InputStateMachine};
81use crate::issuance::NoteIssuanceRequest;
82use crate::output::{MintOutputStateMachine, OutputSMCommon, OutputSMState};
83use crate::receive::{ReceiveSMState, ReceiveStateMachine};
84
85const TARGET_PER_DENOMINATION: usize = 3;
86const SLICE_SIZE: u64 = 10000;
87/// How long a guardian sits out after failing to answer a slice request.
88const PEER_READMISSION: Duration = Duration::from_secs(60);
89/// How long a single peer is given to answer a slice request.
90///
91/// A slice takes a second or two from a healthy guardian, so waiting half a
92/// minute only delays noticing that one is not going to answer. The timeout
93/// grows on every failed attempt up to [`MAX_SLICE_TIMEOUT`], so a client
94/// on a slow connection where every guardian exceeds the initial timeout
95/// still makes progress instead of retrying forever.
96const SLICE_TIMEOUT: Duration = Duration::from_secs(10);
97/// Upper bound for the per-retry growth of [`SLICE_TIMEOUT`]; the flat
98/// timeout used before the growth was introduced.
99const MAX_SLICE_TIMEOUT: Duration = Duration::from_secs(30);
100const PARALLEL_HASH_REQUESTS: usize = 10;
101const PARALLEL_SLICE_REQUESTS: usize = 10;
102
103#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable)]
104pub struct SpendableNote {
105    pub denomination: Denomination,
106    pub keypair: Keypair,
107    pub signature: tbs::Signature,
108}
109
110impl SpendableNote {
111    pub fn amount(&self) -> Amount {
112        self.denomination.amount()
113    }
114}
115
116impl SpendableNote {
117    fn nonce(&self) -> PublicKey {
118        self.keypair.public_key()
119    }
120
121    fn note(&self) -> Note {
122        Note {
123            denomination: self.denomination,
124            nonce: self.nonce(),
125            signature: self.signature,
126        }
127    }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub enum MintOperationMeta {
132    Send {
133        ecash: String,
134        custom_meta: Value,
135    },
136    Reissue {
137        change_outpoint_range: OutPointRange,
138        amount: Amount,
139        custom_meta: Value,
140    },
141    Receive {
142        change_outpoint_range: OutPointRange,
143        ecash: String,
144        custom_meta: Value,
145    },
146}
147
148#[derive(Debug, Clone)]
149pub struct MintClientInit;
150
151impl ModuleInit for MintClientInit {
152    type Common = MintCommonInit;
153
154    async fn dump_database(
155        &self,
156        _dbtx: &mut DatabaseTransaction<'_>,
157        _prefix_names: Vec<String>,
158    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
159        Box::new(BTreeMap::new().into_iter())
160    }
161}
162
163#[apply(async_trait_maybe_send!)]
164impl ClientModuleInit for MintClientInit {
165    type Module = MintClientModule;
166
167    fn supported_api_versions(&self) -> MultiApiVersion {
168        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 1 }])
169            .expect("no version conflicts")
170    }
171
172    fn recovery_mode(&self) -> RecoveryMode {
173        RecoveryMode::Usable
174    }
175
176    async fn prepare_recovery(&self, args: &ClientModuleRecoveryPrepareArgs) -> anyhow::Result<()> {
177        if args
178            .db()
179            .begin_transaction_nc()
180            .await
181            .get_value(&RecoveryStateKey)
182            .await
183            .is_some()
184        {
185            return Ok(());
186        }
187
188        // The total is the number of items the federation had processed when
189        // the recovery began, so every note this client issues from here on
190        // lands beyond it and is never rediscovered by the scan. Committing
191        // that bound before the module is initialized is what lets the module
192        // be used while its own recovery is still running: the two cannot
193        // arrive at the same note.
194        let state = RecoveryState {
195            next_index: 0,
196            total_items: args.module_api().fetch_recovery_count().await?,
197            requests: BTreeMap::new(),
198            nonces: BTreeSet::new(),
199        };
200
201        let mut dbtx = args.db().begin_transaction().await;
202
203        dbtx.insert_entry(&RecoveryStateKey, &state).await;
204
205        dbtx.commit_tx().await;
206
207        Ok(())
208    }
209
210    async fn recover(
211        &self,
212        args: &ClientModuleRecoverArgs<Self>,
213        _snapshot: Option<&NoModuleBackup>,
214    ) -> anyhow::Result<Option<Amount>> {
215        let mut state = args
216            .db()
217            .begin_transaction_nc()
218            .await
219            .get_value(&RecoveryStateKey)
220            .await
221            .expect("Prepare recovery commits the state before the recovery is started");
222
223        if state.next_index == state.total_items {
224            return Ok(None);
225        }
226
227        let peer_pool = PeerPool::new(args.api().all_peers());
228
229        let mut recovery_stream = futures::stream::iter(
230            (state.next_index..state.total_items).step_by(SLICE_SIZE as usize),
231        )
232        .map(|start| {
233            let api = args.module_api().clone();
234            let end = std::cmp::min(start + SLICE_SIZE, state.total_items);
235
236            async move { (start, end, api.fetch_recovery_slice_hash(start, end).await) }
237        })
238        .buffered(PARALLEL_HASH_REQUESTS)
239        .map(|(start, end, hash)| {
240            let module_api = args.module_api().clone();
241            let peer_pool = peer_pool.clone();
242
243            async move {
244                (
245                    start,
246                    download_slice(module_api, peer_pool, start, end, hash).await,
247                )
248            }
249        })
250        // Unordered, so a guardian that goes quiet holds up only its own
251        // slice. Delivering in order would let it halt everything: the other
252        // requests would finish, their results would fill the buffer, and no
253        // new request could start until the straggler returned. Downloading
254        // runs ahead of it instead, and the items are put back in order below.
255        .buffer_unordered(PARALLEL_SLICE_REQUESTS);
256
257        let tweak_filter = issuance::tweak_filter(args.module_root_secret());
258
259        // Slices that arrived before the ones in front of them. An input
260        // spends an output that has to have been seen already, so items are
261        // scanned in index order however they turn up. This holds whatever
262        // downloaded while a slice was outstanding, which the timeout bounds.
263        let mut pending: BTreeMap<u64, Vec<RecoveryItem>> = BTreeMap::new();
264
265        loop {
266            let items = loop {
267                if let Some(items) = pending.remove(&state.next_index) {
268                    break items;
269                }
270
271                let (start, items) = recovery_stream
272                    .next()
273                    .await
274                    .context("Recovery stream finished before recovery is complete")?;
275
276                pending.insert(start, items);
277            };
278
279            for item in &items {
280                match item {
281                    RecoveryItem::Output {
282                        denomination,
283                        nonce_hash,
284                        tweak,
285                    } => {
286                        if !issuance::check_tweak(*tweak, tweak_filter) {
287                            continue;
288                        }
289                        let output_secret = issuance::output_secret(
290                            *denomination,
291                            *tweak,
292                            args.module_root_secret(),
293                        );
294
295                        if !issuance::check_nonce(&output_secret, *nonce_hash) {
296                            continue;
297                        }
298
299                        let computed_nonce_hash = issuance::nonce(&output_secret).consensus_hash();
300
301                        // Ignore possible duplicate nonces
302                        if !state.nonces.insert(computed_nonce_hash) {
303                            continue;
304                        }
305
306                        state.requests.insert(
307                            computed_nonce_hash,
308                            NoteIssuanceRequest::new(
309                                *denomination,
310                                *tweak,
311                                args.module_root_secret(),
312                            ),
313                        );
314                    }
315                    RecoveryItem::Input { nonce_hash } => {
316                        state.requests.remove(nonce_hash);
317                        state.nonces.remove(nonce_hash);
318                    }
319                }
320            }
321
322            state.next_index += items.len() as u64;
323
324            let mut dbtx = args.db().begin_transaction().await;
325
326            dbtx.insert_entry(&RecoveryStateKey, &state).await;
327
328            if state.next_index == state.total_items {
329                // Total value of the notes reconstructed during recovery
330                let recovered_amount = state
331                    .requests
332                    .values()
333                    .map(|request| request.denomination.amount())
334                    .sum::<Amount>();
335
336                let state_machines = args
337                    .context()
338                    .map_dyn(vec![MintClientStateMachines::Output(
339                        MintOutputStateMachine {
340                            common: OutputSMCommon {
341                                operation_id: OperationId::new_random(),
342                                range: None,
343                                issuance_requests: state.requests.into_values().collect(),
344                            },
345                            state: OutputSMState::Pending,
346                        },
347                    )])
348                    .collect();
349
350                args.context()
351                    .add_state_machines_dbtx(&mut dbtx.to_ref_nc(), state_machines)
352                    .await
353                    .expect("state machine is valid");
354
355                dbtx.commit_tx().await;
356
357                return Ok(Some(recovered_amount));
358            }
359
360            dbtx.commit_tx().await;
361
362            args.update_recovery_progress(RecoveryProgress {
363                complete: state.next_index.try_into().unwrap_or(u32::MAX),
364                total: state.total_items.try_into().unwrap_or(u32::MAX),
365            });
366        }
367    }
368
369    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
370        let (tweak_sender, tweak_receiver) = async_channel::bounded(50);
371
372        let filter = issuance::tweak_filter(args.module_root_secret());
373
374        // Only ~1/65536 random tweaks pass the filter, so grinding is
375        // CPU-bound. Pre-grind in the background and feed tweaks through a
376        // channel to keep the cost off the spend path. `send` only suspends
377        // once the channel is full, so we yield explicitly on each found tweak
378        // to keep single-threaded (wasm) runtimes responsive while the channel
379        // still has space.
380        fedimint_core::task::spawn("mintv2-tweak-grinder", async move {
381            loop {
382                let tweak: [u8; 16] = thread_rng().r#gen();
383
384                if !issuance::check_tweak(tweak, filter) {
385                    continue;
386                }
387
388                if tweak_sender.send(tweak).await.is_err() {
389                    return;
390                }
391
392                fedimint_core::task::sleep(Duration::ZERO).await;
393            }
394        });
395
396        Ok(MintClientModule {
397            federation_id: *args.federation_id(),
398            cfg: args.cfg().clone(),
399            root_secret: args.module_root_secret().clone(),
400            notifier: args.notifier().clone(),
401            client_ctx: args.context(),
402            balance_update_sender: tokio::sync::watch::channel(()).0,
403            tweak_receiver,
404        })
405    }
406
407    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
408        BTreeMap::new()
409    }
410}
411
412#[derive(Debug)]
413pub struct MintClientModule {
414    federation_id: FederationId,
415    cfg: MintClientConfig,
416    root_secret: DerivableSecret,
417    notifier: ModuleNotifier<MintClientStateMachines>,
418    client_ctx: ClientContext<Self>,
419    balance_update_sender: tokio::sync::watch::Sender<()>,
420    tweak_receiver: async_channel::Receiver<[u8; 16]>,
421}
422
423#[derive(Debug, Clone)]
424pub struct MintClientContext {
425    client_ctx: ClientContext<MintClientModule>,
426    tbs_agg_pks: BTreeMap<Denomination, AggregatePublicKey>,
427    tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, tbs::PublicKeyShare>>,
428    pub balance_update_sender: tokio::sync::watch::Sender<()>,
429}
430
431impl Context for MintClientContext {
432    const KIND: Option<ModuleKind> = Some(KIND);
433}
434
435#[apply(async_trait_maybe_send!)]
436impl ClientModule for MintClientModule {
437    type Init = MintClientInit;
438    type Common = MintModuleTypes;
439    type Backup = NoModuleBackup;
440    type ModuleStateMachineContext = MintClientContext;
441    type States = MintClientStateMachines;
442
443    fn context(&self) -> Self::ModuleStateMachineContext {
444        MintClientContext {
445            client_ctx: self.client_ctx.clone(),
446            tbs_agg_pks: self.cfg.tbs_agg_pks.clone(),
447            tbs_pks: self.cfg.tbs_pks.clone(),
448            balance_update_sender: self.balance_update_sender.clone(),
449        }
450    }
451
452    fn input_fee(
453        &self,
454        amounts: &Amounts,
455        _input: &<Self::Common as ModuleCommon>::Input,
456    ) -> Option<Amounts> {
457        let unit = self.cfg.amount_unit;
458        let amount = amounts.get(&unit).copied().unwrap_or_default();
459        let fee = self.cfg.fee_consensus.fee(amount);
460
461        Some(Amounts::new_custom(unit, fee))
462    }
463
464    fn output_fee(
465        &self,
466        amounts: &Amounts,
467        _output: &<Self::Common as ModuleCommon>::Output,
468    ) -> Option<Amounts> {
469        let unit = self.cfg.amount_unit;
470        let amount = amounts.get(&unit).copied().unwrap_or_default();
471        let fee = self.cfg.fee_consensus.fee(amount);
472
473        Some(Amounts::new_custom(unit, fee))
474    }
475
476    #[cfg(feature = "cli")]
477    async fn handle_cli_command(
478        &self,
479        args: &[std::ffi::OsString],
480    ) -> anyhow::Result<serde_json::Value> {
481        cli::handle_cli_command(self, args).await
482    }
483
484    fn supports_being_primary(&self) -> PrimaryModuleSupport {
485        PrimaryModuleSupport::selected(PrimaryModulePriority::HIGH, [self.cfg.amount_unit])
486    }
487
488    async fn create_final_inputs_and_outputs(
489        &self,
490        dbtx: &mut DatabaseTransaction<'_>,
491        operation_id: OperationId,
492        unit: AmountUnit,
493        mut input_amount: Amount,
494        mut output_amount: Amount,
495    ) -> anyhow::Result<(
496        ClientInputBundle<MintInput, MintClientStateMachines>,
497        ClientOutputBundle<MintOutput, MintClientStateMachines>,
498    )> {
499        if unit != self.cfg.amount_unit {
500            anyhow::bail!("Module can only handle its configured amount unit");
501        }
502
503        let requested_amount = output_amount.saturating_sub(input_amount);
504        // `select_funding_input` only reads notes, so the balance it left behind is
505        // still accurate for reporting a total below.
506        let Some(funding_notes) = self.select_funding_input(dbtx, requested_amount).await else {
507            let total_amount = self.get_balance(dbtx, unit).await;
508            return Err(InsufficientBalanceError {
509                requested_amount,
510                total_amount,
511            }
512            .into());
513        };
514
515        for note in &funding_notes {
516            self.remove_spendable_note(dbtx, note).await;
517        }
518
519        input_amount += funding_notes.iter().map(SpendableNote::amount).sum();
520
521        output_amount += funding_notes
522            .iter()
523            .map(|input| self.cfg.fee_consensus.fee(input.amount()))
524            .sum();
525
526        assert!(output_amount <= input_amount);
527
528        let (input_notes, output_amounts) = self
529            .rebalance(dbtx, &self.cfg.fee_consensus, input_amount - output_amount)
530            .await;
531
532        for note in &input_notes {
533            self.remove_spendable_note(dbtx, note).await;
534        }
535
536        input_amount += input_notes.iter().map(SpendableNote::amount).sum();
537
538        output_amount += input_notes
539            .iter()
540            .map(|note| self.cfg.fee_consensus.fee(note.amount()))
541            .sum();
542
543        output_amount += output_amounts
544            .iter()
545            .map(|denomination| {
546                denomination.amount() + self.cfg.fee_consensus.fee(denomination.amount())
547            })
548            .sum();
549
550        assert!(output_amount <= input_amount);
551
552        let mut spendable_notes = funding_notes
553            .into_iter()
554            .chain(input_notes)
555            .collect::<Vec<SpendableNote>>();
556
557        // We sort the notes by denomination to minimize the leaked information.
558        spendable_notes.sort_by_key(|note| note.denomination);
559
560        let input_bundle =
561            Self::create_input_bundle(operation_id, spendable_notes, false, self.cfg.amount_unit);
562
563        let mut denominations = represent_amount_with_fees(
564            input_amount.saturating_sub(output_amount),
565            &self.cfg.fee_consensus,
566        )
567        .into_iter()
568        .chain(output_amounts)
569        .collect::<Vec<Denomination>>();
570
571        // We sort the amounts to minimize the leaked information.
572        denominations.sort();
573
574        let output_bundle = self.create_output_bundle(operation_id, denominations).await;
575
576        let sender = self.balance_update_sender.clone();
577        dbtx.on_commit(move || sender.send_replace(()));
578
579        Ok((input_bundle, output_bundle))
580    }
581
582    async fn await_primary_module_output(
583        &self,
584        operation_id: OperationId,
585        outpoint: OutPoint,
586    ) -> anyhow::Result<()> {
587        self.await_output_sm_success(operation_id, outpoint).await
588    }
589
590    async fn get_balance(&self, dbtx: &mut DatabaseTransaction<'_>, unit: AmountUnit) -> Amount {
591        if unit != self.cfg.amount_unit {
592            return Amount::ZERO;
593        }
594
595        self.get_count_by_denomination_dbtx(dbtx)
596            .await
597            .into_iter()
598            .map(|(denomination, count)| denomination.amount().mul_u64(count))
599            .sum()
600    }
601
602    async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
603        Box::pin(tokio_stream::wrappers::WatchStream::new(
604            self.balance_update_sender.subscribe(),
605        ))
606    }
607}
608
609impl MintClientModule {
610    async fn select_funding_input(
611        &self,
612        dbtx: &mut DatabaseTransaction<'_>,
613        mut excess_output: Amount,
614    ) -> Option<Vec<SpendableNote>> {
615        let mut selected_notes = Vec::new();
616        let mut target_notes = Vec::new();
617        let mut excess_notes = Vec::new();
618
619        for amount in client_denominations().rev() {
620            let notes_amount = dbtx
621                .find_by_prefix(&SpendableNoteAmountPrefix(amount))
622                .await
623                .map(|entry| entry.0.0)
624                .collect::<Vec<SpendableNote>>()
625                .await;
626
627            target_notes.extend(notes_amount.iter().take(TARGET_PER_DENOMINATION).cloned());
628
629            if notes_amount.len() > 2 * TARGET_PER_DENOMINATION {
630                for note in notes_amount.into_iter().skip(TARGET_PER_DENOMINATION) {
631                    let note_fee = self.cfg.fee_consensus.fee(note.amount());
632
633                    let note_value = note
634                        .amount()
635                        .checked_sub(note_fee)
636                        .expect("All our notes are economical");
637
638                    excess_output = excess_output.saturating_sub(note_value);
639
640                    selected_notes.push(note);
641                }
642            } else {
643                excess_notes.extend(notes_amount.into_iter().skip(TARGET_PER_DENOMINATION));
644            }
645        }
646
647        if excess_output == Amount::ZERO {
648            return Some(selected_notes);
649        }
650
651        for note in excess_notes.into_iter().chain(target_notes) {
652            let note_amount = note.amount();
653            let note_value = note_amount
654                .checked_sub(self.cfg.fee_consensus.fee(note_amount))
655                .expect("All our notes are economical");
656
657            excess_output = excess_output.saturating_sub(note_value);
658
659            selected_notes.push(note);
660
661            if excess_output == Amount::ZERO {
662                return Some(selected_notes);
663            }
664        }
665
666        None
667    }
668
669    async fn rebalance(
670        &self,
671        dbtx: &mut DatabaseTransaction<'_>,
672        fee: &FeeConsensus,
673        mut excess_input: Amount,
674    ) -> (Vec<SpendableNote>, Vec<Denomination>) {
675        let n_denominations = self.get_count_by_denomination_dbtx(dbtx).await;
676
677        let mut notes = dbtx
678            .find_by_prefix_sorted_descending(&SpendableNotePrefix)
679            .await
680            .map(|entry| entry.0.0)
681            .fuse();
682
683        let mut input_notes = Vec::new();
684        let mut output_denominations = Vec::new();
685
686        for d in client_denominations() {
687            let n_denomination = n_denominations.get(&d).copied().unwrap_or(0);
688
689            let n_missing = TARGET_PER_DENOMINATION.saturating_sub(n_denomination as usize);
690
691            for _ in 0..n_missing {
692                match excess_input.checked_sub(d.amount() + fee.fee(d.amount())) {
693                    Some(remaining_excess) => excess_input = remaining_excess,
694                    None => match notes.next().await {
695                        Some(note) => {
696                            if note.amount() <= d.amount() + fee.fee(d.amount()) {
697                                break;
698                            }
699
700                            excess_input += note.amount() - (d.amount() + fee.fee(d.amount()));
701
702                            input_notes.push(note);
703                        }
704                        None => break,
705                    },
706                }
707
708                output_denominations.push(d);
709            }
710        }
711
712        (input_notes, output_denominations)
713    }
714
715    fn create_input_bundle(
716        operation_id: OperationId,
717        notes: Vec<SpendableNote>,
718        include_receive_sm: bool,
719        amount_unit: AmountUnit,
720    ) -> ClientInputBundle<MintInput, MintClientStateMachines> {
721        let inputs = notes
722            .iter()
723            .map(|spendable_note| ClientInput {
724                input: MintInput::new_v0(spendable_note.note()),
725                keys: vec![spendable_note.keypair],
726                amounts: Amounts::new_custom(amount_unit, spendable_note.amount()),
727            })
728            .collect();
729
730        let input_sms = vec![ClientInputSM {
731            state_machines: Arc::new(move |range: OutPointRange| {
732                let mut sms = vec![MintClientStateMachines::Input(InputStateMachine {
733                    common: InputSMCommon {
734                        operation_id,
735                        txid: range.txid(),
736                        spendable_notes: notes.clone(),
737                    },
738                    state: InputSMState::Pending,
739                })];
740
741                if include_receive_sm {
742                    sms.push(MintClientStateMachines::Receive(ReceiveStateMachine {
743                        common: crate::receive::ReceiveSMCommon {
744                            operation_id,
745                            txid: range.txid(),
746                        },
747                        state: crate::receive::ReceiveSMState::Pending,
748                    }));
749                }
750
751                sms
752            }),
753        }];
754
755        ClientInputBundle::new(inputs, input_sms)
756    }
757
758    /// Build the blinded outputs and their output state machines for a
759    /// reissue transaction.
760    ///
761    /// # Cancel safety
762    ///
763    /// Cancel safe: this only reads pre-ground blinding tweaks from an
764    /// in-memory channel and constructs values, it performs no database
765    /// writes. Dropping the future merely discards the tweaks it had taken,
766    /// which are random (so nothing is lost) and replenished by the
767    /// background grinder task.
768    async fn create_output_bundle(
769        &self,
770        operation_id: OperationId,
771        requested_denominations: Vec<Denomination>,
772    ) -> ClientOutputBundle<MintOutput, MintClientStateMachines> {
773        let issuance_requests = futures::stream::iter(requested_denominations)
774            .zip(self.tweak_receiver.clone())
775            .map(|(d, tweak)| NoteIssuanceRequest::new(d, tweak, &self.root_secret))
776            .collect::<Vec<NoteIssuanceRequest>>()
777            .await;
778
779        let amount_unit = self.cfg.amount_unit;
780        let outputs = issuance_requests
781            .iter()
782            .map(|request| ClientOutput {
783                output: request.output(),
784                amounts: Amounts::new_custom(amount_unit, request.denomination.amount()),
785            })
786            .collect();
787
788        let output_sms = vec![ClientOutputSM {
789            state_machines: Arc::new(move |range: OutPointRange| {
790                vec![MintClientStateMachines::Output(MintOutputStateMachine {
791                    common: OutputSMCommon {
792                        operation_id,
793                        range: Some(range),
794                        issuance_requests: issuance_requests.clone(),
795                    },
796                    state: OutputSMState::Pending,
797                })]
798            }),
799        }];
800
801        ClientOutputBundle::new(outputs, output_sms)
802    }
803
804    /// Wait for the output state machine of the given outpoint to reach a
805    /// terminal state.
806    ///
807    /// # Cancel safety
808    ///
809    /// Cancel safe: this only subscribes to the operation's state
810    /// notifications and waits, it performs no database writes. Dropping the
811    /// future stops the wait; the underlying state machine keeps running in
812    /// the executor.
813    async fn await_output_sm_success(
814        &self,
815        operation_id: OperationId,
816        outpoint: OutPoint,
817    ) -> anyhow::Result<()> {
818        let stream = self
819            .notifier
820            .subscribe(operation_id)
821            .await
822            .filter_map(|state| async {
823                let MintClientStateMachines::Output(state) = state else {
824                    return None;
825                };
826
827                if !state.common.range?.into_iter().contains(&outpoint) {
828                    return None;
829                }
830
831                match state.state {
832                    OutputSMState::Pending => None,
833                    OutputSMState::Success => Some(Ok(())),
834                    OutputSMState::Aborted => Some(Err(anyhow!("Transaction was rejected"))),
835                    OutputSMState::Failure => Some(Err(anyhow!("Failed to finalize notes",))),
836                }
837            });
838
839        pin_mut!(stream);
840
841        stream.next_or_pending().await
842    }
843
844    /// Count the `ECash` notes in the client's database by denomination.
845    pub async fn get_count_by_denomination(&self) -> BTreeMap<Denomination, u64> {
846        self.get_count_by_denomination_dbtx(
847            &mut self.client_ctx.module_db().begin_transaction_nc().await,
848        )
849        .await
850    }
851
852    async fn get_count_by_denomination_dbtx(
853        &self,
854        dbtx: &mut DatabaseTransaction<'_>,
855    ) -> BTreeMap<Denomination, u64> {
856        dbtx.find_by_prefix(&SpendableNotePrefix)
857            .await
858            .fold(BTreeMap::new(), |mut acc, entry| async move {
859                acc.entry(entry.0.0.denomination)
860                    .and_modify(|count| *count += 1)
861                    .or_insert(1);
862
863                acc
864            })
865            .await
866    }
867
868    /// Send `ECash` for the given amount and return the send operation ID. The
869    /// amount will be rounded up to a multiple of 512 msats which is the
870    /// smallest denomination used throughout the client. If the rounded
871    /// amount cannot be covered with the ecash notes in the client's
872    /// database the client will create a transaction to reissue the
873    /// required denominations. To cancel a successful ecash send simply
874    /// receive it yourself.
875    ///
876    /// If `include_invite` is set, the federation's invite code is embedded in
877    /// the returned ecash so a recipient that has not joined the federation can
878    /// do so directly from the received ecash.
879    ///
880    /// # Cancel safety
881    ///
882    /// This method is cancel safe. Every database mutation it makes happens
883    /// inside a single
884    /// [`autocommit`](fedimint_core::db::Database::autocommit) transaction
885    /// (spending the notes for the fast path, and creating the change-making
886    /// reissue transaction otherwise), so dropping the future either commits
887    /// a unit of work in full or leaves the database untouched: notes are
888    /// never removed without a persisted operation that produces the ecash or
889    /// returns the change. Federation submission is driven by a state machine
890    /// in the executor rather than awaited here, so a cancelled call cannot
891    /// abort an in-flight reissue; if change-making had already been
892    /// submitted it completes on its own and the notes return to the balance.
893    pub async fn send(
894        &self,
895        amount: Amount,
896        custom_meta: Value,
897        include_invite: bool,
898    ) -> Result<(OperationId, ECash), SendECashError> {
899        let amount = round_to_multiple(amount, client_denominations().next().unwrap().amount());
900
901        if let Some((operation_id, ecash)) = self
902            .client_ctx
903            .module_db()
904            .autocommit(
905                |dbtx, _| {
906                    Box::pin(self.send_ecash_dbtx(
907                        dbtx,
908                        amount,
909                        custom_meta.clone(),
910                        include_invite,
911                    ))
912                },
913                Some(100),
914            )
915            .await
916            .expect("Failed to commit dbtx after 100 retries")
917        {
918            return Ok((operation_id, ecash));
919        }
920
921        self.client_ctx
922            .global_api()
923            .session_count()
924            .await
925            .map_err(|_| SendECashError::Offline)?;
926
927        let operation_id = OperationId::new_random();
928
929        let output = self
930            .create_output_bundle(operation_id, represent_amount(amount))
931            .await;
932        let output = self.client_ctx.make_client_outputs(output);
933        let cm = custom_meta.clone();
934
935        let range = self
936            .client_ctx
937            .finalize_and_submit_transaction(
938                operation_id,
939                MintCommonInit::KIND.as_str(),
940                move |change_outpoint_range| MintOperationMeta::Reissue {
941                    change_outpoint_range,
942                    amount,
943                    custom_meta: cm.clone(),
944                },
945                TransactionBuilder::new().with_outputs(output),
946            )
947            .await
948            .map_err(|error| match error {
949                TransactionSubmitError::InsufficientFunds(_) => SendECashError::InsufficientBalance,
950                other => SendECashError::Failed(other),
951            })?;
952
953        for outpoint in range {
954            self.await_output_sm_success(operation_id, outpoint)
955                .await
956                .map_err(|_| SendECashError::Failure)?;
957        }
958
959        Box::pin(self.send(amount, custom_meta, include_invite)).await
960    }
961
962    /// Fast path for [`Self::send`]: if exact change is already held, spend
963    /// those notes and record the send operation. Returns `None` when exact
964    /// change is not available, leaving the caller to make change.
965    ///
966    /// # Cancel safety
967    ///
968    /// All writes go to the passed `dbtx`, so this inherits the cancel safety
969    /// of that transaction: it must be run inside an `autocommit` (as `send`
970    /// does) for the note spend and the operation log entry to commit or roll
971    /// back together.
972    async fn send_ecash_dbtx(
973        &self,
974        dbtx: &mut DatabaseTransaction<'_>,
975        remaining_amount: Amount,
976        custom_meta: Value,
977        include_invite: bool,
978    ) -> Result<Option<(OperationId, ECash)>, Infallible> {
979        let Some(notes) = Self::select_exact_change(&mut dbtx.to_ref_nc(), remaining_amount).await
980        else {
981            return Ok(None);
982        };
983
984        for spendable_note in &notes {
985            self.remove_spendable_note(dbtx, spendable_note).await;
986        }
987
988        let ecash = if include_invite {
989            let invite = self.client_ctx.get_invite_code().await;
990            ECash::new_with_invite(notes, &invite)
991        } else {
992            ECash::new(self.federation_id, notes)
993        }
994        .with_unit(self.cfg.amount_unit);
995        let amount = ecash.amount();
996        let operation_id = OperationId::new_random();
997
998        self.client_ctx
999            .add_operation_log_entry_dbtx(
1000                dbtx,
1001                operation_id,
1002                MintCommonInit::KIND.as_str(),
1003                MintOperationMeta::Send {
1004                    ecash: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
1005                    custom_meta,
1006                },
1007            )
1008            .await;
1009
1010        self.client_ctx
1011            .log_event(
1012                dbtx,
1013                SendPaymentEvent {
1014                    operation_id,
1015                    amount,
1016                    ecash: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
1017                },
1018            )
1019            .await;
1020
1021        let sender = self.balance_update_sender.clone();
1022        dbtx.on_commit(move || sender.send_replace(()));
1023
1024        Ok(Some((operation_id, ecash)))
1025    }
1026
1027    /// Receive the `ECash` by reissuing the notes and return the operation ID.
1028    pub async fn receive(
1029        &self,
1030        ecash: ECash,
1031        custom_meta: Value,
1032    ) -> Result<OperationId, ReceiveECashError> {
1033        let operation_id = OperationId::from_encodable(&ecash);
1034
1035        if ecash.mint() != Some(self.federation_id) {
1036            return Err(ReceiveECashError::WrongFederation);
1037        }
1038
1039        if ecash
1040            .notes()
1041            .iter()
1042            .any(|note| note.amount() <= self.cfg.fee_consensus.base_fee())
1043        {
1044            return Err(ReceiveECashError::UneconomicalDenomination);
1045        }
1046
1047        let input =
1048            Self::create_input_bundle(operation_id, ecash.notes(), true, self.cfg.amount_unit);
1049        let input = self.client_ctx.make_client_inputs(input);
1050        let ec = base32::encode_prefixed(FEDIMINT_PREFIX, &ecash);
1051
1052        self.client_ctx
1053            .finalize_and_submit_transaction(
1054                operation_id,
1055                MintCommonInit::KIND.as_str(),
1056                move |change_outpoint_range| MintOperationMeta::Receive {
1057                    change_outpoint_range,
1058                    ecash: ec.clone(),
1059                    custom_meta: custom_meta.clone(),
1060                },
1061                TransactionBuilder::new().with_inputs(input),
1062            )
1063            .await
1064            .map_err(|error| match error {
1065                TransactionSubmitError::OperationAlreadyExists(_) => {
1066                    ReceiveECashError::AlreadyReceived
1067                }
1068                TransactionSubmitError::InsufficientFunds(_) => {
1069                    ReceiveECashError::InsufficientFunds
1070                }
1071                other => ReceiveECashError::Failed(other),
1072            })?;
1073
1074        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1075
1076        self.client_ctx
1077            .log_event(
1078                &mut dbtx,
1079                ReceivePaymentEvent {
1080                    operation_id,
1081                    amount: ecash.amount(),
1082                },
1083            )
1084            .await;
1085
1086        dbtx.commit_tx().await;
1087
1088        Ok(operation_id)
1089    }
1090
1091    /// Computes the exact fee a `receive(ecash)` would incur given the client's
1092    /// current note inventory, without submitting anything.
1093    ///
1094    /// This runs the same change generation the real receive does
1095    /// (`create_final_inputs_and_outputs`, including funding selection and
1096    /// rebalancing) against a non-committable transaction that is dropped
1097    /// rather than committed, so the client's notes are read but left
1098    /// untouched. The quote is point-in-time: it depends on the current
1099    /// inventory and can move as notes change.
1100    pub async fn receive_fee_quote(
1101        &self,
1102        ecash: &ECash,
1103    ) -> Result<FeeQuote, TransactionSubmitError> {
1104        // A receive submits the ecash notes as explicit inputs and no explicit
1105        // outputs; the shared, module-agnostic fee quote runs the primary-module
1106        // balancing (rebalancing + minting change) over the real inventory.
1107        let notes = ecash.notes();
1108        let input_amount: Amount = notes.iter().map(SpendableNote::amount).sum();
1109        let input_fee: Amount = notes
1110            .iter()
1111            .map(|note| self.cfg.fee_consensus.fee(note.amount()))
1112            .sum();
1113
1114        self.client_ctx
1115            .fee_quote(
1116                OperationId::new_random(),
1117                FeeQuoteRequest {
1118                    input_amount: Amounts::new_custom(self.cfg.amount_unit, input_amount),
1119                    output_amount: Amounts::ZERO,
1120                    input_fee: Amounts::new_custom(self.cfg.amount_unit, input_fee),
1121                    output_fee: Amounts::ZERO,
1122                },
1123            )
1124            .await
1125    }
1126
1127    /// Computes the fee a `send(amount)` would incur given the client's current
1128    /// note inventory, without sending anything.
1129    ///
1130    /// A send is free when the client's existing notes can cover the (rounded)
1131    /// amount exactly — it just hands those notes out. Otherwise the send first
1132    /// reissues itself the right denominations, and that self-reissue
1133    /// transaction is the only thing a send ever pays a fee for. This quote
1134    /// mirrors that: it returns [`FeeQuote::ZERO`] when exact change is
1135    /// available, and otherwise quotes the reissue the same way the real send
1136    /// submits it (explicit outputs `represent_amount(amount)`, no explicit
1137    /// inputs) via the shared, module-agnostic fee quote over the real
1138    /// inventory. The quote is point-in-time: it depends on the current
1139    /// inventory and can move as notes change.
1140    pub async fn send_fee_quote(&self, amount: Amount) -> Result<FeeQuote, TransactionSubmitError> {
1141        let amount = round_to_multiple(amount, client_denominations().next().unwrap().amount());
1142
1143        // Exact-change path: handing out existing notes never costs a fee.
1144        if self.can_make_exact_change(amount).await {
1145            return Ok(FeeQuote::ZERO);
1146        }
1147
1148        // Reissue path: the send mints itself `represent_amount(amount)` as
1149        // explicit outputs (no explicit inputs) and the primary module funds and
1150        // balances it. Quote that exact transaction.
1151        let denominations = represent_amount(amount);
1152        let output_amount: Amount = denominations.iter().map(|d| d.amount()).sum();
1153        let output_fee: Amount = denominations
1154            .iter()
1155            .map(|d| self.cfg.fee_consensus.fee(d.amount()))
1156            .sum();
1157
1158        self.client_ctx
1159            .fee_quote(
1160                OperationId::new_random(),
1161                FeeQuoteRequest {
1162                    input_amount: Amounts::ZERO,
1163                    output_amount: Amounts::new_custom(self.cfg.amount_unit, output_amount),
1164                    input_fee: Amounts::ZERO,
1165                    output_fee: Amounts::new_custom(self.cfg.amount_unit, output_fee),
1166                },
1167            )
1168            .await
1169    }
1170
1171    /// Returns whether the client's current notes can be handed out to cover
1172    /// `amount` exactly — the free path in [`Self::send`] — without modifying
1173    /// the inventory. Shares its greedy selection with
1174    /// [`Self::send_ecash_dbtx`] via [`Self::select_exact_change`].
1175    async fn can_make_exact_change(&self, remaining_amount: Amount) -> bool {
1176        let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1177
1178        Self::select_exact_change(&mut dbtx, remaining_amount)
1179            .await
1180            .is_some()
1181    }
1182
1183    /// Greedily selects spendable notes (largest-first) summing to *exactly*
1184    /// `remaining_amount`, or `None` if the inventory can't make exact change.
1185    /// Reads the DB but does not modify it. Single source of truth for the free
1186    /// (hand-out-existing-notes) selection shared by [`Self::send_ecash_dbtx`]
1187    /// and [`Self::can_make_exact_change`].
1188    async fn select_exact_change(
1189        dbtx: &mut DatabaseTransaction<'_>,
1190        mut remaining_amount: Amount,
1191    ) -> Option<Vec<SpendableNote>> {
1192        let mut stream = dbtx
1193            .find_by_prefix_sorted_descending(&SpendableNotePrefix)
1194            .await
1195            .map(|entry| entry.0.0);
1196
1197        let mut notes = vec![];
1198
1199        while let Some(spendable_note) = stream.next().await {
1200            remaining_amount = match remaining_amount.checked_sub(spendable_note.amount()) {
1201                Some(amount) => amount,
1202                None => continue,
1203            };
1204
1205            notes.push(spendable_note);
1206
1207            if remaining_amount == Amount::ZERO {
1208                break;
1209            }
1210        }
1211
1212        (remaining_amount == Amount::ZERO).then_some(notes)
1213    }
1214
1215    /// Await the final state of the receive operation.
1216    pub async fn await_final_receive_operation_state(
1217        &self,
1218        operation_id: OperationId,
1219    ) -> Result<FinalReceiveOperationState, OperationLookupError> {
1220        let operation = self.client_ctx.get_operation(operation_id).await?;
1221        let mut stream = self.notifier.subscribe(operation_id).await;
1222
1223        let mut stream = self
1224            .client_ctx
1225            .outcome_or_updates(&operation, operation_id, |_| true, move || {
1226                async_stream::stream! {
1227                    loop {
1228                        if let Some(MintClientStateMachines::Receive(state)) = stream.next().await {
1229                            match state.state {
1230                                ReceiveSMState::Pending => {}
1231                                ReceiveSMState::Success => {
1232                                    yield FinalReceiveOperationState::Success;
1233                                    return;
1234                                }
1235                                ReceiveSMState::Rejected(..) => {
1236                                    yield FinalReceiveOperationState::Rejected;
1237                                    return;
1238                                }
1239                            }
1240                        }
1241                    }
1242                }
1243            })
1244            .into_stream();
1245
1246        let mut final_state = None;
1247
1248        while let Some(state) = stream.next().await {
1249            final_state = Some(state);
1250        }
1251
1252        Ok(final_state.expect("Stream contains one final state"))
1253    }
1254
1255    async fn remove_spendable_note(
1256        &self,
1257        dbtx: &mut DatabaseTransaction<'_>,
1258        spendable_note: &SpendableNote,
1259    ) {
1260        dbtx.remove_entry(&SpendableNoteKey(spendable_note.clone()))
1261            .await
1262            .expect("Must delete existing spendable note");
1263    }
1264}
1265
1266/// Hands out guardians so that only one slice request is outstanding to each.
1267///
1268/// Whichever peer finishes first takes the next slice, so a slow guardian
1269/// receives less work without anyone having to measure how slow it is, and one
1270/// that is not answering ties up a single request rather than a share of all
1271/// of them.
1272#[derive(Clone)]
1273struct PeerPool {
1274    receiver: async_channel::Receiver<PeerId>,
1275    sender: async_channel::Sender<PeerId>,
1276}
1277
1278impl PeerPool {
1279    fn new(peers: &BTreeSet<PeerId>) -> Self {
1280        let (sender, receiver) = async_channel::bounded(peers.len().max(1));
1281
1282        for peer in peers {
1283            sender
1284                .try_send(*peer)
1285                .expect("Capacity was sized to hold every peer");
1286        }
1287
1288        Self { receiver, sender }
1289    }
1290
1291    /// Wait for a guardian with no request outstanding.
1292    async fn acquire(&self) -> PeerId {
1293        self.receiver
1294            .recv()
1295            .await
1296            .expect("The sender is held for as long as the receiver")
1297    }
1298
1299    /// Take a guardian out of rotation, putting it back once it has sat out
1300    /// [`PEER_READMISSION`].
1301    ///
1302    /// Dropping it for good would cost a guardian that timed out once the rest
1303    /// of the recovery, which on a federation of four is a quarter of the
1304    /// capacity thrown away for a single bad request.
1305    fn retire(&self, peer: PeerId) {
1306        let pool = self.clone();
1307
1308        fedimint_core::runtime::spawn("mintv2 recovery peer readmission", async move {
1309            fedimint_core::runtime::sleep(PEER_READMISSION).await;
1310
1311            pool.release(peer);
1312        });
1313    }
1314
1315    /// Put a guardian back for the next slice.
1316    fn release(&self, peer: PeerId) {
1317        self.sender
1318            .try_send(peer)
1319            .expect("Only peers taken from the pool are put back");
1320    }
1321}
1322
1323/// Download a slice, asking one guardian at a time and holding it for the
1324/// duration of the request.
1325async fn download_slice(
1326    module_api: DynModuleApi,
1327    peers: PeerPool,
1328    start: u64,
1329    end: u64,
1330    expected_hash: sha256::Hash,
1331) -> Vec<RecoveryItem> {
1332    let mut timeouts = custom_backoff(SLICE_TIMEOUT, MAX_SLICE_TIMEOUT, None);
1333
1334    loop {
1335        let peer = peers.acquire().await;
1336
1337        let timeout = timeouts.next().expect("The backoff never gives up");
1338
1339        let result = module_api
1340            .fetch_recovery_slice(peer, timeout, start, end)
1341            .await;
1342
1343        match result {
1344            Ok(data) if data.consensus_hash::<sha256::Hash>() == expected_hash => {
1345                peers.release(peer);
1346
1347                return data;
1348            }
1349            // Either served something the other guardians disagree with, or
1350            // did not answer at all. Either way it sits out for a while: a
1351            // request that reaches the timeout took many times longer than a
1352            // healthy guardian needs, so asking it again mostly buys another
1353            // timeout. The timeout grows in case it is the client's own
1354            // connection that is too slow for the initial one.
1355            Ok(_) | Err(_) => peers.retire(peer),
1356        }
1357    }
1358}
1359
1360/// A failure to send e-cash by preparing notes to hand to the recipient.
1361#[derive(Error, Debug)]
1362#[non_exhaustive]
1363pub enum SendECashError {
1364    /// The client needs to reissue notes to make change, but has no
1365    /// connection to the federation to do so.
1366    #[error("We need to reissue notes but the client is offline")]
1367    Offline,
1368    /// The client's balance cannot cover the amount requested.
1369    #[error("The clients balance is insufficient")]
1370    InsufficientBalance,
1371    /// The client failed to prepare the notes for a reason it cannot
1372    /// recover from.
1373    #[error("A non-recoverable error has occurred")]
1374    Failure,
1375    /// The change-making reissue transaction could not be submitted for a
1376    /// reason unrelated to funding.
1377    #[error("The reissue transaction could not be submitted")]
1378    Failed(#[source] TransactionSubmitError),
1379}
1380
1381/// A failure to receive e-cash by reissuing it.
1382#[derive(Error, Debug)]
1383#[non_exhaustive]
1384pub enum ReceiveECashError {
1385    /// The e-cash was issued by a different federation.
1386    #[error("The ECash is from a different federation")]
1387    WrongFederation,
1388
1389    /// One of the notes is worth no more than the fee to reissue it.
1390    #[error("ECash contains an uneconomical denomination")]
1391    UneconomicalDenomination,
1392
1393    /// The client cannot cover the fee the reissue costs.
1394    #[error("Receiving ecash requires additional funds")]
1395    InsufficientFunds,
1396
1397    /// An operation for this exact e-cash already exists, so it was already
1398    /// received.
1399    #[error("The ECash was already received")]
1400    AlreadyReceived,
1401
1402    /// The reissue transaction could not be submitted for a reason unrelated
1403    /// to funding.
1404    #[error("The reissue transaction could not be submitted")]
1405    Failed(#[source] TransactionSubmitError),
1406}
1407
1408#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1409pub enum FinalReceiveOperationState {
1410    // The ecash notes have been reissued
1411    Success,
1412    // The ecash notes were already spent
1413    Rejected,
1414}
1415
1416#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1417pub enum MintClientStateMachines {
1418    Input(InputStateMachine),
1419    Output(MintOutputStateMachine),
1420    Receive(ReceiveStateMachine),
1421}
1422
1423impl IntoDynInstance for MintClientStateMachines {
1424    type DynType = DynState;
1425
1426    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1427        DynState::from_typed(instance_id, self)
1428    }
1429}
1430
1431impl State for MintClientStateMachines {
1432    type ModuleContext = MintClientContext;
1433
1434    fn transitions(
1435        &self,
1436        context: &Self::ModuleContext,
1437        global_context: &DynGlobalClientContext,
1438    ) -> Vec<StateTransition<Self>> {
1439        match self {
1440            MintClientStateMachines::Input(redemption_state) => {
1441                sm_enum_variant_translation!(
1442                    redemption_state.transitions(context, global_context),
1443                    MintClientStateMachines::Input
1444                )
1445            }
1446            MintClientStateMachines::Output(issuance_state) => {
1447                sm_enum_variant_translation!(
1448                    issuance_state.transitions(context, global_context),
1449                    MintClientStateMachines::Output
1450                )
1451            }
1452            MintClientStateMachines::Receive(receive_state) => {
1453                sm_enum_variant_translation!(
1454                    receive_state.transitions(context, global_context),
1455                    MintClientStateMachines::Receive
1456                )
1457            }
1458        }
1459    }
1460
1461    fn operation_id(&self) -> OperationId {
1462        match self {
1463            MintClientStateMachines::Input(redemption_state) => redemption_state.operation_id(),
1464            MintClientStateMachines::Output(issuance_state) => issuance_state.operation_id(),
1465            MintClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
1466        }
1467    }
1468}
1469
1470fn round_to_multiple(amount: Amount, min_denomiation: Amount) -> Amount {
1471    Amount::from_msats(amount.msats.next_multiple_of(min_denomiation.msats))
1472}
1473
1474fn represent_amount_with_fees(
1475    mut remaining_amount: Amount,
1476    fee_consensus: &FeeConsensus,
1477) -> Vec<Denomination> {
1478    let mut denominations = Vec::new();
1479
1480    // Add denominations with a greedy algorithm
1481    for denomination in client_denominations().rev() {
1482        let n_add =
1483            remaining_amount / (denomination.amount() + fee_consensus.fee(denomination.amount()));
1484
1485        denominations.extend(std::iter::repeat_n(denomination, n_add as usize));
1486
1487        remaining_amount -=
1488            n_add * (denomination.amount() + fee_consensus.fee(denomination.amount()));
1489    }
1490
1491    // We sort the notes by amount to minimize the leaked information.
1492    denominations.sort();
1493
1494    denominations
1495}
1496
1497fn represent_amount(mut remaining_amount: Amount) -> Vec<Denomination> {
1498    let mut denominations = Vec::new();
1499
1500    // Add denominations with a greedy algorithm
1501    for denomination in client_denominations().rev() {
1502        let n_add = remaining_amount / denomination.amount();
1503
1504        denominations.extend(std::iter::repeat_n(denomination, n_add as usize));
1505
1506        remaining_amount -= n_add * denomination.amount();
1507    }
1508
1509    denominations
1510}