fedimint_mint_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
9// Backup and restore logic
10pub mod backup;
11/// Modularized Cli for sending and receiving out-of-band ecash
12#[cfg(feature = "cli")]
13mod cli;
14/// Database keys used throughout the mint client module
15pub mod client_db;
16/// State machines for mint inputs
17mod input;
18/// State machines for out-of-band transmitted e-cash notes
19mod oob;
20/// State machines for mint outputs
21pub mod output;
22
23pub mod event;
24
25/// API client impl for mint-specific requests
26pub mod api;
27
28pub mod repair_wallet;
29
30use std::cmp::{Ordering, min};
31use std::collections::{BTreeMap, BTreeSet};
32use std::fmt;
33use std::fmt::{Display, Formatter};
34use std::io::Read;
35use std::str::FromStr;
36use std::sync::Arc;
37use std::time::Duration;
38
39use anyhow::{Context as _, anyhow, bail, ensure};
40use async_stream::{stream, try_stream};
41use backup::recovery::MintRecovery;
42use base64::Engine as _;
43use bitcoin_hashes::{Hash, HashEngine as BitcoinHashEngine, sha256, sha256t};
44use client_db::{
45    DbKeyPrefix, NoteKeyPrefix, RecoveryFinalizedKey, ReusedNoteIndices, migrate_state_to_v2,
46    migrate_to_v1,
47};
48use event::{NoteSpent, OOBNotesReissued, OOBNotesSpent};
49use fedimint_client_module::db::{ClientModuleMigrationFn, migrate_state};
50use fedimint_client_module::module::init::{
51    ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs,
52};
53use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
54use fedimint_client_module::oplog::{OperationLogEntry, UpdateStreamOrOutcome};
55use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
56use fedimint_client_module::transaction::{
57    ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
58    ClientOutputSM, TransactionBuilder,
59};
60use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
61use fedimint_core::config::{FederationId, FederationIdPrefix};
62use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
63use fedimint_core::db::{
64    AutocommitError, Database, DatabaseTransaction, DatabaseVersion,
65    IDatabaseTransactionOpsCoreTyped,
66};
67use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
68use fedimint_core::invite_code::InviteCode;
69use fedimint_core::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
70use fedimint_core::module::{
71    ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
72};
73use fedimint_core::secp256k1::{All, Keypair, Secp256k1};
74use fedimint_core::util::{BoxFuture, BoxStream, NextOrPending, SafeUrl};
75use fedimint_core::{
76    Amount, OutPoint, PeerId, Tiered, TieredCounts, TieredMulti, TransactionId, apply,
77    async_trait_maybe_send, push_db_pair_items,
78};
79use fedimint_derive_secret::{ChildId, DerivableSecret};
80use fedimint_logging::LOG_CLIENT_MODULE_MINT;
81pub use fedimint_mint_common as common;
82use fedimint_mint_common::config::{FeeConsensus, MintClientConfig};
83pub use fedimint_mint_common::*;
84use futures::future::try_join_all;
85use futures::{StreamExt, pin_mut};
86use hex::ToHex;
87use input::MintInputStateCreatedBundle;
88use itertools::Itertools as _;
89use oob::MintOOBStatesCreatedMulti;
90use output::MintOutputStatesCreatedMulti;
91use serde::{Deserialize, Serialize};
92use strum::IntoEnumIterator;
93use tbs::AggregatePublicKey;
94use thiserror::Error;
95use tracing::{debug, warn};
96
97use crate::backup::EcashBackup;
98use crate::client_db::{
99    CancelledOOBSpendKey, CancelledOOBSpendKeyPrefix, NextECashNoteIndexKey,
100    NextECashNoteIndexKeyPrefix, NoteKey,
101};
102use crate::input::{MintInputCommon, MintInputStateMachine, MintInputStates};
103use crate::oob::{MintOOBStateMachine, MintOOBStates};
104use crate::output::{
105    MintOutputCommon, MintOutputStateMachine, MintOutputStates, NoteIssuanceRequest,
106};
107
108const MINT_E_CASH_TYPE_CHILD_ID: ChildId = ChildId(0);
109
110/// An encapsulation of [`FederationId`] and e-cash notes in the form of
111/// [`TieredMulti<SpendableNote>`] for the purpose of spending e-cash
112/// out-of-band. Also used for validating and reissuing such out-of-band notes.
113///
114/// ## Invariants
115/// * Has to contain at least one `Notes` item
116/// * Has to contain at least one `FederationIdPrefix` item
117#[derive(Clone, Debug, Encodable, PartialEq, Eq)]
118pub struct OOBNotes(Vec<OOBNotesPart>);
119
120/// For extendability [`OOBNotes`] consists of parts, where client can ignore
121/// ones they don't understand.
122#[derive(Clone, Debug, Decodable, Encodable, PartialEq, Eq)]
123enum OOBNotesPart {
124    Notes(TieredMulti<SpendableNote>),
125    FederationIdPrefix(FederationIdPrefix),
126    /// Invite code to join the federation by which the e-cash was issued
127    ///
128    /// Introduced in 0.3.0
129    Invite {
130        // This is a vec for future-proofness, in case we want to include multiple guardian APIs
131        peer_apis: Vec<(PeerId, SafeUrl)>,
132        federation_id: FederationId,
133    },
134    ApiSecret(String),
135    #[encodable_default]
136    Default {
137        variant: u64,
138        bytes: Vec<u8>,
139    },
140}
141
142impl OOBNotes {
143    pub fn new(
144        federation_id_prefix: FederationIdPrefix,
145        notes: TieredMulti<SpendableNote>,
146    ) -> Self {
147        Self(vec![
148            OOBNotesPart::FederationIdPrefix(federation_id_prefix),
149            OOBNotesPart::Notes(notes),
150        ])
151    }
152
153    pub fn new_with_invite(notes: TieredMulti<SpendableNote>, invite: &InviteCode) -> Self {
154        let mut data = vec![
155            // FIXME: once we can break compatibility with 0.2 we can remove the prefix in case an
156            // invite is present
157            OOBNotesPart::FederationIdPrefix(invite.federation_id().to_prefix()),
158            OOBNotesPart::Notes(notes),
159            OOBNotesPart::Invite {
160                peer_apis: vec![(invite.peer(), invite.url())],
161                federation_id: invite.federation_id(),
162            },
163        ];
164        if let Some(api_secret) = invite.api_secret() {
165            data.push(OOBNotesPart::ApiSecret(api_secret));
166        }
167        Self(data)
168    }
169
170    pub fn federation_id_prefix(&self) -> FederationIdPrefix {
171        self.0
172            .iter()
173            .find_map(|data| match data {
174                OOBNotesPart::FederationIdPrefix(prefix) => Some(*prefix),
175                OOBNotesPart::Invite { federation_id, .. } => Some(federation_id.to_prefix()),
176                _ => None,
177            })
178            .expect("Invariant violated: OOBNotes does not contain a FederationIdPrefix")
179    }
180
181    pub fn notes(&self) -> &TieredMulti<SpendableNote> {
182        self.0
183            .iter()
184            .find_map(|data| match data {
185                OOBNotesPart::Notes(notes) => Some(notes),
186                _ => None,
187            })
188            .expect("Invariant violated: OOBNotes does not contain any notes")
189    }
190
191    pub fn notes_json(&self) -> Result<serde_json::Value, serde_json::Error> {
192        let mut notes_map = serde_json::Map::new();
193        for notes in &self.0 {
194            match notes {
195                OOBNotesPart::Notes(notes) => {
196                    let notes_json = serde_json::to_value(notes)?;
197                    notes_map.insert("notes".to_string(), notes_json);
198                }
199                OOBNotesPart::FederationIdPrefix(prefix) => {
200                    notes_map.insert(
201                        "federation_id_prefix".to_string(),
202                        serde_json::to_value(prefix.to_string())?,
203                    );
204                }
205                OOBNotesPart::Invite {
206                    peer_apis,
207                    federation_id,
208                } => {
209                    let (peer_id, api) = peer_apis
210                        .first()
211                        .cloned()
212                        .expect("Decoding makes sure peer_apis isn't empty");
213                    notes_map.insert(
214                        "invite".to_string(),
215                        serde_json::to_value(InviteCode::new(
216                            api,
217                            peer_id,
218                            *federation_id,
219                            self.api_secret(),
220                        ))?,
221                    );
222                }
223                OOBNotesPart::ApiSecret(_) => { /* already covered inside `Invite` */ }
224                OOBNotesPart::Default { variant, bytes } => {
225                    notes_map.insert(
226                        format!("default_{variant}"),
227                        serde_json::to_value(bytes.encode_hex::<String>())?,
228                    );
229                }
230            }
231        }
232        Ok(serde_json::Value::Object(notes_map))
233    }
234
235    pub fn federation_invite(&self) -> Option<InviteCode> {
236        self.0.iter().find_map(|data| {
237            let OOBNotesPart::Invite {
238                peer_apis,
239                federation_id,
240            } = data
241            else {
242                return None;
243            };
244            let (peer_id, api) = peer_apis
245                .first()
246                .cloned()
247                .expect("Decoding makes sure peer_apis isn't empty");
248            Some(InviteCode::new(
249                api,
250                peer_id,
251                *federation_id,
252                self.api_secret(),
253            ))
254        })
255    }
256
257    fn api_secret(&self) -> Option<String> {
258        self.0.iter().find_map(|data| {
259            let OOBNotesPart::ApiSecret(api_secret) = data else {
260                return None;
261            };
262            Some(api_secret.clone())
263        })
264    }
265}
266
267impl Decodable for OOBNotes {
268    fn consensus_decode_partial<R: Read>(
269        r: &mut R,
270        _modules: &ModuleDecoderRegistry,
271    ) -> Result<Self, DecodeError> {
272        let inner =
273            Vec::<OOBNotesPart>::consensus_decode_partial(r, &ModuleDecoderRegistry::default())?;
274
275        // TODO: maybe write some macros for defining TLV structs?
276        if !inner
277            .iter()
278            .any(|data| matches!(data, OOBNotesPart::Notes(_)))
279        {
280            return Err(DecodeError::from_str(
281                "No e-cash notes were found in OOBNotes data",
282            ));
283        }
284
285        let maybe_federation_id_prefix = inner.iter().find_map(|data| match data {
286            OOBNotesPart::FederationIdPrefix(prefix) => Some(*prefix),
287            _ => None,
288        });
289
290        let maybe_invite = inner.iter().find_map(|data| match data {
291            OOBNotesPart::Invite {
292                federation_id,
293                peer_apis,
294            } => Some((federation_id, peer_apis)),
295            _ => None,
296        });
297
298        match (maybe_federation_id_prefix, maybe_invite) {
299            (Some(p), Some((ip, _))) => {
300                if p != ip.to_prefix() {
301                    return Err(DecodeError::from_str(
302                        "Inconsistent Federation ID provided in OOBNotes data",
303                    ));
304                }
305            }
306            (None, None) => {
307                return Err(DecodeError::from_str(
308                    "No Federation ID provided in OOBNotes data",
309                ));
310            }
311            _ => {}
312        }
313
314        if let Some((_, invite)) = maybe_invite
315            && invite.is_empty()
316        {
317            return Err(DecodeError::from_str("Invite didn't contain API endpoints"));
318        }
319
320        Ok(OOBNotes(inner))
321    }
322}
323
324const BASE64_URL_SAFE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
325    &base64::alphabet::URL_SAFE,
326    base64::engine::general_purpose::PAD,
327);
328
329impl FromStr for OOBNotes {
330    type Err = anyhow::Error;
331
332    /// Decode a set of out-of-band e-cash notes from a base64 string.
333    fn from_str(s: &str) -> Result<Self, Self::Err> {
334        let s: String = s.chars().filter(|&c| !c.is_whitespace()).collect();
335
336        let bytes = if let Ok(bytes) = BASE64_URL_SAFE.decode(&s) {
337            bytes
338        } else {
339            base64::engine::general_purpose::STANDARD.decode(&s)?
340        };
341        let oob_notes: OOBNotes =
342            Decodable::consensus_decode_whole(&bytes, &ModuleDecoderRegistry::default())?;
343
344        ensure!(!oob_notes.notes().is_empty(), "OOBNotes cannot be empty");
345
346        Ok(oob_notes)
347    }
348}
349
350impl Display for OOBNotes {
351    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
352        let bytes = Encodable::consensus_encode_to_vec(self);
353
354        f.write_str(&BASE64_URL_SAFE.encode(&bytes))
355    }
356}
357
358impl Serialize for OOBNotes {
359    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360    where
361        S: serde::Serializer,
362    {
363        serializer.serialize_str(&self.to_string())
364    }
365}
366
367impl<'de> Deserialize<'de> for OOBNotes {
368    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
369    where
370        D: serde::Deserializer<'de>,
371    {
372        let s = String::deserialize(deserializer)?;
373        FromStr::from_str(&s).map_err(serde::de::Error::custom)
374    }
375}
376
377impl OOBNotes {
378    /// Returns the total value of all notes in msat as `Amount`
379    pub fn total_amount(&self) -> Amount {
380        self.notes().total_amount()
381    }
382}
383
384/// The high-level state of a reissue operation started with
385/// [`MintClientModule::reissue_external_notes`].
386#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
387pub enum ReissueExternalNotesState {
388    /// The operation has been created and is waiting to be accepted by the
389    /// federation.
390    Created,
391    /// We are waiting for blind signatures to arrive but can already assume the
392    /// transaction to be successful.
393    Issuing,
394    /// The operation has been completed successfully.
395    Done,
396    /// Some error happened and the operation failed.
397    Failed(String),
398}
399
400/// The high-level state of a raw e-cash spend operation started with
401/// [`MintClientModule::spend_notes_with_selector`].
402#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
403pub enum SpendOOBState {
404    /// The e-cash has been selected and given to the caller
405    Created,
406    /// The user requested a cancellation of the operation, we are waiting for
407    /// the outcome of the cancel transaction.
408    UserCanceledProcessing,
409    /// The user-requested cancellation was successful, we got all our money
410    /// back.
411    UserCanceledSuccess,
412    /// The user-requested cancellation failed, the e-cash notes have been spent
413    /// by someone else already.
414    UserCanceledFailure,
415    /// We tried to cancel the operation automatically after the timeout but
416    /// failed, indicating the recipient reissued the e-cash to themselves,
417    /// making the out-of-band spend **successful**.
418    Success,
419    /// We tried to cancel the operation automatically after the timeout and
420    /// succeeded, indicating the recipient did not reissue the e-cash to
421    /// themselves, meaning the out-of-band spend **failed**.
422    Refunded,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct MintOperationMeta {
427    pub variant: MintOperationMetaVariant,
428    pub amount: Amount,
429    pub extra_meta: serde_json::Value,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
433#[serde(rename_all = "snake_case")]
434pub enum MintOperationMetaVariant {
435    // TODO: add migrations for operation log and clean up schema
436    /// Either `legacy_out_point` or both `txid` and `out_point_indices` will be
437    /// present.
438    Reissuance {
439        // Removed in 0.3.0:
440        #[serde(skip_serializing, default, rename = "out_point")]
441        legacy_out_point: Option<OutPoint>,
442        // Introduced in 0.3.0:
443        #[serde(default)]
444        txid: Option<TransactionId>,
445        // Introduced in 0.3.0:
446        #[serde(default)]
447        out_point_indices: Vec<u64>,
448    },
449    SpendOOB {
450        requested_amount: Amount,
451        oob_notes: OOBNotes,
452    },
453}
454
455#[derive(Debug, Clone)]
456pub struct MintClientInit;
457
458impl ModuleInit for MintClientInit {
459    type Common = MintCommonInit;
460
461    async fn dump_database(
462        &self,
463        dbtx: &mut DatabaseTransaction<'_>,
464        prefix_names: Vec<String>,
465    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
466        let mut mint_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
467            BTreeMap::new();
468        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
469            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
470        });
471
472        for table in filtered_prefixes {
473            match table {
474                DbKeyPrefix::Note => {
475                    push_db_pair_items!(
476                        dbtx,
477                        NoteKeyPrefix,
478                        NoteKey,
479                        SpendableNoteUndecoded,
480                        mint_client_items,
481                        "Notes"
482                    );
483                }
484                DbKeyPrefix::NextECashNoteIndex => {
485                    push_db_pair_items!(
486                        dbtx,
487                        NextECashNoteIndexKeyPrefix,
488                        NextECashNoteIndexKey,
489                        u64,
490                        mint_client_items,
491                        "NextECashNoteIndex"
492                    );
493                }
494                DbKeyPrefix::CancelledOOBSpend => {
495                    push_db_pair_items!(
496                        dbtx,
497                        CancelledOOBSpendKeyPrefix,
498                        CancelledOOBSpendKey,
499                        (),
500                        mint_client_items,
501                        "CancelledOOBSpendKey"
502                    );
503                }
504                DbKeyPrefix::RecoveryFinalized => {
505                    if let Some(val) = dbtx.get_value(&RecoveryFinalizedKey).await {
506                        mint_client_items.insert("RecoveryFinalized".to_string(), Box::new(val));
507                    }
508                }
509                DbKeyPrefix::RecoveryState
510                | DbKeyPrefix::ReusedNoteIndices
511                | DbKeyPrefix::ExternalReservedStart
512                | DbKeyPrefix::CoreInternalReservedStart
513                | DbKeyPrefix::CoreInternalReservedEnd => {}
514            }
515        }
516
517        Box::new(mint_client_items.into_iter())
518    }
519}
520
521#[apply(async_trait_maybe_send!)]
522impl ClientModuleInit for MintClientInit {
523    type Module = MintClientModule;
524
525    fn supported_api_versions(&self) -> MultiApiVersion {
526        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
527            .expect("no version conflicts")
528    }
529
530    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
531        Ok(MintClientModule {
532            federation_id: *args.federation_id(),
533            cfg: args.cfg().clone(),
534            secret: args.module_root_secret().clone(),
535            secp: Secp256k1::new(),
536            notifier: args.notifier().clone(),
537            client_ctx: args.context(),
538        })
539    }
540
541    async fn recover(
542        &self,
543        args: &ClientModuleRecoverArgs<Self>,
544        snapshot: Option<&<Self::Module as ClientModule>::Backup>,
545    ) -> anyhow::Result<()> {
546        args.recover_from_history::<MintRecovery>(self, snapshot)
547            .await
548    }
549
550    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
551        let mut migrations: BTreeMap<DatabaseVersion, ClientModuleMigrationFn> = BTreeMap::new();
552        migrations.insert(DatabaseVersion(0), |dbtx, _, _| {
553            Box::pin(migrate_to_v1(dbtx))
554        });
555        migrations.insert(DatabaseVersion(1), |_, active_states, inactive_states| {
556            Box::pin(async { migrate_state(active_states, inactive_states, migrate_state_to_v2) })
557        });
558
559        migrations
560    }
561
562    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
563        Some(
564            DbKeyPrefix::iter()
565                .map(|p| p as u8)
566                .chain(
567                    DbKeyPrefix::ExternalReservedStart as u8
568                        ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
569                )
570                .collect(),
571        )
572    }
573}
574
575/// The `MintClientModule` is responsible for handling e-cash minting
576/// operations. It interacts with the mint server to issue, reissue, and
577/// validate e-cash notes.
578///
579/// # Derivable Secret
580///
581/// The `DerivableSecret` is a cryptographic secret that can be used to derive
582/// other secrets. In the context of the `MintClientModule`, it is used to
583/// derive the blinding and spend keys for e-cash notes. The `DerivableSecret`
584/// is initialized when the `MintClientModule` is created and is kept private
585/// within the module.
586///
587/// # Blinding Key
588///
589/// The blinding key is derived from the `DerivableSecret` and is used to blind
590/// the e-cash note during the issuance process. This ensures that the mint
591/// server cannot link the e-cash note to the client that requested it,
592/// providing privacy for the client.
593///
594/// # Spend Key
595///
596/// The spend key is also derived from the `DerivableSecret` and is used to
597/// spend the e-cash note. Only the client that possesses the `DerivableSecret`
598/// can derive the correct spend key to spend the e-cash note. This ensures that
599/// only the owner of the e-cash note can spend it.
600#[derive(Debug)]
601pub struct MintClientModule {
602    federation_id: FederationId,
603    cfg: MintClientConfig,
604    secret: DerivableSecret,
605    secp: Secp256k1<All>,
606    notifier: ModuleNotifier<MintClientStateMachines>,
607    pub client_ctx: ClientContext<Self>,
608}
609
610// TODO: wrap in Arc
611#[derive(Debug, Clone)]
612pub struct MintClientContext {
613    pub client_ctx: ClientContext<MintClientModule>,
614    pub mint_decoder: Decoder,
615    pub tbs_pks: Tiered<AggregatePublicKey>,
616    pub peer_tbs_pks: BTreeMap<PeerId, Tiered<tbs::PublicKeyShare>>,
617    pub secret: DerivableSecret,
618    // FIXME: putting a DB ref here is an antipattern, global context should become more powerful
619    // but we need to consider it more carefully as its APIs will be harder to change.
620    pub module_db: Database,
621}
622
623impl MintClientContext {
624    fn await_cancel_oob_payment(&self, operation_id: OperationId) -> BoxFuture<'static, ()> {
625        let db = self.module_db.clone();
626        Box::pin(async move {
627            db.wait_key_exists(&CancelledOOBSpendKey(operation_id))
628                .await;
629        })
630    }
631}
632
633impl Context for MintClientContext {
634    const KIND: Option<ModuleKind> = Some(KIND);
635}
636
637#[apply(async_trait_maybe_send!)]
638impl ClientModule for MintClientModule {
639    type Init = MintClientInit;
640    type Common = MintModuleTypes;
641    type Backup = EcashBackup;
642    type ModuleStateMachineContext = MintClientContext;
643    type States = MintClientStateMachines;
644
645    fn context(&self) -> Self::ModuleStateMachineContext {
646        MintClientContext {
647            client_ctx: self.client_ctx.clone(),
648            mint_decoder: self.decoder(),
649            tbs_pks: self.cfg.tbs_pks.clone(),
650            peer_tbs_pks: self.cfg.peer_tbs_pks.clone(),
651            secret: self.secret.clone(),
652            module_db: self.client_ctx.module_db().clone(),
653        }
654    }
655
656    fn input_fee(
657        &self,
658        amount: Amount,
659        _input: &<Self::Common as ModuleCommon>::Input,
660    ) -> Option<Amount> {
661        Some(self.cfg.fee_consensus.fee(amount))
662    }
663
664    fn output_fee(
665        &self,
666        amount: Amount,
667        _output: &<Self::Common as ModuleCommon>::Output,
668    ) -> Option<Amount> {
669        Some(self.cfg.fee_consensus.fee(amount))
670    }
671
672    #[cfg(feature = "cli")]
673    async fn handle_cli_command(
674        &self,
675        args: &[std::ffi::OsString],
676    ) -> anyhow::Result<serde_json::Value> {
677        cli::handle_cli_command(self, args).await
678    }
679
680    fn supports_backup(&self) -> bool {
681        true
682    }
683
684    async fn backup(&self) -> anyhow::Result<EcashBackup> {
685        self.client_ctx
686            .module_db()
687            .autocommit(
688                |dbtx_ctx, _| {
689                    Box::pin(async { self.prepare_plaintext_ecash_backup(dbtx_ctx).await })
690                },
691                None,
692            )
693            .await
694            .map_err(|e| match e {
695                AutocommitError::ClosureError { error, .. } => error,
696                AutocommitError::CommitFailed { last_error, .. } => {
697                    anyhow!("Commit to DB failed: {last_error}")
698                }
699            })
700    }
701
702    fn supports_being_primary(&self) -> bool {
703        true
704    }
705
706    async fn create_final_inputs_and_outputs(
707        &self,
708        dbtx: &mut DatabaseTransaction<'_>,
709        operation_id: OperationId,
710        mut input_amount: Amount,
711        mut output_amount: Amount,
712    ) -> anyhow::Result<(
713        ClientInputBundle<MintInput, MintClientStateMachines>,
714        ClientOutputBundle<MintOutput, MintClientStateMachines>,
715    )> {
716        let consolidation_inputs = self.consolidate_notes(dbtx).await?;
717
718        input_amount += consolidation_inputs
719            .iter()
720            .map(|input| input.0.amount)
721            .sum();
722
723        output_amount += consolidation_inputs
724            .iter()
725            .map(|input| self.cfg.fee_consensus.fee(input.0.amount))
726            .sum();
727
728        let additional_inputs = self
729            .create_sufficient_input(dbtx, output_amount.saturating_sub(input_amount))
730            .await?;
731
732        input_amount += additional_inputs.iter().map(|input| input.0.amount).sum();
733
734        output_amount += additional_inputs
735            .iter()
736            .map(|input| self.cfg.fee_consensus.fee(input.0.amount))
737            .sum();
738
739        let outputs = self
740            .create_output(
741                dbtx,
742                operation_id,
743                2,
744                input_amount.saturating_sub(output_amount),
745            )
746            .await;
747
748        Ok((
749            create_bundle_for_inputs(
750                [consolidation_inputs, additional_inputs].concat(),
751                operation_id,
752            ),
753            outputs,
754        ))
755    }
756
757    async fn await_primary_module_output(
758        &self,
759        operation_id: OperationId,
760        out_point: OutPoint,
761    ) -> anyhow::Result<()> {
762        self.await_output_finalized(operation_id, out_point).await
763    }
764
765    async fn get_balance(&self, dbtx: &mut DatabaseTransaction<'_>) -> Amount {
766        self.get_note_counts_by_denomination(dbtx)
767            .await
768            .total_amount()
769    }
770
771    async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
772        Box::pin(
773            self.notifier
774                .subscribe_all_operations()
775                .filter_map(|state| async move {
776                    #[allow(deprecated)]
777                    match state {
778                        MintClientStateMachines::Output(MintOutputStateMachine {
779                            state: MintOutputStates::Succeeded(_),
780                            ..
781                        })
782                        | MintClientStateMachines::Input(MintInputStateMachine {
783                            state: MintInputStates::Created(_) | MintInputStates::CreatedBundle(_),
784                            ..
785                        })
786                        | MintClientStateMachines::OOB(MintOOBStateMachine {
787                            state: MintOOBStates::Created(_) | MintOOBStates::CreatedMulti(_),
788                            ..
789                        }) => Some(()),
790                        // The negative cases are enumerated explicitly to avoid missing new
791                        // variants that need to trigger balance updates
792                        MintClientStateMachines::Output(MintOutputStateMachine {
793                            state:
794                                MintOutputStates::Created(_)
795                                | MintOutputStates::CreatedMulti(_)
796                                | MintOutputStates::Failed(_)
797                                | MintOutputStates::Aborted(_),
798                            ..
799                        })
800                        | MintClientStateMachines::Input(MintInputStateMachine {
801                            state:
802                                MintInputStates::Error(_)
803                                | MintInputStates::Success(_)
804                                | MintInputStates::Refund(_)
805                                | MintInputStates::RefundedBundle(_)
806                                | MintInputStates::RefundedPerNote(_)
807                                | MintInputStates::RefundSuccess(_),
808                            ..
809                        })
810                        | MintClientStateMachines::OOB(MintOOBStateMachine {
811                            state:
812                                MintOOBStates::TimeoutRefund(_)
813                                | MintOOBStates::UserRefund(_)
814                                | MintOOBStates::UserRefundMulti(_),
815                            ..
816                        })
817                        | MintClientStateMachines::Restore(_) => None,
818                    }
819                }),
820        )
821    }
822
823    async fn leave(&self, dbtx: &mut DatabaseTransaction<'_>) -> anyhow::Result<()> {
824        let balance = ClientModule::get_balance(self, dbtx).await;
825        if Amount::from_sats(0) < balance {
826            bail!("Outstanding balance: {balance}");
827        }
828
829        if !self.client_ctx.get_own_active_states().await.is_empty() {
830            bail!("Pending operations")
831        }
832        Ok(())
833    }
834    async fn handle_rpc(
835        &self,
836        method: String,
837        request: serde_json::Value,
838    ) -> BoxStream<'_, anyhow::Result<serde_json::Value>> {
839        Box::pin(try_stream! {
840            match method.as_str() {
841                "reissue_external_notes" => {
842                    let req: ReissueExternalNotesRequest = serde_json::from_value(request)?;
843                    let result = self.reissue_external_notes(req.oob_notes, req.extra_meta).await?;
844                    yield serde_json::to_value(result)?;
845                }
846                "subscribe_reissue_external_notes" => {
847                    let req: SubscribeReissueExternalNotesRequest = serde_json::from_value(request)?;
848                    let stream = self.subscribe_reissue_external_notes(req.operation_id).await?;
849                    for await state in stream.into_stream() {
850                        yield serde_json::to_value(state)?;
851                    }
852                }
853                "spend_notes" => {
854                    let req: SpendNotesRequest = serde_json::from_value(request)?;
855                    let result = self.spend_notes_with_selector(
856                        &SelectNotesWithExactAmount,
857                        req.amount,
858                        req.try_cancel_after,
859                        req.include_invite,
860                        req.extra_meta
861                    ).await?;
862                    yield serde_json::to_value(result)?;
863                }
864                "spend_notes_expert" => {
865                    let req: SpendNotesExpertRequest = serde_json::from_value(request)?;
866                    let result = self.spend_notes_with_selector(
867                        &SelectNotesWithAtleastAmount,
868                        req.min_amount,
869                        req.try_cancel_after,
870                        req.include_invite,
871                        req.extra_meta
872                    ).await?;
873                    yield serde_json::to_value(result)?;
874                }
875                "validate_notes" => {
876                    let req: ValidateNotesRequest = serde_json::from_value(request)?;
877                    let result = self.validate_notes(&req.oob_notes)?;
878                    yield serde_json::to_value(result)?;
879                }
880                "try_cancel_spend_notes" => {
881                    let req: TryCancelSpendNotesRequest = serde_json::from_value(request)?;
882                    let result = self.try_cancel_spend_notes(req.operation_id).await;
883                    yield serde_json::to_value(result)?;
884                }
885                "subscribe_spend_notes" => {
886                    let req: SubscribeSpendNotesRequest = serde_json::from_value(request)?;
887                    let stream = self.subscribe_spend_notes(req.operation_id).await?;
888                    for await state in stream.into_stream() {
889                        yield serde_json::to_value(state)?;
890                    }
891                }
892                "await_spend_oob_refund" => {
893                    let req: AwaitSpendOobRefundRequest = serde_json::from_value(request)?;
894                    let value = self.await_spend_oob_refund(req.operation_id).await;
895                    yield serde_json::to_value(value)?;
896                }
897                "note_counts_by_denomination" => {
898                    let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
899                    let note_counts = self.get_note_counts_by_denomination(&mut dbtx).await;
900                    yield serde_json::to_value(note_counts)?;
901                }
902                _ => {
903                    Err(anyhow::format_err!("Unknown method: {}", method))?;
904                    unreachable!()
905                },
906            }
907        })
908    }
909}
910
911#[derive(Deserialize)]
912struct ReissueExternalNotesRequest {
913    oob_notes: OOBNotes,
914    extra_meta: serde_json::Value,
915}
916
917#[derive(Deserialize)]
918struct SubscribeReissueExternalNotesRequest {
919    operation_id: OperationId,
920}
921
922/// Caution: if no notes of the correct denomination are available the next
923/// bigger note will be selected. You might want to use `spend_notes` instead.
924#[derive(Deserialize)]
925struct SpendNotesExpertRequest {
926    min_amount: Amount,
927    try_cancel_after: Duration,
928    include_invite: bool,
929    extra_meta: serde_json::Value,
930}
931
932#[derive(Deserialize)]
933struct SpendNotesRequest {
934    amount: Amount,
935    try_cancel_after: Duration,
936    include_invite: bool,
937    extra_meta: serde_json::Value,
938}
939
940#[derive(Deserialize)]
941struct ValidateNotesRequest {
942    oob_notes: OOBNotes,
943}
944
945#[derive(Deserialize)]
946struct TryCancelSpendNotesRequest {
947    operation_id: OperationId,
948}
949
950#[derive(Deserialize)]
951struct SubscribeSpendNotesRequest {
952    operation_id: OperationId,
953}
954
955#[derive(Deserialize)]
956struct AwaitSpendOobRefundRequest {
957    operation_id: OperationId,
958}
959
960#[derive(thiserror::Error, Debug, Clone)]
961pub enum ReissueExternalNotesError {
962    #[error("Federation ID does not match")]
963    WrongFederationId,
964    #[error("We already reissued these notes")]
965    AlreadyReissued,
966}
967
968impl MintClientModule {
969    async fn create_sufficient_input(
970        &self,
971        dbtx: &mut DatabaseTransaction<'_>,
972        min_amount: Amount,
973    ) -> anyhow::Result<Vec<(ClientInput<MintInput>, SpendableNote)>> {
974        if min_amount == Amount::ZERO {
975            return Ok(vec![]);
976        }
977
978        let selected_notes = Self::select_notes(
979            dbtx,
980            &SelectNotesWithAtleastAmount,
981            min_amount,
982            self.cfg.fee_consensus.clone(),
983        )
984        .await?;
985
986        for (amount, note) in selected_notes.iter_items() {
987            debug!(target: LOG_CLIENT_MODULE_MINT, %amount, %note, "Spending note as sufficient input to fund a tx");
988            MintClientModule::delete_spendable_note(&self.client_ctx, dbtx, amount, note).await;
989        }
990
991        let inputs = self.create_input_from_notes(selected_notes)?;
992
993        assert!(!inputs.is_empty());
994
995        Ok(inputs)
996    }
997
998    /// Returns the number of held e-cash notes per denomination
999    #[deprecated(
1000        since = "0.5.0",
1001        note = "Use `get_note_counts_by_denomination` instead"
1002    )]
1003    pub async fn get_notes_tier_counts(&self, dbtx: &mut DatabaseTransaction<'_>) -> TieredCounts {
1004        self.get_note_counts_by_denomination(dbtx).await
1005    }
1006
1007    /// Pick [`SpendableNote`]s by given counts, when available
1008    ///
1009    /// Return the notes picked, and counts of notes that were not available.
1010    pub async fn get_available_notes_by_tier_counts(
1011        &self,
1012        dbtx: &mut DatabaseTransaction<'_>,
1013        counts: TieredCounts,
1014    ) -> (TieredMulti<SpendableNoteUndecoded>, TieredCounts) {
1015        dbtx.find_by_prefix(&NoteKeyPrefix)
1016            .await
1017            .fold(
1018                (TieredMulti::<SpendableNoteUndecoded>::default(), counts),
1019                |(mut notes, mut counts), (key, note)| async move {
1020                    let amount = key.amount;
1021                    if 0 < counts.get(amount) {
1022                        counts.dec(amount);
1023                        notes.push(amount, note);
1024                    }
1025
1026                    (notes, counts)
1027                },
1028            )
1029            .await
1030    }
1031
1032    // TODO: put "notes per denomination" default into cfg
1033    /// Creates a mint output close to the given `amount`, issuing e-cash
1034    /// notes such that the client holds `notes_per_denomination` notes of each
1035    /// e-cash note denomination held.
1036    pub async fn create_output(
1037        &self,
1038        dbtx: &mut DatabaseTransaction<'_>,
1039        operation_id: OperationId,
1040        notes_per_denomination: u16,
1041        exact_amount: Amount,
1042    ) -> ClientOutputBundle<MintOutput, MintClientStateMachines> {
1043        if exact_amount == Amount::ZERO {
1044            return ClientOutputBundle::new(vec![], vec![]);
1045        }
1046
1047        let denominations = represent_amount(
1048            exact_amount,
1049            &self.get_note_counts_by_denomination(dbtx).await,
1050            &self.cfg.tbs_pks,
1051            notes_per_denomination,
1052            &self.cfg.fee_consensus,
1053        );
1054
1055        let mut outputs = Vec::new();
1056        let mut issuance_requests = Vec::new();
1057
1058        for (amount, num) in denominations.iter() {
1059            for _ in 0..num {
1060                let (issuance_request, blind_nonce) = self.new_ecash_note(amount, dbtx).await;
1061
1062                debug!(
1063                    %amount,
1064                    "Generated issuance request"
1065                );
1066
1067                outputs.push(ClientOutput {
1068                    output: MintOutput::new_v0(amount, blind_nonce),
1069                    amount,
1070                });
1071
1072                issuance_requests.push((amount, issuance_request));
1073            }
1074        }
1075
1076        let state_generator = Arc::new(move |out_point_range: OutPointRange| {
1077            assert_eq!(out_point_range.count(), issuance_requests.len());
1078            vec![MintClientStateMachines::Output(MintOutputStateMachine {
1079                common: MintOutputCommon {
1080                    operation_id,
1081                    out_point_range,
1082                },
1083                state: MintOutputStates::CreatedMulti(MintOutputStatesCreatedMulti {
1084                    issuance_requests: out_point_range
1085                        .into_iter()
1086                        .map(|out_point| out_point.out_idx)
1087                        .zip(issuance_requests.clone())
1088                        .collect(),
1089                }),
1090            })]
1091        });
1092
1093        ClientOutputBundle::new(
1094            outputs,
1095            vec![ClientOutputSM {
1096                state_machines: state_generator,
1097            }],
1098        )
1099    }
1100
1101    /// Returns the number of held e-cash notes per denomination
1102    pub async fn get_note_counts_by_denomination(
1103        &self,
1104        dbtx: &mut DatabaseTransaction<'_>,
1105    ) -> TieredCounts {
1106        dbtx.find_by_prefix(&NoteKeyPrefix)
1107            .await
1108            .fold(
1109                TieredCounts::default(),
1110                |mut acc, (key, _note)| async move {
1111                    acc.inc(key.amount, 1);
1112                    acc
1113                },
1114            )
1115            .await
1116    }
1117
1118    /// Returns the number of held e-cash notes per denomination
1119    #[deprecated(
1120        since = "0.5.0",
1121        note = "Use `get_note_counts_by_denomination` instead"
1122    )]
1123    pub async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> TieredCounts {
1124        self.get_note_counts_by_denomination(dbtx).await
1125    }
1126
1127    /// Wait for the e-cash notes to be retrieved. If this is not possible
1128    /// because another terminal state was reached an error describing the
1129    /// failure is returned.
1130    pub async fn await_output_finalized(
1131        &self,
1132        operation_id: OperationId,
1133        out_point: OutPoint,
1134    ) -> anyhow::Result<()> {
1135        let stream = self
1136            .notifier
1137            .subscribe(operation_id)
1138            .await
1139            .filter_map(|state| async {
1140                let MintClientStateMachines::Output(state) = state else {
1141                    return None;
1142                };
1143
1144                if state.common.txid() != out_point.txid
1145                    || !state
1146                        .common
1147                        .out_point_range
1148                        .out_idx_iter()
1149                        .contains(&out_point.out_idx)
1150                {
1151                    return None;
1152                }
1153
1154                match state.state {
1155                    MintOutputStates::Succeeded(_) => Some(Ok(())),
1156                    MintOutputStates::Aborted(_) => Some(Err(anyhow!("Transaction was rejected"))),
1157                    MintOutputStates::Failed(failed) => Some(Err(anyhow!(
1158                        "Failed to finalize transaction: {}",
1159                        failed.error
1160                    ))),
1161                    MintOutputStates::Created(_) | MintOutputStates::CreatedMulti(_) => None,
1162                }
1163            });
1164        pin_mut!(stream);
1165
1166        stream.next_or_pending().await
1167    }
1168
1169    /// Provisional implementation of note consolidation
1170    ///
1171    /// When a certain denomination crosses the threshold of notes allowed,
1172    /// spend some chunk of them as inputs.
1173    ///
1174    /// Return notes and the sume of their amount.
1175    pub async fn consolidate_notes(
1176        &self,
1177        dbtx: &mut DatabaseTransaction<'_>,
1178    ) -> anyhow::Result<Vec<(ClientInput<MintInput>, SpendableNote)>> {
1179        /// At how many notes of the same denomination should we try to
1180        /// consolidate
1181        const MAX_NOTES_PER_TIER_TRIGGER: usize = 8;
1182        /// Number of notes per tier to leave after threshold was crossed
1183        const MIN_NOTES_PER_TIER: usize = 4;
1184        /// Maximum number of notes to consolidate per one tx,
1185        /// to limit the size of a transaction produced.
1186        const MAX_NOTES_TO_CONSOLIDATE_IN_TX: usize = 20;
1187        // it's fine, it's just documentation
1188        #[allow(clippy::assertions_on_constants)]
1189        {
1190            assert!(MIN_NOTES_PER_TIER <= MAX_NOTES_PER_TIER_TRIGGER);
1191        }
1192
1193        let counts = self.get_note_counts_by_denomination(dbtx).await;
1194
1195        let should_consolidate = counts
1196            .iter()
1197            .any(|(_, count)| MAX_NOTES_PER_TIER_TRIGGER < count);
1198
1199        if !should_consolidate {
1200            return Ok(vec![]);
1201        }
1202
1203        let mut max_count = MAX_NOTES_TO_CONSOLIDATE_IN_TX;
1204
1205        let excessive_counts: TieredCounts = counts
1206            .iter()
1207            .map(|(amount, count)| {
1208                let take = (count.saturating_sub(MIN_NOTES_PER_TIER)).min(max_count);
1209
1210                max_count -= take;
1211                (amount, take)
1212            })
1213            .collect();
1214
1215        let (selected_notes, unavailable) = self
1216            .get_available_notes_by_tier_counts(dbtx, excessive_counts)
1217            .await;
1218
1219        debug_assert!(
1220            unavailable.is_empty(),
1221            "Can't have unavailable notes on a subset of all notes: {unavailable:?}"
1222        );
1223
1224        if !selected_notes.is_empty() {
1225            debug!(target: LOG_CLIENT_MODULE_MINT, note_num=selected_notes.count_items(), denominations_msats=?selected_notes.iter_items().map(|(amount, _)| amount.msats).collect::<Vec<_>>(), "Will consolidate excessive notes");
1226        }
1227
1228        let mut selected_notes_decoded = vec![];
1229        for (amount, note) in selected_notes.iter_items() {
1230            let spendable_note_decoded = note.decode()?;
1231            debug!(target: LOG_CLIENT_MODULE_MINT, %amount, %note, "Consolidating note");
1232            Self::delete_spendable_note(&self.client_ctx, dbtx, amount, &spendable_note_decoded)
1233                .await;
1234            selected_notes_decoded.push((amount, spendable_note_decoded));
1235        }
1236
1237        self.create_input_from_notes(selected_notes_decoded.into_iter().collect())
1238    }
1239
1240    /// Create a mint input from external, potentially untrusted notes
1241    #[allow(clippy::type_complexity)]
1242    pub fn create_input_from_notes(
1243        &self,
1244        notes: TieredMulti<SpendableNote>,
1245    ) -> anyhow::Result<Vec<(ClientInput<MintInput>, SpendableNote)>> {
1246        let mut inputs_and_notes = Vec::new();
1247
1248        for (amount, spendable_note) in notes.into_iter_items() {
1249            let key = self
1250                .cfg
1251                .tbs_pks
1252                .get(amount)
1253                .ok_or(anyhow!("Invalid amount tier: {amount}"))?;
1254
1255            let note = spendable_note.note();
1256
1257            if !note.verify(*key) {
1258                bail!("Invalid note");
1259            }
1260
1261            inputs_and_notes.push((
1262                ClientInput {
1263                    input: MintInput::new_v0(amount, note),
1264                    keys: vec![spendable_note.spend_key],
1265                    amount,
1266                },
1267                spendable_note,
1268            ));
1269        }
1270
1271        Ok(inputs_and_notes)
1272    }
1273
1274    async fn spend_notes_oob(
1275        &self,
1276        dbtx: &mut DatabaseTransaction<'_>,
1277        notes_selector: &impl NotesSelector,
1278        amount: Amount,
1279        try_cancel_after: Duration,
1280    ) -> anyhow::Result<(
1281        OperationId,
1282        Vec<MintClientStateMachines>,
1283        TieredMulti<SpendableNote>,
1284    )> {
1285        ensure!(
1286            amount > Amount::ZERO,
1287            "zero-amount out-of-band spends are not supported"
1288        );
1289
1290        let selected_notes =
1291            Self::select_notes(dbtx, notes_selector, amount, FeeConsensus::zero()).await?;
1292
1293        let operation_id = spendable_notes_to_operation_id(&selected_notes);
1294
1295        for (amount, note) in selected_notes.iter_items() {
1296            debug!(target: LOG_CLIENT_MODULE_MINT, %amount, %note, "Spending note as oob");
1297            MintClientModule::delete_spendable_note(&self.client_ctx, dbtx, amount, note).await;
1298        }
1299
1300        let state_machines = vec![MintClientStateMachines::OOB(MintOOBStateMachine {
1301            operation_id,
1302            state: MintOOBStates::CreatedMulti(MintOOBStatesCreatedMulti {
1303                spendable_notes: selected_notes.clone().into_iter_items().collect(),
1304                timeout: fedimint_core::time::now() + try_cancel_after,
1305            }),
1306        })];
1307
1308        Ok((operation_id, state_machines, selected_notes))
1309    }
1310
1311    pub async fn await_spend_oob_refund(&self, operation_id: OperationId) -> SpendOOBRefund {
1312        Box::pin(
1313            self.notifier
1314                .subscribe(operation_id)
1315                .await
1316                .filter_map(|state| async {
1317                    let MintClientStateMachines::OOB(state) = state else {
1318                        return None;
1319                    };
1320
1321                    match state.state {
1322                        MintOOBStates::TimeoutRefund(refund) => Some(SpendOOBRefund {
1323                            user_triggered: false,
1324                            transaction_ids: vec![refund.refund_txid],
1325                        }),
1326                        MintOOBStates::UserRefund(refund) => Some(SpendOOBRefund {
1327                            user_triggered: true,
1328                            transaction_ids: vec![refund.refund_txid],
1329                        }),
1330                        MintOOBStates::UserRefundMulti(refund) => Some(SpendOOBRefund {
1331                            user_triggered: true,
1332                            transaction_ids: vec![refund.refund_txid],
1333                        }),
1334                        MintOOBStates::Created(_) | MintOOBStates::CreatedMulti(_) => None,
1335                    }
1336                }),
1337        )
1338        .next_or_pending()
1339        .await
1340    }
1341
1342    /// Select notes with `requested_amount` using `notes_selector`.
1343    async fn select_notes(
1344        dbtx: &mut DatabaseTransaction<'_>,
1345        notes_selector: &impl NotesSelector,
1346        requested_amount: Amount,
1347        fee_consensus: FeeConsensus,
1348    ) -> anyhow::Result<TieredMulti<SpendableNote>> {
1349        let note_stream = dbtx
1350            .find_by_prefix_sorted_descending(&NoteKeyPrefix)
1351            .await
1352            .map(|(key, note)| (key.amount, note));
1353
1354        notes_selector
1355            .select_notes(note_stream, requested_amount, fee_consensus)
1356            .await?
1357            .into_iter_items()
1358            .map(|(amt, snote)| Ok((amt, snote.decode()?)))
1359            .collect::<anyhow::Result<TieredMulti<_>>>()
1360    }
1361
1362    async fn get_all_spendable_notes(
1363        dbtx: &mut DatabaseTransaction<'_>,
1364    ) -> TieredMulti<SpendableNoteUndecoded> {
1365        (dbtx
1366            .find_by_prefix(&NoteKeyPrefix)
1367            .await
1368            .map(|(key, note)| (key.amount, note))
1369            .collect::<Vec<_>>()
1370            .await)
1371            .into_iter()
1372            .collect()
1373    }
1374
1375    async fn get_next_note_index(
1376        &self,
1377        dbtx: &mut DatabaseTransaction<'_>,
1378        amount: Amount,
1379    ) -> NoteIndex {
1380        NoteIndex(
1381            dbtx.get_value(&NextECashNoteIndexKey(amount))
1382                .await
1383                .unwrap_or(0),
1384        )
1385    }
1386
1387    /// Derive the note `DerivableSecret` from the Mint's `secret` the `amount`
1388    /// tier and `note_idx`
1389    ///
1390    /// Static to help re-use in other places, that don't have a whole [`Self`]
1391    /// available
1392    ///
1393    /// # E-Cash Note Creation
1394    ///
1395    /// When creating an e-cash note, the `MintClientModule` first derives the
1396    /// blinding and spend keys from the `DerivableSecret`. It then creates a
1397    /// `NoteIssuanceRequest` containing the blinded spend key and sends it to
1398    /// the mint server. The mint server signs the blinded spend key and
1399    /// returns it to the client. The client can then unblind the signed
1400    /// spend key to obtain the e-cash note, which can be spent using the
1401    /// spend key.
1402    pub fn new_note_secret_static(
1403        secret: &DerivableSecret,
1404        amount: Amount,
1405        note_idx: NoteIndex,
1406    ) -> DerivableSecret {
1407        assert_eq!(secret.level(), 2);
1408        debug!(?secret, %amount, %note_idx, "Deriving new mint note");
1409        secret
1410            .child_key(MINT_E_CASH_TYPE_CHILD_ID) // TODO: cache
1411            .child_key(ChildId(note_idx.as_u64()))
1412            .child_key(ChildId(amount.msats))
1413    }
1414
1415    /// We always keep track of an incrementing index in the database and use
1416    /// it as part of the derivation path for the note secret. This ensures that
1417    /// we never reuse the same note secret twice.
1418    async fn new_note_secret(
1419        &self,
1420        amount: Amount,
1421        dbtx: &mut DatabaseTransaction<'_>,
1422    ) -> DerivableSecret {
1423        let new_idx = self.get_next_note_index(dbtx, amount).await;
1424        dbtx.insert_entry(&NextECashNoteIndexKey(amount), &new_idx.next().as_u64())
1425            .await;
1426        Self::new_note_secret_static(&self.secret, amount, new_idx)
1427    }
1428
1429    pub async fn new_ecash_note(
1430        &self,
1431        amount: Amount,
1432        dbtx: &mut DatabaseTransaction<'_>,
1433    ) -> (NoteIssuanceRequest, BlindNonce) {
1434        let secret = self.new_note_secret(amount, dbtx).await;
1435        NoteIssuanceRequest::new(&self.secp, &secret)
1436    }
1437
1438    /// Try to reissue e-cash notes received from a third party to receive them
1439    /// in our wallet. The progress and outcome can be observed using
1440    /// [`MintClientModule::subscribe_reissue_external_notes`].
1441    /// Can return error of type [`ReissueExternalNotesError`]
1442    pub async fn reissue_external_notes<M: Serialize + Send>(
1443        &self,
1444        oob_notes: OOBNotes,
1445        extra_meta: M,
1446    ) -> anyhow::Result<OperationId> {
1447        let notes = oob_notes.notes().clone();
1448        let federation_id_prefix = oob_notes.federation_id_prefix();
1449
1450        ensure!(
1451            notes.total_amount() > Amount::ZERO,
1452            "Reissuing zero-amount e-cash isn't supported"
1453        );
1454
1455        if federation_id_prefix != self.federation_id.to_prefix() {
1456            bail!(ReissueExternalNotesError::WrongFederationId);
1457        }
1458
1459        let operation_id = OperationId(
1460            notes
1461                .consensus_hash::<sha256t::Hash<OOBReissueTag>>()
1462                .to_byte_array(),
1463        );
1464
1465        let amount = notes.total_amount();
1466        let mint_inputs = self.create_input_from_notes(notes)?;
1467
1468        let tx = TransactionBuilder::new().with_inputs(
1469            self.client_ctx
1470                .make_dyn(create_bundle_for_inputs(mint_inputs, operation_id)),
1471        );
1472
1473        let extra_meta = serde_json::to_value(extra_meta)
1474            .expect("MintClientModule::reissue_external_notes extra_meta is serializable");
1475        let operation_meta_gen = move |change_range: OutPointRange| MintOperationMeta {
1476            variant: MintOperationMetaVariant::Reissuance {
1477                legacy_out_point: None,
1478                txid: Some(change_range.txid()),
1479                out_point_indices: change_range
1480                    .into_iter()
1481                    .map(|out_point| out_point.out_idx)
1482                    .collect(),
1483            },
1484            amount,
1485            extra_meta: extra_meta.clone(),
1486        };
1487
1488        self.client_ctx
1489            .finalize_and_submit_transaction(
1490                operation_id,
1491                MintCommonInit::KIND.as_str(),
1492                operation_meta_gen,
1493                tx,
1494            )
1495            .await
1496            .context(ReissueExternalNotesError::AlreadyReissued)?;
1497        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1498        self.client_ctx
1499            .log_event(&mut dbtx, OOBNotesReissued { amount })
1500            .await;
1501        dbtx.commit_tx().await;
1502
1503        Ok(operation_id)
1504    }
1505
1506    /// Subscribe to updates on the progress of a reissue operation started with
1507    /// [`MintClientModule::reissue_external_notes`].
1508    pub async fn subscribe_reissue_external_notes(
1509        &self,
1510        operation_id: OperationId,
1511    ) -> anyhow::Result<UpdateStreamOrOutcome<ReissueExternalNotesState>> {
1512        let operation = self.mint_operation(operation_id).await?;
1513        let (txid, out_points) = match operation.meta::<MintOperationMeta>().variant {
1514            MintOperationMetaVariant::Reissuance {
1515                legacy_out_point,
1516                txid,
1517                out_point_indices,
1518            } => {
1519                // Either txid or legacy_out_point will be present, so we should always
1520                // have a source for the txid
1521                let txid = txid
1522                    .or(legacy_out_point.map(|out_point| out_point.txid))
1523                    .context("Empty reissuance not permitted, this should never happen")?;
1524
1525                let out_points = out_point_indices
1526                    .into_iter()
1527                    .map(|out_idx| OutPoint { txid, out_idx })
1528                    .chain(legacy_out_point)
1529                    .collect::<Vec<_>>();
1530
1531                (txid, out_points)
1532            }
1533            MintOperationMetaVariant::SpendOOB { .. } => bail!("Operation is not a reissuance"),
1534        };
1535
1536        let client_ctx = self.client_ctx.clone();
1537
1538        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1539            stream! {
1540                yield ReissueExternalNotesState::Created;
1541
1542                match client_ctx
1543                    .transaction_updates(operation_id)
1544                    .await
1545                    .await_tx_accepted(txid)
1546                    .await
1547                {
1548                    Ok(()) => {
1549                        yield ReissueExternalNotesState::Issuing;
1550                    }
1551                    Err(e) => {
1552                        yield ReissueExternalNotesState::Failed(format!("Transaction not accepted {e:?}"));
1553                        return;
1554                    }
1555                }
1556
1557                for out_point in out_points {
1558                    if let Err(e) = client_ctx.self_ref().await_output_finalized(operation_id, out_point).await {
1559                        yield ReissueExternalNotesState::Failed(e.to_string());
1560                        return;
1561                    }
1562                }
1563                yield ReissueExternalNotesState::Done;
1564            }}
1565        ))
1566    }
1567
1568    /// Fetches and removes notes of *at least* amount `min_amount` from the
1569    /// wallet to be sent to the recipient out of band. These spends can be
1570    /// canceled by calling [`MintClientModule::try_cancel_spend_notes`] as long
1571    /// as the recipient hasn't reissued the e-cash notes themselves yet.
1572    ///
1573    /// The client will also automatically attempt to cancel the operation after
1574    /// `try_cancel_after` time has passed. This is a safety mechanism to avoid
1575    /// users forgetting about failed out-of-band transactions. The timeout
1576    /// should be chosen such that the recipient (who is potentially offline at
1577    /// the time of receiving the e-cash notes) had a reasonable timeframe to
1578    /// come online and reissue the notes themselves.
1579    #[deprecated(
1580        since = "0.5.0",
1581        note = "Use `spend_notes_with_selector` instead, with `SelectNotesWithAtleastAmount` to maintain the same behavior"
1582    )]
1583    pub async fn spend_notes<M: Serialize + Send>(
1584        &self,
1585        min_amount: Amount,
1586        try_cancel_after: Duration,
1587        include_invite: bool,
1588        extra_meta: M,
1589    ) -> anyhow::Result<(OperationId, OOBNotes)> {
1590        self.spend_notes_with_selector(
1591            &SelectNotesWithAtleastAmount,
1592            min_amount,
1593            try_cancel_after,
1594            include_invite,
1595            extra_meta,
1596        )
1597        .await
1598    }
1599
1600    /// Fetches and removes notes from the wallet to be sent to the recipient
1601    /// out of band. The note selection algorithm is determined by
1602    /// `note_selector`. See the [`NotesSelector`] trait for available
1603    /// implementations.
1604    ///
1605    /// These spends can be canceled by calling
1606    /// [`MintClientModule::try_cancel_spend_notes`] as long
1607    /// as the recipient hasn't reissued the e-cash notes themselves yet.
1608    ///
1609    /// The client will also automatically attempt to cancel the operation after
1610    /// `try_cancel_after` time has passed. This is a safety mechanism to avoid
1611    /// users forgetting about failed out-of-band transactions. The timeout
1612    /// should be chosen such that the recipient (who is potentially offline at
1613    /// the time of receiving the e-cash notes) had a reasonable timeframe to
1614    /// come online and reissue the notes themselves.
1615    pub async fn spend_notes_with_selector<M: Serialize + Send>(
1616        &self,
1617        notes_selector: &impl NotesSelector,
1618        requested_amount: Amount,
1619        try_cancel_after: Duration,
1620        include_invite: bool,
1621        extra_meta: M,
1622    ) -> anyhow::Result<(OperationId, OOBNotes)> {
1623        let federation_id_prefix = self.federation_id.to_prefix();
1624        let extra_meta = serde_json::to_value(extra_meta)
1625            .expect("MintClientModule::spend_notes extra_meta is serializable");
1626
1627        self.client_ctx
1628            .module_db()
1629            .autocommit(
1630                |dbtx, _| {
1631                    let extra_meta = extra_meta.clone();
1632                    Box::pin(async {
1633                        let (operation_id, states, notes) = self
1634                            .spend_notes_oob(
1635                                dbtx,
1636                                notes_selector,
1637                                requested_amount,
1638                                try_cancel_after,
1639                            )
1640                            .await?;
1641
1642                        let oob_notes = if include_invite {
1643                            OOBNotes::new_with_invite(
1644                                notes,
1645                                &self.client_ctx.get_invite_code().await,
1646                            )
1647                        } else {
1648                            OOBNotes::new(federation_id_prefix, notes)
1649                        };
1650
1651                        self.client_ctx
1652                            .add_state_machines_dbtx(
1653                                dbtx,
1654                                self.client_ctx.map_dyn(states).collect(),
1655                            )
1656                            .await?;
1657                        self.client_ctx
1658                            .add_operation_log_entry_dbtx(
1659                                dbtx,
1660                                operation_id,
1661                                MintCommonInit::KIND.as_str(),
1662                                MintOperationMeta {
1663                                    variant: MintOperationMetaVariant::SpendOOB {
1664                                        requested_amount,
1665                                        oob_notes: oob_notes.clone(),
1666                                    },
1667                                    amount: oob_notes.total_amount(),
1668                                    extra_meta,
1669                                },
1670                            )
1671                            .await;
1672                        self.client_ctx
1673                            .log_event(
1674                                dbtx,
1675                                OOBNotesSpent {
1676                                    requested_amount,
1677                                    spent_amount: oob_notes.total_amount(),
1678                                    timeout: try_cancel_after,
1679                                    include_invite,
1680                                },
1681                            )
1682                            .await;
1683
1684                        Ok((operation_id, oob_notes))
1685                    })
1686                },
1687                Some(100),
1688            )
1689            .await
1690            .map_err(|e| match e {
1691                AutocommitError::ClosureError { error, .. } => error,
1692                AutocommitError::CommitFailed { last_error, .. } => {
1693                    anyhow!("Commit to DB failed: {last_error}")
1694                }
1695            })
1696    }
1697
1698    /// Validate the given notes and return the total amount of the notes.
1699    /// Validation checks that:
1700    /// - the federation ID is correct
1701    /// - the note has a valid signature
1702    /// - the spend key is correct.
1703    pub fn validate_notes(&self, oob_notes: &OOBNotes) -> anyhow::Result<Amount> {
1704        let federation_id_prefix = oob_notes.federation_id_prefix();
1705        let notes = oob_notes.notes().clone();
1706
1707        if federation_id_prefix != self.federation_id.to_prefix() {
1708            bail!("Federation ID does not match");
1709        }
1710
1711        let tbs_pks = &self.cfg.tbs_pks;
1712
1713        for (idx, (amt, snote)) in notes.iter_items().enumerate() {
1714            let key = tbs_pks
1715                .get(amt)
1716                .ok_or_else(|| anyhow!("Note {idx} uses an invalid amount tier {amt}"))?;
1717
1718            let note = snote.note();
1719            if !note.verify(*key) {
1720                bail!("Note {idx} has an invalid federation signature");
1721            }
1722
1723            let expected_nonce = Nonce(snote.spend_key.public_key());
1724            if note.nonce != expected_nonce {
1725                bail!("Note {idx} cannot be spent using the supplied spend key");
1726            }
1727        }
1728
1729        Ok(notes.total_amount())
1730    }
1731
1732    /// Contacts the mint and checks if the supplied notes were already spent.
1733    ///
1734    /// **Caution:** This reduces privacy and can lead to race conditions. **DO
1735    /// NOT** rely on it for receiving funds unless you really know what you are
1736    /// doing.
1737    pub async fn check_note_spent(&self, oob_notes: &OOBNotes) -> anyhow::Result<bool> {
1738        use crate::api::MintFederationApi;
1739
1740        let api_client = self.client_ctx.module_api();
1741        let any_spent = try_join_all(oob_notes.notes().iter().flat_map(|(_, notes)| {
1742            notes
1743                .iter()
1744                .map(|note| api_client.check_note_spent(note.nonce()))
1745        }))
1746        .await?
1747        .into_iter()
1748        .any(|spent| spent);
1749
1750        Ok(any_spent)
1751    }
1752
1753    /// Try to cancel a spend operation started with
1754    /// [`MintClientModule::spend_notes_with_selector`]. If the e-cash notes
1755    /// have already been spent this operation will fail which can be
1756    /// observed using [`MintClientModule::subscribe_spend_notes`].
1757    pub async fn try_cancel_spend_notes(&self, operation_id: OperationId) {
1758        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1759        dbtx.insert_entry(&CancelledOOBSpendKey(operation_id), &())
1760            .await;
1761        if let Err(e) = dbtx.commit_tx_result().await {
1762            warn!("We tried to cancel the same OOB spend multiple times concurrently: {e}");
1763        }
1764    }
1765
1766    /// Subscribe to updates on the progress of a raw e-cash spend operation
1767    /// started with [`MintClientModule::spend_notes_with_selector`].
1768    pub async fn subscribe_spend_notes(
1769        &self,
1770        operation_id: OperationId,
1771    ) -> anyhow::Result<UpdateStreamOrOutcome<SpendOOBState>> {
1772        let operation = self.mint_operation(operation_id).await?;
1773        if !matches!(
1774            operation.meta::<MintOperationMeta>().variant,
1775            MintOperationMetaVariant::SpendOOB { .. }
1776        ) {
1777            bail!("Operation is not a out-of-band spend");
1778        }
1779
1780        let client_ctx = self.client_ctx.clone();
1781
1782        Ok(self
1783            .client_ctx
1784            .outcome_or_updates(operation, operation_id, move || {
1785                stream! {
1786                    yield SpendOOBState::Created;
1787
1788                    let self_ref = client_ctx.self_ref();
1789
1790                    let refund = self_ref
1791                        .await_spend_oob_refund(operation_id)
1792                        .await;
1793
1794                    if refund.user_triggered {
1795                        yield SpendOOBState::UserCanceledProcessing;
1796                    }
1797
1798                    let mut success = true;
1799
1800                    for txid in refund.transaction_ids {
1801                        debug!(
1802                            target: LOG_CLIENT_MODULE_MINT,
1803                            %txid,
1804                            operation_id=%operation_id.fmt_short(),
1805                            "Waiting for oob refund txid"
1806                        );
1807                        if client_ctx
1808                            .transaction_updates(operation_id)
1809                            .await
1810                            .await_tx_accepted(txid)
1811                            .await.is_err() {
1812                                success = false;
1813                            }
1814                    }
1815
1816                    debug!(
1817                        target: LOG_CLIENT_MODULE_MINT,
1818                        operation_id=%operation_id.fmt_short(),
1819                        %success,
1820                        "Done waiting for all refund oob txids"
1821                     );
1822
1823                    match (refund.user_triggered, success) {
1824                        (true, true) => {
1825                            yield SpendOOBState::UserCanceledSuccess;
1826                        },
1827                        (true, false) => {
1828                            yield SpendOOBState::UserCanceledFailure;
1829                        },
1830                        (false, true) => {
1831                            yield SpendOOBState::Refunded;
1832                        },
1833                        (false, false) => {
1834                            yield SpendOOBState::Success;
1835                        }
1836                    }
1837                }
1838            }))
1839    }
1840
1841    async fn mint_operation(&self, operation_id: OperationId) -> anyhow::Result<OperationLogEntry> {
1842        let operation = self.client_ctx.get_operation(operation_id).await?;
1843
1844        if operation.operation_module_kind() != MintCommonInit::KIND.as_str() {
1845            bail!("Operation is not a mint operation");
1846        }
1847
1848        Ok(operation)
1849    }
1850
1851    async fn delete_spendable_note(
1852        client_ctx: &ClientContext<MintClientModule>,
1853        dbtx: &mut DatabaseTransaction<'_>,
1854        amount: Amount,
1855        note: &SpendableNote,
1856    ) {
1857        client_ctx
1858            .log_event(
1859                dbtx,
1860                NoteSpent {
1861                    nonce: note.nonce(),
1862                },
1863            )
1864            .await;
1865        dbtx.remove_entry(&NoteKey {
1866            amount,
1867            nonce: note.nonce(),
1868        })
1869        .await
1870        .expect("Must deleted existing spendable note");
1871    }
1872
1873    pub async fn advance_note_idx(&self, amount: Amount) -> anyhow::Result<DerivableSecret> {
1874        let db = self.client_ctx.module_db().clone();
1875
1876        Ok(db
1877            .autocommit(
1878                |dbtx, _| {
1879                    Box::pin(async {
1880                        Ok::<DerivableSecret, anyhow::Error>(
1881                            self.new_note_secret(amount, dbtx).await,
1882                        )
1883                    })
1884                },
1885                None,
1886            )
1887            .await?)
1888    }
1889
1890    /// Returns secrets for the note indices that were reused by previous
1891    /// clients with same client secret.
1892    pub async fn reused_note_secrets(&self) -> Vec<(Amount, NoteIssuanceRequest, BlindNonce)> {
1893        self.client_ctx
1894            .module_db()
1895            .begin_transaction_nc()
1896            .await
1897            .get_value(&ReusedNoteIndices)
1898            .await
1899            .unwrap_or_default()
1900            .into_iter()
1901            .map(|(amount, note_idx)| {
1902                let secret = Self::new_note_secret_static(&self.secret, amount, note_idx);
1903                let (request, blind_nonce) =
1904                    NoteIssuanceRequest::new(fedimint_core::secp256k1::SECP256K1, &secret);
1905                (amount, request, blind_nonce)
1906            })
1907            .collect()
1908    }
1909}
1910
1911pub fn spendable_notes_to_operation_id(
1912    spendable_selected_notes: &TieredMulti<SpendableNote>,
1913) -> OperationId {
1914    OperationId(
1915        spendable_selected_notes
1916            .consensus_hash::<sha256t::Hash<OOBSpendTag>>()
1917            .to_byte_array(),
1918    )
1919}
1920
1921#[derive(Debug, Serialize, Deserialize, Clone)]
1922pub struct SpendOOBRefund {
1923    pub user_triggered: bool,
1924    pub transaction_ids: Vec<TransactionId>,
1925}
1926
1927/// Defines a strategy for selecting e-cash notes given a specific target amount
1928/// and fee per note transaction input.
1929#[apply(async_trait_maybe_send!)]
1930pub trait NotesSelector<Note = SpendableNoteUndecoded>: Send + Sync {
1931    /// Select notes from stream for `requested_amount`.
1932    /// The stream must produce items in non- decreasing order of amount.
1933    async fn select_notes(
1934        &self,
1935        // FIXME: async trait doesn't like maybe_add_send
1936        #[cfg(not(target_family = "wasm"))] stream: impl futures::Stream<Item = (Amount, Note)> + Send,
1937        #[cfg(target_family = "wasm")] stream: impl futures::Stream<Item = (Amount, Note)>,
1938        requested_amount: Amount,
1939        fee_consensus: FeeConsensus,
1940    ) -> anyhow::Result<TieredMulti<Note>>;
1941}
1942
1943/// Select notes with total amount of *at least* `request_amount`. If more than
1944/// requested amount of notes are returned it was because exact change couldn't
1945/// be made, and the next smallest amount will be returned.
1946///
1947/// The caller can request change from the federation.
1948pub struct SelectNotesWithAtleastAmount;
1949
1950#[apply(async_trait_maybe_send!)]
1951impl<Note: Send> NotesSelector<Note> for SelectNotesWithAtleastAmount {
1952    async fn select_notes(
1953        &self,
1954        #[cfg(not(target_family = "wasm"))] stream: impl futures::Stream<Item = (Amount, Note)> + Send,
1955        #[cfg(target_family = "wasm")] stream: impl futures::Stream<Item = (Amount, Note)>,
1956        requested_amount: Amount,
1957        fee_consensus: FeeConsensus,
1958    ) -> anyhow::Result<TieredMulti<Note>> {
1959        Ok(select_notes_from_stream(stream, requested_amount, fee_consensus).await?)
1960    }
1961}
1962
1963/// Select notes with total amount of *exactly* `request_amount`. If the amount
1964/// cannot be represented with the available denominations an error is returned,
1965/// this **does not** mean that the balance is too low.
1966pub struct SelectNotesWithExactAmount;
1967
1968#[apply(async_trait_maybe_send!)]
1969impl<Note: Send> NotesSelector<Note> for SelectNotesWithExactAmount {
1970    async fn select_notes(
1971        &self,
1972        #[cfg(not(target_family = "wasm"))] stream: impl futures::Stream<Item = (Amount, Note)> + Send,
1973        #[cfg(target_family = "wasm")] stream: impl futures::Stream<Item = (Amount, Note)>,
1974        requested_amount: Amount,
1975        fee_consensus: FeeConsensus,
1976    ) -> anyhow::Result<TieredMulti<Note>> {
1977        let notes = select_notes_from_stream(stream, requested_amount, fee_consensus).await?;
1978
1979        if notes.total_amount() != requested_amount {
1980            bail!(
1981                "Could not select notes with exact amount. Requested amount: {}. Selected amount: {}",
1982                requested_amount,
1983                notes.total_amount()
1984            );
1985        }
1986
1987        Ok(notes)
1988    }
1989}
1990
1991// We are using a greedy algorithm to select notes. We start with the largest
1992// then proceed to the lowest tiers/denominations.
1993// But there is a catch: we don't know if there are enough notes in the lowest
1994// tiers, so we need to save a big note in case the sum of the following
1995// small notes are not enough.
1996async fn select_notes_from_stream<Note>(
1997    stream: impl futures::Stream<Item = (Amount, Note)>,
1998    requested_amount: Amount,
1999    fee_consensus: FeeConsensus,
2000) -> Result<TieredMulti<Note>, InsufficientBalanceError> {
2001    if requested_amount == Amount::ZERO {
2002        return Ok(TieredMulti::default());
2003    }
2004    let mut stream = Box::pin(stream);
2005    let mut selected = vec![];
2006    // This is the big note we save in case the sum of the following small notes are
2007    // not sufficient to cover the pending amount
2008    // The tuple is (amount, note, checkpoint), where checkpoint is the index where
2009    // the note should be inserted on the selected vector if it is needed
2010    let mut last_big_note_checkpoint: Option<(Amount, Note, usize)> = None;
2011    let mut pending_amount = requested_amount;
2012    let mut previous_amount: Option<Amount> = None; // used to assert descending order
2013    loop {
2014        if let Some((note_amount, note)) = stream.next().await {
2015            assert!(
2016                previous_amount.is_none_or(|previous| previous >= note_amount),
2017                "notes are not sorted in descending order"
2018            );
2019            previous_amount = Some(note_amount);
2020
2021            if note_amount <= fee_consensus.fee(note_amount) {
2022                continue;
2023            }
2024
2025            match note_amount.cmp(&(pending_amount + fee_consensus.fee(note_amount))) {
2026                Ordering::Less => {
2027                    // keep adding notes until we have enough
2028                    pending_amount += fee_consensus.fee(note_amount);
2029                    pending_amount -= note_amount;
2030                    selected.push((note_amount, note));
2031                }
2032                Ordering::Greater => {
2033                    // probably we don't need this big note, but we'll keep it in case the
2034                    // following small notes don't add up to the
2035                    // requested amount
2036                    last_big_note_checkpoint = Some((note_amount, note, selected.len()));
2037                }
2038                Ordering::Equal => {
2039                    // exactly enough notes, return
2040                    selected.push((note_amount, note));
2041
2042                    let notes: TieredMulti<Note> = selected.into_iter().collect();
2043
2044                    assert!(
2045                        notes.total_amount().msats
2046                            >= requested_amount.msats
2047                                + notes
2048                                    .iter()
2049                                    .map(|note| fee_consensus.fee(note.0))
2050                                    .sum::<Amount>()
2051                                    .msats
2052                    );
2053
2054                    return Ok(notes);
2055                }
2056            }
2057        } else {
2058            assert!(pending_amount > Amount::ZERO);
2059            if let Some((big_note_amount, big_note, checkpoint)) = last_big_note_checkpoint {
2060                // the sum of the small notes don't add up to the pending amount, remove
2061                // them
2062                selected.truncate(checkpoint);
2063                // and use the big note to cover it
2064                selected.push((big_note_amount, big_note));
2065
2066                let notes: TieredMulti<Note> = selected.into_iter().collect();
2067
2068                assert!(
2069                    notes.total_amount().msats
2070                        >= requested_amount.msats
2071                            + notes
2072                                .iter()
2073                                .map(|note| fee_consensus.fee(note.0))
2074                                .sum::<Amount>()
2075                                .msats
2076                );
2077
2078                // so now we have enough to cover the requested amount, return
2079                return Ok(notes);
2080            }
2081
2082            let total_amount = requested_amount.saturating_sub(pending_amount);
2083            // not enough notes, return
2084            return Err(InsufficientBalanceError {
2085                requested_amount,
2086                total_amount,
2087            });
2088        }
2089    }
2090}
2091
2092#[derive(Debug, Clone, Error)]
2093pub struct InsufficientBalanceError {
2094    pub requested_amount: Amount,
2095    pub total_amount: Amount,
2096}
2097
2098impl std::fmt::Display for InsufficientBalanceError {
2099    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2100        write!(
2101            f,
2102            "Insufficient balance: requested {} but only {} available",
2103            self.requested_amount, self.total_amount
2104        )
2105    }
2106}
2107
2108/// Old and no longer used, will be deleted in the future
2109#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
2110enum MintRestoreStates {
2111    #[encodable_default]
2112    Default { variant: u64, bytes: Vec<u8> },
2113}
2114
2115/// Old and no longer used, will be deleted in the future
2116#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
2117pub struct MintRestoreStateMachine {
2118    operation_id: OperationId,
2119    state: MintRestoreStates,
2120}
2121
2122#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
2123pub enum MintClientStateMachines {
2124    Output(MintOutputStateMachine),
2125    Input(MintInputStateMachine),
2126    OOB(MintOOBStateMachine),
2127    // Removed in https://github.com/fedimint/fedimint/pull/4035 , now ignored
2128    Restore(MintRestoreStateMachine),
2129}
2130
2131impl IntoDynInstance for MintClientStateMachines {
2132    type DynType = DynState;
2133
2134    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
2135        DynState::from_typed(instance_id, self)
2136    }
2137}
2138
2139impl State for MintClientStateMachines {
2140    type ModuleContext = MintClientContext;
2141
2142    fn transitions(
2143        &self,
2144        context: &Self::ModuleContext,
2145        global_context: &DynGlobalClientContext,
2146    ) -> Vec<StateTransition<Self>> {
2147        match self {
2148            MintClientStateMachines::Output(issuance_state) => {
2149                sm_enum_variant_translation!(
2150                    issuance_state.transitions(context, global_context),
2151                    MintClientStateMachines::Output
2152                )
2153            }
2154            MintClientStateMachines::Input(redemption_state) => {
2155                sm_enum_variant_translation!(
2156                    redemption_state.transitions(context, global_context),
2157                    MintClientStateMachines::Input
2158                )
2159            }
2160            MintClientStateMachines::OOB(oob_state) => {
2161                sm_enum_variant_translation!(
2162                    oob_state.transitions(context, global_context),
2163                    MintClientStateMachines::OOB
2164                )
2165            }
2166            MintClientStateMachines::Restore(_) => {
2167                sm_enum_variant_translation!(vec![], MintClientStateMachines::Restore)
2168            }
2169        }
2170    }
2171
2172    fn operation_id(&self) -> OperationId {
2173        match self {
2174            MintClientStateMachines::Output(issuance_state) => issuance_state.operation_id(),
2175            MintClientStateMachines::Input(redemption_state) => redemption_state.operation_id(),
2176            MintClientStateMachines::OOB(oob_state) => oob_state.operation_id(),
2177            MintClientStateMachines::Restore(r) => r.operation_id,
2178        }
2179    }
2180}
2181
2182/// A [`Note`] with associated secret key that allows to proof ownership (spend
2183/// it)
2184#[derive(Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, Encodable, Decodable)]
2185pub struct SpendableNote {
2186    pub signature: tbs::Signature,
2187    pub spend_key: Keypair,
2188}
2189
2190impl fmt::Debug for SpendableNote {
2191    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2192        f.debug_struct("SpendableNote")
2193            .field("nonce", &self.nonce())
2194            .field("signature", &self.signature)
2195            .field("spend_key", &self.spend_key)
2196            .finish()
2197    }
2198}
2199impl fmt::Display for SpendableNote {
2200    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2201        self.nonce().fmt(f)
2202    }
2203}
2204
2205impl SpendableNote {
2206    pub fn nonce(&self) -> Nonce {
2207        Nonce(self.spend_key.public_key())
2208    }
2209
2210    fn note(&self) -> Note {
2211        Note {
2212            nonce: self.nonce(),
2213            signature: self.signature,
2214        }
2215    }
2216
2217    pub fn to_undecoded(&self) -> SpendableNoteUndecoded {
2218        SpendableNoteUndecoded {
2219            signature: self
2220                .signature
2221                .consensus_encode_to_vec()
2222                .try_into()
2223                .expect("Encoded size always correct"),
2224            spend_key: self.spend_key,
2225        }
2226    }
2227}
2228
2229/// A version of [`SpendableNote`] that didn't decode the `signature` yet
2230///
2231/// **Note**: signature decoding from raw bytes is faliable, as not all bytes
2232/// are valid signatures. Therefore this type must not be used for external
2233/// data, and should be limited to optimizing reading from internal database.
2234///
2235/// The signature bytes will be validated in [`Self::decode`].
2236///
2237/// Decoding [`tbs::Signature`] is somewhat CPU-intensive (see benches in this
2238/// crate), and when most of the result will be filtered away or completely
2239/// unused, it makes sense to skip/delay decoding.
2240#[derive(Clone, Copy, PartialEq, Eq, Hash, Encodable, Decodable, Serialize)]
2241pub struct SpendableNoteUndecoded {
2242    // Need to keep this in sync with `tbs::Signature`, but there's a test
2243    // verifying they serialize and decode the same.
2244    #[serde(serialize_with = "serdect::array::serialize_hex_lower_or_bin")]
2245    pub signature: [u8; 48],
2246    pub spend_key: Keypair,
2247}
2248
2249impl fmt::Display for SpendableNoteUndecoded {
2250    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2251        self.nonce().fmt(f)
2252    }
2253}
2254
2255impl fmt::Debug for SpendableNoteUndecoded {
2256    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2257        f.debug_struct("SpendableNote")
2258            .field("nonce", &self.nonce())
2259            .field("signature", &"[raw]")
2260            .field("spend_key", &self.spend_key)
2261            .finish()
2262    }
2263}
2264
2265impl SpendableNoteUndecoded {
2266    fn nonce(&self) -> Nonce {
2267        Nonce(self.spend_key.public_key())
2268    }
2269
2270    pub fn decode(self) -> anyhow::Result<SpendableNote> {
2271        Ok(SpendableNote {
2272            signature: Decodable::consensus_decode_partial_from_finite_reader(
2273                &mut self.signature.as_slice(),
2274                &ModuleRegistry::default(),
2275            )?,
2276            spend_key: self.spend_key,
2277        })
2278    }
2279}
2280
2281/// An index used to deterministically derive [`Note`]s
2282///
2283/// We allow converting it to u64 and incrementing it, but
2284/// messing with it should be somewhat restricted to prevent
2285/// silly errors.
2286#[derive(
2287    Copy,
2288    Clone,
2289    Debug,
2290    Serialize,
2291    Deserialize,
2292    PartialEq,
2293    Eq,
2294    Encodable,
2295    Decodable,
2296    Default,
2297    PartialOrd,
2298    Ord,
2299)]
2300pub struct NoteIndex(u64);
2301
2302impl NoteIndex {
2303    pub fn next(self) -> Self {
2304        Self(self.0 + 1)
2305    }
2306
2307    fn prev(self) -> Option<Self> {
2308        self.0.checked_sub(0).map(Self)
2309    }
2310
2311    pub fn as_u64(self) -> u64 {
2312        self.0
2313    }
2314
2315    // Private. If it turns out it is useful outside,
2316    // we can relax and convert to `From<u64>`
2317    // Actually used in tests RN, so cargo complains in non-test builds.
2318    #[allow(unused)]
2319    pub fn from_u64(v: u64) -> Self {
2320        Self(v)
2321    }
2322
2323    pub fn advance(&mut self) {
2324        *self = self.next();
2325    }
2326}
2327
2328impl std::fmt::Display for NoteIndex {
2329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2330        self.0.fmt(f)
2331    }
2332}
2333
2334struct OOBSpendTag;
2335
2336impl sha256t::Tag for OOBSpendTag {
2337    fn engine() -> sha256::HashEngine {
2338        let mut engine = sha256::HashEngine::default();
2339        engine.input(b"oob-spend");
2340        engine
2341    }
2342}
2343
2344struct OOBReissueTag;
2345
2346impl sha256t::Tag for OOBReissueTag {
2347    fn engine() -> sha256::HashEngine {
2348        let mut engine = sha256::HashEngine::default();
2349        engine.input(b"oob-reissue");
2350        engine
2351    }
2352}
2353
2354/// Determines the denominations to use when representing an amount
2355///
2356/// Algorithm tries to leave the user with a target number of
2357/// `denomination_sets` starting at the lowest denomination.  `self`
2358/// gives the denominations that the user already has.
2359pub fn represent_amount<K>(
2360    amount: Amount,
2361    current_denominations: &TieredCounts,
2362    tiers: &Tiered<K>,
2363    denomination_sets: u16,
2364    fee_consensus: &FeeConsensus,
2365) -> TieredCounts {
2366    let mut remaining_amount = amount;
2367    let mut denominations = TieredCounts::default();
2368
2369    // try to hit the target `denomination_sets`
2370    for tier in tiers.tiers() {
2371        let notes = current_denominations.get(*tier);
2372        let missing_notes = u64::from(denomination_sets).saturating_sub(notes as u64);
2373        let possible_notes = remaining_amount / (*tier + fee_consensus.fee(*tier));
2374
2375        let add_notes = min(possible_notes, missing_notes);
2376        denominations.inc(*tier, add_notes as usize);
2377        remaining_amount -= (*tier + fee_consensus.fee(*tier)) * add_notes;
2378    }
2379
2380    // if there is a remaining amount, add denominations with a greedy algorithm
2381    for tier in tiers.tiers().rev() {
2382        let res = remaining_amount / (*tier + fee_consensus.fee(*tier));
2383        remaining_amount -= (*tier + fee_consensus.fee(*tier)) * res;
2384        denominations.inc(*tier, res as usize);
2385    }
2386
2387    let represented: u64 = denominations
2388        .iter()
2389        .map(|(k, v)| (k + fee_consensus.fee(k)).msats * (v as u64))
2390        .sum();
2391
2392    assert!(represented <= amount.msats);
2393    assert!(represented + fee_consensus.fee(Amount::from_msats(1)).msats >= amount.msats);
2394
2395    denominations
2396}
2397
2398pub(crate) fn create_bundle_for_inputs(
2399    inputs_and_notes: Vec<(ClientInput<MintInput>, SpendableNote)>,
2400    operation_id: OperationId,
2401) -> ClientInputBundle<MintInput, MintClientStateMachines> {
2402    let mut inputs = Vec::new();
2403    let mut input_states = Vec::new();
2404
2405    for (input, spendable_note) in inputs_and_notes {
2406        input_states.push((input.amount, spendable_note));
2407        inputs.push(input);
2408    }
2409
2410    let input_sm = Arc::new(move |out_point_range: OutPointRange| {
2411        debug_assert_eq!(out_point_range.into_iter().count(), input_states.len());
2412
2413        vec![MintClientStateMachines::Input(MintInputStateMachine {
2414            common: MintInputCommon {
2415                operation_id,
2416                out_point_range,
2417            },
2418            state: MintInputStates::CreatedBundle(MintInputStateCreatedBundle {
2419                notes: input_states.clone(),
2420            }),
2421        })]
2422    });
2423
2424    ClientInputBundle::new(
2425        inputs,
2426        vec![ClientInputSM {
2427            state_machines: input_sm,
2428        }],
2429    )
2430}
2431
2432#[cfg(test)]
2433mod tests {
2434    use std::fmt::Display;
2435    use std::str::FromStr;
2436
2437    use bitcoin_hashes::Hash;
2438    use fedimint_core::config::FederationId;
2439    use fedimint_core::encoding::Decodable;
2440    use fedimint_core::invite_code::InviteCode;
2441    use fedimint_core::module::registry::ModuleRegistry;
2442    use fedimint_core::{
2443        Amount, OutPoint, PeerId, Tiered, TieredCounts, TieredMulti, TransactionId,
2444    };
2445    use fedimint_mint_common::config::FeeConsensus;
2446    use itertools::Itertools;
2447    use serde_json::json;
2448
2449    use crate::{
2450        MintOperationMetaVariant, OOBNotes, OOBNotesPart, SpendableNote, SpendableNoteUndecoded,
2451        represent_amount, select_notes_from_stream,
2452    };
2453
2454    #[test]
2455    fn represent_amount_targets_denomination_sets() {
2456        fn tiers(tiers: Vec<u64>) -> Tiered<()> {
2457            tiers
2458                .into_iter()
2459                .map(|tier| (Amount::from_sats(tier), ()))
2460                .collect()
2461        }
2462
2463        fn denominations(denominations: Vec<(Amount, usize)>) -> TieredCounts {
2464            TieredCounts::from_iter(denominations)
2465        }
2466
2467        let starting = notes(vec![
2468            (Amount::from_sats(1), 1),
2469            (Amount::from_sats(2), 3),
2470            (Amount::from_sats(3), 2),
2471        ])
2472        .summary();
2473        let tiers = tiers(vec![1, 2, 3, 4]);
2474
2475        // target 3 tiers will fill out the 1 and 3 denominations
2476        assert_eq!(
2477            represent_amount(
2478                Amount::from_sats(6),
2479                &starting,
2480                &tiers,
2481                3,
2482                &FeeConsensus::zero()
2483            ),
2484            denominations(vec![(Amount::from_sats(1), 3), (Amount::from_sats(3), 1),])
2485        );
2486
2487        // target 2 tiers will fill out the 1 and 4 denominations
2488        assert_eq!(
2489            represent_amount(
2490                Amount::from_sats(6),
2491                &starting,
2492                &tiers,
2493                2,
2494                &FeeConsensus::zero()
2495            ),
2496            denominations(vec![(Amount::from_sats(1), 2), (Amount::from_sats(4), 1)])
2497        );
2498    }
2499
2500    #[test_log::test(tokio::test)]
2501    async fn select_notes_avg_test() {
2502        let max_amount = Amount::from_sats(1_000_000);
2503        let tiers = Tiered::gen_denominations(2, max_amount);
2504        let tiered = represent_amount::<()>(
2505            max_amount,
2506            &TieredCounts::default(),
2507            &tiers,
2508            3,
2509            &FeeConsensus::zero(),
2510        );
2511
2512        let mut total_notes = 0;
2513        for multiplier in 1..100 {
2514            let stream = reverse_sorted_note_stream(tiered.iter().collect());
2515            let select = select_notes_from_stream(
2516                stream,
2517                Amount::from_sats(multiplier * 1000),
2518                FeeConsensus::zero(),
2519            )
2520            .await;
2521            total_notes += select.unwrap().into_iter_items().count();
2522        }
2523        assert_eq!(total_notes / 100, 10);
2524    }
2525
2526    #[test_log::test(tokio::test)]
2527    async fn select_notes_returns_exact_amount_with_minimum_notes() {
2528        let f = || {
2529            reverse_sorted_note_stream(vec![
2530                (Amount::from_sats(1), 10),
2531                (Amount::from_sats(5), 10),
2532                (Amount::from_sats(20), 10),
2533            ])
2534        };
2535        assert_eq!(
2536            select_notes_from_stream(f(), Amount::from_sats(7), FeeConsensus::zero())
2537                .await
2538                .unwrap(),
2539            notes(vec![(Amount::from_sats(1), 2), (Amount::from_sats(5), 1)])
2540        );
2541        assert_eq!(
2542            select_notes_from_stream(f(), Amount::from_sats(20), FeeConsensus::zero())
2543                .await
2544                .unwrap(),
2545            notes(vec![(Amount::from_sats(20), 1)])
2546        );
2547    }
2548
2549    #[test_log::test(tokio::test)]
2550    async fn select_notes_returns_next_smallest_amount_if_exact_change_cannot_be_made() {
2551        let stream = reverse_sorted_note_stream(vec![
2552            (Amount::from_sats(1), 1),
2553            (Amount::from_sats(5), 5),
2554            (Amount::from_sats(20), 5),
2555        ]);
2556        assert_eq!(
2557            select_notes_from_stream(stream, Amount::from_sats(7), FeeConsensus::zero())
2558                .await
2559                .unwrap(),
2560            notes(vec![(Amount::from_sats(5), 2)])
2561        );
2562    }
2563
2564    #[test_log::test(tokio::test)]
2565    async fn select_notes_uses_big_note_if_small_amounts_are_not_sufficient() {
2566        let stream = reverse_sorted_note_stream(vec![
2567            (Amount::from_sats(1), 3),
2568            (Amount::from_sats(5), 3),
2569            (Amount::from_sats(20), 2),
2570        ]);
2571        assert_eq!(
2572            select_notes_from_stream(stream, Amount::from_sats(39), FeeConsensus::zero())
2573                .await
2574                .unwrap(),
2575            notes(vec![(Amount::from_sats(20), 2)])
2576        );
2577    }
2578
2579    #[test_log::test(tokio::test)]
2580    async fn select_notes_returns_error_if_amount_is_too_large() {
2581        let stream = reverse_sorted_note_stream(vec![(Amount::from_sats(10), 1)]);
2582        let error = select_notes_from_stream(stream, Amount::from_sats(100), FeeConsensus::zero())
2583            .await
2584            .unwrap_err();
2585        assert_eq!(error.total_amount, Amount::from_sats(10));
2586    }
2587
2588    fn reverse_sorted_note_stream(
2589        notes: Vec<(Amount, usize)>,
2590    ) -> impl futures::Stream<Item = (Amount, String)> {
2591        futures::stream::iter(
2592            notes
2593                .into_iter()
2594                // We are creating `number` dummy notes of `amount` value
2595                .flat_map(|(amount, number)| vec![(amount, "dummy note".into()); number])
2596                .sorted()
2597                .rev(),
2598        )
2599    }
2600
2601    fn notes(notes: Vec<(Amount, usize)>) -> TieredMulti<String> {
2602        notes
2603            .into_iter()
2604            .flat_map(|(amount, number)| vec![(amount, "dummy note".into()); number])
2605            .collect()
2606    }
2607
2608    #[test]
2609    fn decoding_empty_oob_notes_fails() {
2610        let empty_oob_notes =
2611            OOBNotes::new(FederationId::dummy().to_prefix(), TieredMulti::default());
2612        let oob_notes_string = empty_oob_notes.to_string();
2613
2614        let res = oob_notes_string.parse::<OOBNotes>();
2615
2616        assert!(res.is_err(), "An empty OOB notes string should not parse");
2617    }
2618
2619    fn test_roundtrip_serialize_str<T, F>(data: T, assertions: F)
2620    where
2621        T: FromStr + Display,
2622        <T as FromStr>::Err: std::fmt::Debug,
2623        F: Fn(T),
2624    {
2625        let data_str = data.to_string();
2626        assertions(data);
2627        let data_parsed = data_str.parse().expect("Deserialization failed");
2628        assertions(data_parsed);
2629    }
2630
2631    #[test]
2632    fn notes_encode_decode() {
2633        let federation_id_1 =
2634            FederationId(bitcoin_hashes::sha256::Hash::from_byte_array([0x21; 32]));
2635        let federation_id_prefix_1 = federation_id_1.to_prefix();
2636        let federation_id_2 =
2637            FederationId(bitcoin_hashes::sha256::Hash::from_byte_array([0x42; 32]));
2638        let federation_id_prefix_2 = federation_id_2.to_prefix();
2639
2640        let notes = vec![(
2641            Amount::from_sats(1),
2642            SpendableNote::consensus_decode_hex("a5dd3ebacad1bc48bd8718eed5a8da1d68f91323bef2848ac4fa2e6f8eed710f3178fd4aef047cc234e6b1127086f33cc408b39818781d9521475360de6b205f3328e490a6d99d5e2553a4553207c8bd", &ModuleRegistry::default()).unwrap(),
2643        )]
2644        .into_iter()
2645        .collect::<TieredMulti<_>>();
2646
2647        // Can decode inviteless notes
2648        let notes_no_invite = OOBNotes::new(federation_id_prefix_1, notes.clone());
2649        test_roundtrip_serialize_str(notes_no_invite, |oob_notes| {
2650            assert_eq!(oob_notes.notes(), &notes);
2651            assert_eq!(oob_notes.federation_id_prefix(), federation_id_prefix_1);
2652            assert_eq!(oob_notes.federation_invite(), None);
2653        });
2654
2655        // Can decode notes with invite
2656        let invite = InviteCode::new(
2657            "wss://foo.bar".parse().unwrap(),
2658            PeerId::from(0),
2659            federation_id_1,
2660            None,
2661        );
2662        let notes_invite = OOBNotes::new_with_invite(notes.clone(), &invite);
2663        test_roundtrip_serialize_str(notes_invite, |oob_notes| {
2664            assert_eq!(oob_notes.notes(), &notes);
2665            assert_eq!(oob_notes.federation_id_prefix(), federation_id_prefix_1);
2666            assert_eq!(oob_notes.federation_invite(), Some(invite.clone()));
2667        });
2668
2669        // Can decode notes without federation id prefix, so we can optionally remove it
2670        // in the future
2671        let notes_no_prefix = OOBNotes(vec![
2672            OOBNotesPart::Notes(notes.clone()),
2673            OOBNotesPart::Invite {
2674                peer_apis: vec![(PeerId::from(0), "wss://foo.bar".parse().unwrap())],
2675                federation_id: federation_id_1,
2676            },
2677        ]);
2678        test_roundtrip_serialize_str(notes_no_prefix, |oob_notes| {
2679            assert_eq!(oob_notes.notes(), &notes);
2680            assert_eq!(oob_notes.federation_id_prefix(), federation_id_prefix_1);
2681        });
2682
2683        // Rejects notes with inconsistent federation id
2684        let notes_inconsistent = OOBNotes(vec![
2685            OOBNotesPart::Notes(notes),
2686            OOBNotesPart::Invite {
2687                peer_apis: vec![(PeerId::from(0), "wss://foo.bar".parse().unwrap())],
2688                federation_id: federation_id_1,
2689            },
2690            OOBNotesPart::FederationIdPrefix(federation_id_prefix_2),
2691        ]);
2692        let notes_inconsistent_str = notes_inconsistent.to_string();
2693        assert!(notes_inconsistent_str.parse::<OOBNotes>().is_err());
2694    }
2695
2696    #[test]
2697    fn spendable_note_undecoded_sanity() {
2698        // TODO: add more hex dumps to the loop
2699        #[allow(clippy::single_element_loop)]
2700        for note_hex in [
2701            "a5dd3ebacad1bc48bd8718eed5a8da1d68f91323bef2848ac4fa2e6f8eed710f3178fd4aef047cc234e6b1127086f33cc408b39818781d9521475360de6b205f3328e490a6d99d5e2553a4553207c8bd",
2702        ] {
2703            let note =
2704                SpendableNote::consensus_decode_hex(note_hex, &ModuleRegistry::default()).unwrap();
2705            let note_undecoded =
2706                SpendableNoteUndecoded::consensus_decode_hex(note_hex, &ModuleRegistry::default())
2707                    .unwrap()
2708                    .decode()
2709                    .unwrap();
2710            assert_eq!(note, note_undecoded,);
2711            assert_eq!(
2712                serde_json::to_string(&note).unwrap(),
2713                serde_json::to_string(&note_undecoded).unwrap(),
2714            );
2715        }
2716    }
2717
2718    #[test]
2719    fn reissuance_meta_compatibility_02_03() {
2720        let dummy_outpoint = OutPoint {
2721            txid: TransactionId::all_zeros(),
2722            out_idx: 0,
2723        };
2724
2725        let old_meta_json = json!({
2726            "reissuance": {
2727                "out_point": dummy_outpoint
2728            }
2729        });
2730
2731        let old_meta: MintOperationMetaVariant =
2732            serde_json::from_value(old_meta_json).expect("parsing old reissuance meta failed");
2733        assert_eq!(
2734            old_meta,
2735            MintOperationMetaVariant::Reissuance {
2736                legacy_out_point: Some(dummy_outpoint),
2737                txid: None,
2738                out_point_indices: vec![],
2739            }
2740        );
2741
2742        let new_meta_json = serde_json::to_value(MintOperationMetaVariant::Reissuance {
2743            legacy_out_point: None,
2744            txid: Some(dummy_outpoint.txid),
2745            out_point_indices: vec![0],
2746        })
2747        .expect("serializing always works");
2748        assert_eq!(
2749            new_meta_json,
2750            json!({
2751                "reissuance": {
2752                    "txid": dummy_outpoint.txid,
2753                    "out_point_indices": [dummy_outpoint.out_idx],
2754                }
2755            })
2756        );
2757    }
2758}