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