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
9pub mod backup;
11#[cfg(feature = "cli")]
13mod cli;
14pub mod client_db;
16pub mod error;
18mod input;
20mod oob;
22pub mod output;
24
25pub mod events;
26
27pub mod api;
29
30pub mod repair_wallet;
31
32pub mod visualize;
33
34use std::cmp::{Ordering, min};
35use std::collections::{BTreeMap, BTreeSet};
36use std::fmt;
37use std::fmt::{Display, Formatter};
38use std::io::Read;
39use std::str::FromStr;
40use std::sync::{Arc, RwLock};
41use std::time::Duration;
42
43use api::MintFederationApi;
44use async_stream::{stream, try_stream};
45use backup::recovery::{MintRecovery, RecoveryStateV2};
46use base64::Engine as _;
47use bitcoin_hashes::{Hash, HashEngine as BitcoinHashEngine, sha256, sha256t};
48use client_db::{
49 DbKeyPrefix, NoteKeyPrefix, RecoveryFinalizedKey, RecoveryStateKey, RecoveryStateV2Key,
50 ReusedNoteIndices, migrate_state_to_v2, migrate_to_v1,
51};
52use events::{NoteSpent, OOBNotesReissued, OOBNotesSpent, ReceivePaymentEvent, SendPaymentEvent};
53use fedimint_api_client::api::{DynModuleApi, FederationError, FederationResult};
54use fedimint_client_module::db::{ClientModuleMigrationFn, migrate_state};
55pub use fedimint_client_module::error::InsufficientBalanceError;
56use fedimint_client_module::error::{
57 AddStateMachinesError, ClientModuleError, OperationLookupError, TransactionSubmitError,
58};
59use fedimint_client_module::module::init::{
60 ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs, RecoveryMode,
61};
62use fedimint_client_module::module::recovery::RecoveryProgress;
63use fedimint_client_module::module::{
64 ClientContext, ClientModule, IClientModule, OutPointRange, PrimaryModulePriority,
65 PrimaryModuleSupport,
66};
67use fedimint_client_module::oplog::{OperationLogEntry, UpdateStreamOrOutcome};
68use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
69use fedimint_client_module::transaction::{
70 ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
71 ClientOutputSM, FeeQuote, FeeQuoteRequest, TransactionBuilder,
72};
73use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
74use fedimint_core::base32::{FEDIMINT_PREFIX, encode_prefixed};
75use fedimint_core::config::{FederationId, FederationIdPrefix};
76use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
77use fedimint_core::db::{
78 AutocommitError, Database, DatabaseTransaction, DatabaseVersion,
79 IDatabaseTransactionOpsCoreTyped,
80};
81use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
82use fedimint_core::invite_code::InviteCode;
83use fedimint_core::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
84use fedimint_core::module::{
85 AmountUnit, Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
86};
87use fedimint_core::secp256k1::rand::prelude::IteratorRandom;
88use fedimint_core::secp256k1::rand::thread_rng;
89use fedimint_core::secp256k1::{All, Keypair, Secp256k1};
90use fedimint_core::util::{BoxFuture, BoxStream, FmtCompact as _, NextOrPending, SafeUrl};
91use fedimint_core::{
92 Amount, IdxRange, OutPoint, PeerId, Tiered, TieredCounts, TieredMulti, TransactionId, apply,
93 async_trait_maybe_send, base32, push_db_pair_items, runtime,
94};
95use fedimint_derive_secret::{ChildId, DerivableSecret};
96use fedimint_logging::LOG_CLIENT_MODULE_MINT;
97pub use fedimint_mint_common as common;
98use fedimint_mint_common::config::{FeeConsensus, MintClientConfig};
99pub use fedimint_mint_common::*;
100use futures::future::try_join_all;
101use futures::{StreamExt, TryStreamExt as _, pin_mut};
102use hex::ToHex;
103use input::MintInputStateCreatedBundle;
104use itertools::Itertools as _;
105use output::MintOutputStatesCreatedMulti;
106use serde::{Deserialize, Serialize};
107use strum::IntoEnumIterator;
108use tbs::AggregatePublicKey;
109use tracing::{debug, warn};
110
111use crate::backup::EcashBackup;
112use crate::client_db::{
113 CancelledOOBSpendKey, CancelledOOBSpendKeyPrefix, NextECashNoteIndexKey,
114 NextECashNoteIndexKeyPrefix, NoteKey,
115};
116pub use crate::error::{
117 AwaitOutputFinalizedError, FetchRecoverySliceError, OOBNotesParseError,
118 PrepareEcashBackupError, RepairWalletError, SelectNotesError, SendOOBNotesError, SpendOOBError,
119 SubscribeReissueExternalNotesError, SubscribeSpendNotesError, ValidateNotesError,
120 VerifyBlindShareError,
121};
122use crate::input::{MintInputCommon, MintInputStateMachine, MintInputStates};
123use crate::oob::{MintOOBStateMachine, MintOOBStates, MintOOBStatesCreatedMulti};
124use crate::output::{
125 MintOutputCommon, MintOutputStateMachine, MintOutputStates, NoteIssuanceRequest,
126};
127
128const MINT_E_CASH_TYPE_CHILD_ID: ChildId = ChildId(0);
129
130const OOB_SPEND_NO_TIMEOUT: Duration = Duration::MAX;
131
132#[derive(Clone)]
133struct PeerSelector {
134 latency: Arc<RwLock<BTreeMap<PeerId, Duration>>>,
135}
136
137impl PeerSelector {
138 fn new(peers: BTreeSet<PeerId>) -> Self {
139 let latency = peers
140 .into_iter()
141 .map(|peer| (peer, Duration::ZERO))
142 .collect();
143
144 Self {
145 latency: Arc::new(RwLock::new(latency)),
146 }
147 }
148
149 fn choose_peer(&self) -> PeerId {
150 let latency = self.latency.read().expect("poisoned");
151
152 let peer_a = latency.iter().choose(&mut thread_rng()).expect("no peers");
153 let peer_b = latency.iter().choose(&mut thread_rng()).expect("no peers");
154
155 if peer_a.1 <= peer_b.1 {
156 *peer_a.0
157 } else {
158 *peer_b.0
159 }
160 }
161
162 fn report(&self, peer: PeerId, duration: Duration) {
163 self.latency
164 .write()
165 .expect("poisoned")
166 .entry(peer)
167 .and_modify(|latency| *latency = *latency * 9 / 10 + duration / 10)
168 .or_insert(duration);
169 }
170
171 fn remove(&self, peer: PeerId) {
172 self.latency.write().expect("poisoned").remove(&peer);
173 }
174}
175
176async fn download_slice_with_hash(
178 module_api: DynModuleApi,
179 peer_selector: PeerSelector,
180 start: u64,
181 end: u64,
182 expected_hash: sha256::Hash,
183) -> Vec<RecoveryItem> {
184 const TIMEOUT: Duration = Duration::from_secs(30);
185
186 loop {
187 let peer = peer_selector.choose_peer();
188 let start_time = fedimint_core::time::now();
189
190 match runtime::timeout(TIMEOUT, module_api.fetch_recovery_slice(peer, start, end)).await {
191 Ok(Ok(data)) => {
192 let elapsed = fedimint_core::time::now()
193 .duration_since(start_time)
194 .unwrap_or(Duration::ZERO);
195
196 peer_selector.report(peer, elapsed);
197
198 if data.consensus_hash::<sha256::Hash>() == expected_hash {
199 return data;
200 }
201
202 peer_selector.remove(peer);
203 }
204 Ok(Err(..)) | Err(..) => {
205 peer_selector.report(peer, TIMEOUT);
206 }
207 }
208 }
209}
210
211#[derive(Clone, Debug, Encodable, PartialEq, Eq)]
219pub struct OOBNotes(Vec<OOBNotesPart>);
220
221#[derive(Clone, Debug, Decodable, Encodable, PartialEq, Eq)]
224enum OOBNotesPart {
225 Notes(TieredMulti<SpendableNote>),
226 FederationIdPrefix(FederationIdPrefix),
227 Invite {
231 peer_apis: Vec<(PeerId, SafeUrl)>,
233 federation_id: FederationId,
234 },
235 ApiSecret(String),
236 #[encodable_default]
237 Default {
238 variant: u64,
239 bytes: Vec<u8>,
240 },
241}
242
243impl OOBNotes {
244 pub fn new(
245 federation_id_prefix: FederationIdPrefix,
246 notes: TieredMulti<SpendableNote>,
247 ) -> Self {
248 Self(vec![
249 OOBNotesPart::FederationIdPrefix(federation_id_prefix),
250 OOBNotesPart::Notes(notes),
251 ])
252 }
253
254 pub fn new_with_invite(notes: TieredMulti<SpendableNote>, invite: &InviteCode) -> Self {
255 let mut data = vec![
256 OOBNotesPart::FederationIdPrefix(invite.federation_id().to_prefix()),
259 OOBNotesPart::Notes(notes),
260 OOBNotesPart::Invite {
261 peer_apis: vec![(invite.peer(), invite.url())],
262 federation_id: invite.federation_id(),
263 },
264 ];
265 if let Some(api_secret) = invite.api_secret() {
266 data.push(OOBNotesPart::ApiSecret(api_secret));
267 }
268 Self(data)
269 }
270
271 pub fn federation_id_prefix(&self) -> FederationIdPrefix {
272 self.0
273 .iter()
274 .find_map(|data| match data {
275 OOBNotesPart::FederationIdPrefix(prefix) => Some(*prefix),
276 OOBNotesPart::Invite { federation_id, .. } => Some(federation_id.to_prefix()),
277 _ => None,
278 })
279 .expect("Invariant violated: OOBNotes does not contain a FederationIdPrefix")
280 }
281
282 pub fn notes(&self) -> &TieredMulti<SpendableNote> {
283 self.0
284 .iter()
285 .find_map(|data| match data {
286 OOBNotesPart::Notes(notes) => Some(notes),
287 _ => None,
288 })
289 .expect("Invariant violated: OOBNotes does not contain any notes")
290 }
291
292 pub fn notes_json(&self) -> Result<serde_json::Value, serde_json::Error> {
293 let mut notes_map = serde_json::Map::new();
294 for notes in &self.0 {
295 match notes {
296 OOBNotesPart::Notes(notes) => {
297 let notes_json: serde_json::Map<String, serde_json::Value> = notes
298 .iter()
299 .map(|(amount, notes_vec)| {
300 let notes_with_nonce: Vec<serde_json::Value> = notes_vec
301 .iter()
302 .map(|note| {
303 serde_json::json!({
304 "signature": note.signature,
305 "spend_key": note.spend_key,
306 "nonce": note.nonce(),
307 })
308 })
309 .collect();
310 (
311 amount.msats.to_string(),
312 serde_json::Value::Array(notes_with_nonce),
313 )
314 })
315 .collect();
316 notes_map.insert("notes".to_string(), serde_json::Value::Object(notes_json));
317 }
318 OOBNotesPart::FederationIdPrefix(prefix) => {
319 notes_map.insert(
320 "federation_id_prefix".to_string(),
321 serde_json::to_value(prefix.to_string())?,
322 );
323 }
324 OOBNotesPart::Invite {
325 peer_apis,
326 federation_id,
327 } => {
328 let (peer_id, api) = peer_apis
329 .first()
330 .cloned()
331 .expect("Decoding makes sure peer_apis isn't empty");
332 notes_map.insert(
333 "invite".to_string(),
334 serde_json::to_value(InviteCode::new(
335 api,
336 peer_id,
337 *federation_id,
338 self.api_secret(),
339 ))?,
340 );
341 }
342 OOBNotesPart::ApiSecret(_) => { }
343 OOBNotesPart::Default { variant, bytes } => {
344 notes_map.insert(
345 format!("default_{variant}"),
346 serde_json::to_value(bytes.encode_hex::<String>())?,
347 );
348 }
349 }
350 }
351 Ok(serde_json::Value::Object(notes_map))
352 }
353
354 pub fn federation_invite(&self) -> Option<InviteCode> {
355 self.0.iter().find_map(|data| {
356 let OOBNotesPart::Invite {
357 peer_apis,
358 federation_id,
359 } = data
360 else {
361 return None;
362 };
363 let (peer_id, api) = peer_apis
364 .first()
365 .cloned()
366 .expect("Decoding makes sure peer_apis isn't empty");
367 Some(InviteCode::new(
368 api,
369 peer_id,
370 *federation_id,
371 self.api_secret(),
372 ))
373 })
374 }
375
376 fn api_secret(&self) -> Option<String> {
377 self.0.iter().find_map(|data| {
378 let OOBNotesPart::ApiSecret(api_secret) = data else {
379 return None;
380 };
381 Some(api_secret.clone())
382 })
383 }
384}
385
386impl Decodable for OOBNotes {
387 fn consensus_decode_partial<R: Read>(
388 r: &mut R,
389 _modules: &ModuleDecoderRegistry,
390 ) -> Result<Self, DecodeError> {
391 let inner =
392 Vec::<OOBNotesPart>::consensus_decode_partial(r, &ModuleDecoderRegistry::default())?;
393
394 if !inner
396 .iter()
397 .any(|data| matches!(data, OOBNotesPart::Notes(_)))
398 {
399 return Err(DecodeError::from_str(
400 "No e-cash notes were found in OOBNotes data",
401 ));
402 }
403
404 let maybe_federation_id_prefix = inner.iter().find_map(|data| match data {
405 OOBNotesPart::FederationIdPrefix(prefix) => Some(*prefix),
406 _ => None,
407 });
408
409 let maybe_invite = inner.iter().find_map(|data| match data {
410 OOBNotesPart::Invite {
411 federation_id,
412 peer_apis,
413 } => Some((federation_id, peer_apis)),
414 _ => None,
415 });
416
417 match (maybe_federation_id_prefix, maybe_invite) {
418 (Some(p), Some((ip, _))) => {
419 if p != ip.to_prefix() {
420 return Err(DecodeError::from_str(
421 "Inconsistent Federation ID provided in OOBNotes data",
422 ));
423 }
424 }
425 (None, None) => {
426 return Err(DecodeError::from_str(
427 "No Federation ID provided in OOBNotes data",
428 ));
429 }
430 _ => {}
431 }
432
433 if let Some((_, invite)) = maybe_invite
434 && invite.is_empty()
435 {
436 return Err(DecodeError::from_str("Invite didn't contain API endpoints"));
437 }
438
439 Ok(OOBNotes(inner))
440 }
441}
442
443const BASE64_URL_SAFE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
444 &base64::alphabet::URL_SAFE,
445 base64::engine::general_purpose::PAD,
446);
447
448impl FromStr for OOBNotes {
449 type Err = OOBNotesParseError;
450
451 fn from_str(s: &str) -> Result<Self, Self::Err> {
453 let s: String = s.chars().filter(|&c| !c.is_whitespace()).collect();
454
455 let oob_notes_bytes = if let Ok(oob_notes_bytes) =
456 base32::decode_prefixed_bytes(FEDIMINT_PREFIX, &s)
457 {
458 oob_notes_bytes
459 } else if let Ok(oob_notes_bytes) = BASE64_URL_SAFE.decode(&s) {
460 oob_notes_bytes
461 } else if let Ok(oob_notes_bytes) = base64::engine::general_purpose::STANDARD.decode(&s) {
462 oob_notes_bytes
463 } else {
464 return Err(OOBNotesParseError::Encoding);
465 };
466
467 let oob_notes =
468 OOBNotes::consensus_decode_whole(&oob_notes_bytes, &ModuleDecoderRegistry::default())?;
469
470 if oob_notes.notes().is_empty() {
471 return Err(OOBNotesParseError::Empty);
472 }
473
474 Ok(oob_notes)
475 }
476}
477
478impl Display for OOBNotes {
479 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
480 let bytes = Encodable::consensus_encode_to_vec(self);
481
482 f.write_str(&BASE64_URL_SAFE.encode(&bytes))
483 }
484}
485
486impl Serialize for OOBNotes {
487 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
488 where
489 S: serde::Serializer,
490 {
491 serializer.serialize_str(&self.to_string())
492 }
493}
494
495impl<'de> Deserialize<'de> for OOBNotes {
496 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
497 where
498 D: serde::Deserializer<'de>,
499 {
500 let s = String::deserialize(deserializer)?;
501 FromStr::from_str(&s).map_err(serde::de::Error::custom)
502 }
503}
504
505impl OOBNotes {
506 pub fn total_amount(&self) -> Amount {
508 self.notes().total_amount()
509 }
510}
511
512#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
515pub enum ReissueExternalNotesState {
516 Created,
519 Issuing,
522 Done,
524 Failed(String),
526}
527
528pub type SubscribeReissueExternalNotesResult =
530 Result<UpdateStreamOrOutcome<ReissueExternalNotesState>, SubscribeReissueExternalNotesError>;
531
532#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
535pub enum SpendOOBState {
536 Created,
538 UserCanceledProcessing,
541 UserCanceledSuccess,
544 UserCanceledFailure,
547 Success,
551 Refunded,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct MintOperationMeta {
559 pub variant: MintOperationMetaVariant,
560 pub amount: Amount,
561 pub extra_meta: serde_json::Value,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
565#[serde(rename_all = "snake_case")]
566pub enum MintOperationMetaVariant {
567 Reissuance {
571 #[serde(skip_serializing, default, rename = "out_point")]
573 legacy_out_point: Option<OutPoint>,
574 #[serde(default)]
576 txid: Option<TransactionId>,
577 #[serde(default)]
579 out_point_indices: Vec<u64>,
580 },
581 SpendOOB {
582 requested_amount: Amount,
583 oob_notes: OOBNotes,
584 #[serde(default)]
585 no_timeout: bool,
586 },
587}
588
589#[derive(Debug, Clone)]
590pub struct MintClientInit;
591
592const SLICE_SIZE: u64 = 10000;
593const PARALLEL_HASH_REQUESTS: usize = 10;
594const PARALLEL_SLICE_REQUESTS: usize = 10;
595
596impl MintClientInit {
597 #[allow(clippy::too_many_lines)]
598 async fn recover_from_slices(
599 &self,
600 args: &ClientModuleRecoverArgs<Self>,
601 ) -> Result<Option<Amount>, RecoverFromSlicesError> {
602 let mut state = if let Some(state) = args
604 .db()
605 .begin_transaction_nc()
606 .await
607 .get_value(&RecoveryStateV2Key)
608 .await
609 {
610 state
611 } else {
612 let total_items = args.module_api().fetch_recovery_count().await?;
614
615 RecoveryStateV2::new(
616 total_items,
617 args.cfg().tbs_pks.tiers().copied().collect(),
618 args.module_root_secret(),
619 )
620 };
621
622 if state.next_index == state.total_items {
623 return Ok(None);
624 }
625
626 let peer_selector = PeerSelector::new(args.api().all_peers().clone());
627
628 let mut recovery_stream = futures::stream::iter(
629 (state.next_index..state.total_items).step_by(SLICE_SIZE as usize),
630 )
631 .map(move |start| {
632 let api = args.module_api().clone();
633 let end = std::cmp::min(start + SLICE_SIZE, state.total_items);
634
635 async move { (start, end, api.fetch_recovery_slice_hash(start, end).await) }
636 })
637 .buffered(PARALLEL_HASH_REQUESTS)
638 .map(move |(start, end, hash)| {
639 download_slice_with_hash(
640 args.module_api().clone(),
641 peer_selector.clone(),
642 start,
643 end,
644 hash,
645 )
646 })
647 .buffered(PARALLEL_SLICE_REQUESTS);
648
649 let secret = args.module_root_secret().clone();
650
651 loop {
652 let items = recovery_stream
653 .next()
654 .await
655 .expect("mint recovery stream finished before recovery is complete");
656
657 for item in &items {
658 match item {
659 RecoveryItem::Output { amount, nonce } => {
660 state.handle_output(*amount, *nonce, &secret);
661 }
662 RecoveryItem::Input { nonce } => {
663 state.handle_input(*nonce);
664 }
665 }
666 }
667
668 state.next_index += items.len() as u64;
669
670 let mut dbtx = args.db().begin_transaction().await;
671
672 dbtx.insert_entry(&RecoveryStateV2Key, &state).await;
673
674 if state.next_index == state.total_items {
675 let finalized = state.finalize();
677
678 let recovered_amount = finalized
680 .pending_notes
681 .iter()
682 .map(|(amount, _)| *amount)
683 .sum::<Amount>();
684
685 let blind_nonces: Vec<BlindNonce> = finalized
687 .pending_notes
688 .iter()
689 .map(|(_, req)| BlindNonce(req.blinded_message()))
690 .collect();
691
692 let outpoints = if blind_nonces.is_empty() {
694 vec![]
695 } else {
696 args.module_api()
697 .fetch_blind_nonce_outpoints(blind_nonces)
698 .await
699 .map_err(RecoverFromSlicesError::BlindNonceOutpoints)?
700 };
701
702 let state_machines: Vec<MintClientStateMachines> = finalized
704 .pending_notes
705 .into_iter()
706 .zip(outpoints)
707 .map(|((amount, issuance_request), out_point)| {
708 MintClientStateMachines::Output(MintOutputStateMachine {
709 common: MintOutputCommon {
710 operation_id: OperationId::new_random(),
711 out_point_range: OutPointRange::new_single(
712 out_point.txid,
713 out_point.out_idx,
714 )
715 .expect("Can't overflow"),
716 },
717 state: MintOutputStates::Created(output::MintOutputStatesCreated {
718 amount,
719 issuance_request,
720 }),
721 })
722 })
723 .collect();
724
725 let state_machines = args.context().map_dyn(state_machines).collect();
726
727 args.context()
728 .add_state_machines_dbtx(&mut dbtx.to_ref_nc(), state_machines)
729 .await?;
730
731 for (amount, note_idx) in finalized.next_note_idx {
733 dbtx.insert_entry(&NextECashNoteIndexKey(amount), ¬e_idx.as_u64())
734 .await;
735 }
736
737 dbtx.commit_tx().await;
738
739 return Ok(Some(recovered_amount));
740 }
741
742 dbtx.commit_tx().await;
743
744 args.update_recovery_progress(RecoveryProgress {
745 complete: state.next_index.try_into().unwrap_or(u32::MAX),
746 total: state.total_items.try_into().unwrap_or(u32::MAX),
747 });
748 }
749 }
750}
751
752#[derive(Debug, thiserror::Error)]
754enum RecoverFromSlicesError {
755 #[error(transparent)]
757 RecoveryCount(#[from] FederationError),
758
759 #[error("Failed to fetch blind nonce outpoints")]
761 BlindNonceOutpoints(#[source] FederationError),
762
763 #[error(transparent)]
766 StateMachines(#[from] AddStateMachinesError),
767}
768
769impl ModuleInit for MintClientInit {
770 type Common = MintCommonInit;
771
772 async fn dump_database(
773 &self,
774 dbtx: &mut DatabaseTransaction<'_>,
775 prefix_names: Vec<String>,
776 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
777 let mut mint_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
778 BTreeMap::new();
779 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
780 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
781 });
782
783 for table in filtered_prefixes {
784 match table {
785 DbKeyPrefix::Note => {
786 push_db_pair_items!(
787 dbtx,
788 NoteKeyPrefix,
789 NoteKey,
790 SpendableNoteUndecoded,
791 mint_client_items,
792 "Notes"
793 );
794 }
795 DbKeyPrefix::NextECashNoteIndex => {
796 push_db_pair_items!(
797 dbtx,
798 NextECashNoteIndexKeyPrefix,
799 NextECashNoteIndexKey,
800 u64,
801 mint_client_items,
802 "NextECashNoteIndex"
803 );
804 }
805 DbKeyPrefix::CancelledOOBSpend => {
806 push_db_pair_items!(
807 dbtx,
808 CancelledOOBSpendKeyPrefix,
809 CancelledOOBSpendKey,
810 (),
811 mint_client_items,
812 "CancelledOOBSpendKey"
813 );
814 }
815 DbKeyPrefix::RecoveryFinalized => {
816 if let Some(val) = dbtx.get_value(&RecoveryFinalizedKey).await {
817 mint_client_items.insert("RecoveryFinalized".to_string(), Box::new(val));
818 }
819 }
820 DbKeyPrefix::RecoveryState
821 | DbKeyPrefix::ReusedNoteIndices
822 | DbKeyPrefix::RecoveryStateV2
823 | DbKeyPrefix::ExternalReservedStart
824 | DbKeyPrefix::CoreInternalReservedStart
825 | DbKeyPrefix::CoreInternalReservedEnd => {}
826 }
827 }
828
829 Box::new(mint_client_items.into_iter())
830 }
831}
832
833#[apply(async_trait_maybe_send!)]
834impl ClientModuleInit for MintClientInit {
835 type Module = MintClientModule;
836
837 fn supported_api_versions(&self) -> MultiApiVersion {
838 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
839 .expect("no version conflicts")
840 }
841
842 async fn init(
843 &self,
844 args: &ClientModuleInitArgs<Self>,
845 ) -> Result<Self::Module, ClientModuleError> {
846 Ok(MintClientModule {
847 federation_id: *args.federation_id(),
848 cfg: args.cfg().clone(),
849 secret: args.module_root_secret().clone(),
850 secp: Secp256k1::new(),
851 notifier: args.notifier().clone(),
852 client_ctx: args.context(),
853 balance_update_sender: tokio::sync::watch::channel(()).0,
854 })
855 }
856
857 fn recovery_mode(&self) -> RecoveryMode {
858 RecoveryMode::Unusable
859 }
860
861 async fn recover(
862 &self,
863 args: &ClientModuleRecoverArgs<Self>,
864 snapshot: Option<&<Self::Module as ClientModule>::Backup>,
865 ) -> Result<Option<Amount>, ClientModuleError> {
866 let mut dbtx = args.db().begin_transaction_nc().await;
867
868 if dbtx.get_value(&RecoveryStateV2Key).await.is_some() {
870 return self
871 .recover_from_slices(args)
872 .await
873 .map_err(ClientModuleError::other);
874 }
875
876 if dbtx.get_value(&RecoveryStateKey).await.is_some() {
878 return args
879 .recover_from_history::<MintRecovery>(self, snapshot)
880 .await;
881 }
882
883 if args.module_api().fetch_recovery_count().await.is_ok() {
886 self.recover_from_slices(args)
888 .await
889 .map_err(ClientModuleError::other)
890 } else {
891 args.recover_from_history::<MintRecovery>(self, snapshot)
893 .await
894 }
895 }
896
897 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
898 let mut migrations: BTreeMap<DatabaseVersion, ClientModuleMigrationFn> = BTreeMap::new();
899 migrations.insert(DatabaseVersion(0), |dbtx, _, _| {
900 Box::pin(migrate_to_v1(dbtx))
901 });
902 migrations.insert(DatabaseVersion(1), |_, active_states, inactive_states| {
903 Box::pin(async { migrate_state(active_states, inactive_states, migrate_state_to_v2) })
904 });
905
906 migrations
907 }
908
909 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
910 Some(
911 DbKeyPrefix::iter()
912 .map(|p| p as u8)
913 .chain(
914 DbKeyPrefix::ExternalReservedStart as u8
915 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
916 )
917 .collect(),
918 )
919 }
920}
921
922pub struct MintClientModule {
948 federation_id: FederationId,
949 cfg: MintClientConfig,
950 secret: DerivableSecret,
951 secp: Secp256k1<All>,
952 notifier: ModuleNotifier<MintClientStateMachines>,
953 pub client_ctx: ClientContext<Self>,
954 balance_update_sender: tokio::sync::watch::Sender<()>,
955}
956
957impl fmt::Debug for MintClientModule {
958 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
959 f.debug_struct("MintClientModule")
960 .field("federation_id", &self.federation_id)
961 .field("cfg", &self.cfg)
962 .field("notifier", &self.notifier)
963 .field("client_ctx", &self.client_ctx)
964 .finish_non_exhaustive()
965 }
966}
967
968#[derive(Clone)]
970pub struct MintClientContext {
971 pub federation_id: FederationId,
972 pub client_ctx: ClientContext<MintClientModule>,
973 pub mint_decoder: Decoder,
974 pub tbs_pks: Tiered<AggregatePublicKey>,
975 pub peer_tbs_pks: BTreeMap<PeerId, Tiered<tbs::PublicKeyShare>>,
976 pub secret: DerivableSecret,
977 pub module_db: Database,
980 pub balance_update_sender: tokio::sync::watch::Sender<()>,
982}
983
984impl fmt::Debug for MintClientContext {
985 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
986 f.debug_struct("MintClientContext")
987 .field("federation_id", &self.federation_id)
988 .finish_non_exhaustive()
989 }
990}
991
992impl MintClientContext {
993 fn await_cancel_oob_payment(&self, operation_id: OperationId) -> BoxFuture<'static, ()> {
994 let db = self.module_db.clone();
995 Box::pin(async move {
996 db.wait_key_exists(&CancelledOOBSpendKey(operation_id))
997 .await;
998 })
999 }
1000}
1001
1002impl Context for MintClientContext {
1003 const KIND: Option<ModuleKind> = Some(KIND);
1004}
1005
1006#[apply(async_trait_maybe_send!)]
1007impl ClientModule for MintClientModule {
1008 type Init = MintClientInit;
1009 type Common = MintModuleTypes;
1010 type Backup = EcashBackup;
1011 type ModuleStateMachineContext = MintClientContext;
1012 type States = MintClientStateMachines;
1013
1014 fn context(&self) -> Self::ModuleStateMachineContext {
1015 MintClientContext {
1016 federation_id: self.federation_id,
1017 client_ctx: self.client_ctx.clone(),
1018 mint_decoder: self.decoder(),
1019 tbs_pks: self.cfg.tbs_pks.clone(),
1020 peer_tbs_pks: self.cfg.peer_tbs_pks.clone(),
1021 secret: self.secret.clone(),
1022 module_db: self.client_ctx.module_db().clone(),
1023 balance_update_sender: self.balance_update_sender.clone(),
1024 }
1025 }
1026
1027 fn input_fee(
1028 &self,
1029 amount: &Amounts,
1030 _input: &<Self::Common as ModuleCommon>::Input,
1031 ) -> Option<Amounts> {
1032 Some(Amounts::new_bitcoin(
1033 self.cfg.fee_consensus.fee(amount.get_bitcoin()),
1034 ))
1035 }
1036
1037 fn output_fee(
1038 &self,
1039 amount: &Amounts,
1040 _output: &<Self::Common as ModuleCommon>::Output,
1041 ) -> Option<Amounts> {
1042 Some(Amounts::new_bitcoin(
1043 self.cfg.fee_consensus.fee(amount.get_bitcoin()),
1044 ))
1045 }
1046
1047 #[cfg(feature = "cli")]
1048 async fn handle_cli_command(
1049 &self,
1050 args: &[std::ffi::OsString],
1051 ) -> Result<serde_json::Value, ClientModuleError> {
1052 cli::handle_cli_command(self, args)
1053 .await
1054 .map_err(ClientModuleError::other)
1055 }
1056
1057 fn supports_backup(&self) -> bool {
1058 true
1059 }
1060
1061 async fn backup(&self) -> Result<EcashBackup, ClientModuleError> {
1062 self.client_ctx
1063 .module_db()
1064 .autocommit(
1065 |dbtx_ctx, _| {
1066 Box::pin(async { self.prepare_plaintext_ecash_backup(dbtx_ctx).await })
1067 },
1068 None,
1069 )
1070 .await
1071 .map_err(|e| match e {
1072 AutocommitError::ClosureError { error, .. } => ClientModuleError::other(error),
1073 AutocommitError::CommitFailed { last_error, .. } => {
1074 ClientModuleError::other(last_error)
1075 }
1076 })
1077 }
1078
1079 fn supports_being_primary(&self) -> PrimaryModuleSupport {
1080 PrimaryModuleSupport::selected(PrimaryModulePriority::HIGH, [AmountUnit::BITCOIN])
1081 }
1082
1083 async fn create_final_inputs_and_outputs(
1084 &self,
1085 dbtx: &mut DatabaseTransaction<'_>,
1086 operation_id: OperationId,
1087 unit: AmountUnit,
1088 mut input_amount: Amount,
1089 mut output_amount: Amount,
1090 ) -> Result<
1091 (
1092 ClientInputBundle<MintInput, MintClientStateMachines>,
1093 ClientOutputBundle<MintOutput, MintClientStateMachines>,
1094 ),
1095 ClientModuleError,
1096 > {
1097 let consolidation_inputs = self
1098 .consolidate_notes(dbtx)
1099 .await
1100 .map_err(ClientModuleError::other)?;
1101
1102 if unit != AmountUnit::BITCOIN {
1103 return Err(ClientModuleError::other("Module can only handle Bitcoin"));
1104 }
1105
1106 input_amount += consolidation_inputs
1107 .iter()
1108 .map(|input| input.0.amounts.get_bitcoin())
1109 .sum();
1110
1111 output_amount += consolidation_inputs
1112 .iter()
1113 .map(|input| self.cfg.fee_consensus.fee(input.0.amounts.get_bitcoin()))
1114 .sum();
1115
1116 let additional_inputs = self
1117 .create_sufficient_input(dbtx, output_amount.saturating_sub(input_amount))
1118 .await?;
1119
1120 input_amount += additional_inputs
1121 .iter()
1122 .map(|input| input.0.amounts.get_bitcoin())
1123 .sum();
1124
1125 output_amount += additional_inputs
1126 .iter()
1127 .map(|input| self.cfg.fee_consensus.fee(input.0.amounts.get_bitcoin()))
1128 .sum();
1129
1130 let outputs = self
1131 .create_output(
1132 dbtx,
1133 operation_id,
1134 2,
1135 input_amount.saturating_sub(output_amount),
1136 )
1137 .await;
1138
1139 Ok((
1140 create_bundle_for_inputs(
1141 [consolidation_inputs, additional_inputs].concat(),
1142 operation_id,
1143 ),
1144 outputs,
1145 ))
1146 }
1147
1148 async fn await_primary_module_output(
1149 &self,
1150 operation_id: OperationId,
1151 out_point: OutPoint,
1152 ) -> Result<(), ClientModuleError> {
1153 self.await_output_finalized(operation_id, out_point)
1154 .await
1155 .map_err(ClientModuleError::other)
1156 }
1157
1158 async fn get_balance(&self, dbtx: &mut DatabaseTransaction<'_>, unit: AmountUnit) -> Amount {
1159 if unit != AmountUnit::BITCOIN {
1160 return Amount::ZERO;
1161 }
1162 self.get_note_counts_by_denomination(dbtx)
1163 .await
1164 .total_amount()
1165 }
1166
1167 async fn get_balances(&self, dbtx: &mut DatabaseTransaction<'_>) -> Amounts {
1168 Amounts::new_bitcoin(
1169 <Self as ClientModule>::get_balance(self, dbtx, AmountUnit::BITCOIN).await,
1170 )
1171 }
1172
1173 async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
1174 Box::pin(tokio_stream::wrappers::WatchStream::new(
1175 self.balance_update_sender.subscribe(),
1176 ))
1177 }
1178
1179 async fn leave(&self, dbtx: &mut DatabaseTransaction<'_>) -> Result<(), ClientModuleError> {
1180 let balance = ClientModule::get_balances(self, dbtx).await;
1181
1182 for (unit, amount) in balance {
1183 if Amount::from_units(0) < amount {
1184 return Err(ClientModuleError::other(format!(
1185 "Outstanding balance: {amount}, unit: {unit:?}"
1186 )));
1187 }
1188 }
1189
1190 if !self.client_ctx.get_own_active_states().await.is_empty() {
1191 return Err(ClientModuleError::other("Pending operations"));
1192 }
1193 Ok(())
1194 }
1195
1196 async fn handle_rpc(
1197 &self,
1198 method: String,
1199 request: serde_json::Value,
1200 ) -> BoxStream<'_, Result<serde_json::Value, ClientModuleError>> {
1201 let stream: BoxStream<'_, Result<serde_json::Value, RpcError>> = Box::pin(try_stream! {
1202 match method.as_str() {
1203 "reissue_external_notes" => {
1204 let req: ReissueExternalNotesRequest = serde_json::from_value(request)?;
1205 let result = self.reissue_external_notes(req.oob_notes, req.extra_meta).await?;
1206 yield serde_json::to_value(result)?;
1207 }
1208 "subscribe_reissue_external_notes" => {
1209 let req: SubscribeReissueExternalNotesRequest = serde_json::from_value(request)?;
1210 let stream = self.subscribe_reissue_external_notes(req.operation_id).await?;
1211 for await state in stream.into_stream() {
1212 yield serde_json::to_value(state)?;
1213 }
1214 }
1215 "spend_notes" => {
1216 let req: SpendNotesRequest = serde_json::from_value(request)?;
1217 let result = self.spend_notes_with_selector(
1218 &SelectNotesWithExactAmount,
1219 req.amount,
1220 req.try_cancel_after,
1221 req.include_invite,
1222 req.extra_meta
1223 ).await?;
1224 yield serde_json::to_value(result)?;
1225 }
1226 "spend_notes_expert" => {
1227 let req: SpendNotesExpertRequest = serde_json::from_value(request)?;
1228 let result = self.spend_notes_with_selector(
1229 &SelectNotesWithAtleastAmount,
1230 req.min_amount,
1231 req.try_cancel_after,
1232 req.include_invite,
1233 req.extra_meta
1234 ).await?;
1235 yield serde_json::to_value(result)?;
1236 }
1237 "validate_notes" => {
1238 let req: ValidateNotesRequest = serde_json::from_value(request)?;
1239 let result = self.validate_notes(&req.oob_notes)?;
1240 yield serde_json::to_value(result)?;
1241 }
1242 "try_cancel_spend_notes" => {
1243 let req: TryCancelSpendNotesRequest = serde_json::from_value(request)?;
1244 let result = self.try_cancel_spend_notes(req.operation_id).await;
1245 yield serde_json::to_value(result)?;
1246 }
1247 "subscribe_spend_notes" => {
1248 let req: SubscribeSpendNotesRequest = serde_json::from_value(request)?;
1249 let stream = self.subscribe_spend_notes(req.operation_id).await?;
1250 for await state in stream.into_stream() {
1251 yield serde_json::to_value(state)?;
1252 }
1253 }
1254 "await_spend_oob_refund" => {
1255 let req: AwaitSpendOobRefundRequest = serde_json::from_value(request)?;
1256 let value = self.await_spend_oob_refund(req.operation_id).await;
1257 yield serde_json::to_value(value)?;
1258 }
1259 "note_counts_by_denomination" => {
1260 let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1261 let note_counts = self.get_note_counts_by_denomination(&mut dbtx).await;
1262 yield serde_json::to_value(note_counts)?;
1263 }
1264 _ => {
1265 Err(RpcError::UnknownMethod { method: method.clone() })?;
1266 unreachable!()
1267 },
1268 }
1269 });
1270 Box::pin(stream.map_err(ClientModuleError::other))
1271 }
1272}
1273
1274#[derive(Debug, thiserror::Error)]
1276enum RpcError {
1277 #[error(transparent)]
1280 Json(#[from] serde_json::Error),
1281
1282 #[error(transparent)]
1284 Reissue(#[from] ReissueExternalNotesError),
1285
1286 #[error(transparent)]
1288 SubscribeReissue(#[from] SubscribeReissueExternalNotesError),
1289
1290 #[error(transparent)]
1292 Spend(#[from] SpendOOBError),
1293
1294 #[error(transparent)]
1296 Validate(#[from] ValidateNotesError),
1297
1298 #[error(transparent)]
1300 SubscribeSpend(#[from] SubscribeSpendNotesError),
1301
1302 #[error("Unknown method: {method}")]
1304 UnknownMethod { method: String },
1305}
1306
1307#[derive(Deserialize)]
1308struct ReissueExternalNotesRequest {
1309 oob_notes: OOBNotes,
1310 extra_meta: serde_json::Value,
1311}
1312
1313#[derive(Deserialize)]
1314struct SubscribeReissueExternalNotesRequest {
1315 operation_id: OperationId,
1316}
1317
1318#[derive(Deserialize)]
1321struct SpendNotesExpertRequest {
1322 min_amount: Amount,
1323 try_cancel_after: Option<Duration>,
1324 include_invite: bool,
1325 extra_meta: serde_json::Value,
1326}
1327
1328#[derive(Deserialize)]
1329struct SpendNotesRequest {
1330 amount: Amount,
1331 try_cancel_after: Option<Duration>,
1332 include_invite: bool,
1333 extra_meta: serde_json::Value,
1334}
1335
1336#[derive(Deserialize)]
1337struct ValidateNotesRequest {
1338 oob_notes: OOBNotes,
1339}
1340
1341#[derive(Deserialize)]
1342struct TryCancelSpendNotesRequest {
1343 operation_id: OperationId,
1344}
1345
1346#[derive(Deserialize)]
1347struct SubscribeSpendNotesRequest {
1348 operation_id: OperationId,
1349}
1350
1351#[derive(Deserialize)]
1352struct AwaitSpendOobRefundRequest {
1353 operation_id: OperationId,
1354}
1355
1356#[derive(thiserror::Error, Debug)]
1358#[non_exhaustive]
1359pub enum ReissueExternalNotesError {
1360 #[error("Reissuing zero-amount e-cash is not supported")]
1362 ZeroAmount,
1363
1364 #[error("The notes were issued by federation {found}, not {expected}")]
1366 WrongFederationId {
1367 expected: FederationIdPrefix,
1369 found: FederationIdPrefix,
1371 },
1372
1373 #[error("We already reissued these notes")]
1376 AlreadyReissued,
1377
1378 #[error("The notes could not be validated")]
1380 Notes(#[from] ValidateNotesError),
1381
1382 #[error("The reissue transaction could not be submitted")]
1384 Transaction(#[source] TransactionSubmitError),
1385}
1386
1387impl MintClientModule {
1388 async fn create_sufficient_input(
1389 &self,
1390 dbtx: &mut DatabaseTransaction<'_>,
1391 min_amount: Amount,
1392 ) -> Result<Vec<(ClientInput<MintInput>, SpendableNote)>, ClientModuleError> {
1393 if min_amount == Amount::ZERO {
1394 return Ok(vec![]);
1395 }
1396
1397 let selected_notes = Self::select_notes(
1398 dbtx,
1399 &SelectNotesWithAtleastAmount,
1400 min_amount,
1401 self.cfg.fee_consensus.clone(),
1402 )
1403 .await
1404 .map_err(|error| match error {
1405 SelectNotesError::InsufficientBalance(error) => {
1408 ClientModuleError::InsufficientBalance(error)
1409 }
1410 other => ClientModuleError::other(other),
1411 })?;
1412
1413 for (amount, note) in selected_notes.iter_items() {
1414 debug!(target: LOG_CLIENT_MODULE_MINT, %amount, %note, "Spending note as sufficient input to fund a tx");
1415 MintClientModule::delete_spendable_note(&self.client_ctx, dbtx, amount, note).await;
1416 }
1417
1418 let sender = self.balance_update_sender.clone();
1419 dbtx.on_commit(move || sender.send_replace(()));
1420
1421 let inputs = self
1422 .create_input_from_notes(selected_notes)
1423 .map_err(ClientModuleError::other)?;
1424
1425 assert!(!inputs.is_empty());
1426
1427 Ok(inputs)
1428 }
1429
1430 #[deprecated(
1432 since = "0.5.0",
1433 note = "Use `get_note_counts_by_denomination` instead"
1434 )]
1435 pub async fn get_notes_tier_counts(&self, dbtx: &mut DatabaseTransaction<'_>) -> TieredCounts {
1436 self.get_note_counts_by_denomination(dbtx).await
1437 }
1438
1439 pub async fn get_available_notes_by_tier_counts(
1443 &self,
1444 dbtx: &mut DatabaseTransaction<'_>,
1445 counts: TieredCounts,
1446 ) -> (TieredMulti<SpendableNoteUndecoded>, TieredCounts) {
1447 dbtx.find_by_prefix(&NoteKeyPrefix)
1448 .await
1449 .fold(
1450 (TieredMulti::<SpendableNoteUndecoded>::default(), counts),
1451 |(mut notes, mut counts), (key, note)| async move {
1452 let amount = key.amount;
1453 if 0 < counts.get(amount) {
1454 counts.dec(amount);
1455 notes.push(amount, note);
1456 }
1457
1458 (notes, counts)
1459 },
1460 )
1461 .await
1462 }
1463
1464 pub async fn create_output(
1469 &self,
1470 dbtx: &mut DatabaseTransaction<'_>,
1471 operation_id: OperationId,
1472 notes_per_denomination: u16,
1473 exact_amount: Amount,
1474 ) -> ClientOutputBundle<MintOutput, MintClientStateMachines> {
1475 if exact_amount == Amount::ZERO {
1476 return ClientOutputBundle::new(vec![], vec![]);
1477 }
1478
1479 let denominations = represent_amount(
1482 exact_amount,
1483 &self.get_note_counts_by_denomination(dbtx).await,
1484 &self.cfg.tbs_pks,
1485 notes_per_denomination,
1486 &self.cfg.fee_consensus,
1487 );
1488
1489 self.create_output_for_denominations(dbtx, operation_id, denominations)
1490 .await
1491 }
1492
1493 async fn create_exact_output(
1505 &self,
1506 dbtx: &mut DatabaseTransaction<'_>,
1507 operation_id: OperationId,
1508 amount: Amount,
1509 ) -> ClientOutputBundle<MintOutput, MintClientStateMachines> {
1510 if amount == Amount::ZERO {
1511 return ClientOutputBundle::new(vec![], vec![]);
1512 }
1513
1514 self.create_output_for_denominations(
1515 dbtx,
1516 operation_id,
1517 self.represent_exact_amount(amount),
1518 )
1519 .await
1520 }
1521
1522 fn represent_exact_amount(&self, amount: Amount) -> TieredCounts {
1528 represent_amount(
1529 amount,
1530 &TieredCounts::default(),
1531 &self.cfg.tbs_pks,
1532 0,
1533 &FeeConsensus::zero(),
1534 )
1535 }
1536
1537 async fn create_output_for_denominations(
1538 &self,
1539 dbtx: &mut DatabaseTransaction<'_>,
1540 operation_id: OperationId,
1541 denominations: TieredCounts,
1542 ) -> ClientOutputBundle<MintOutput, MintClientStateMachines> {
1543 let mut outputs = Vec::new();
1544 let mut issuance_requests = Vec::new();
1545
1546 for (amount, num) in denominations.iter() {
1547 for _ in 0..num {
1548 let (issuance_request, blind_nonce) = self.new_ecash_note(amount, dbtx).await;
1549
1550 debug!(
1551 %amount,
1552 "Generated issuance request"
1553 );
1554
1555 outputs.push(ClientOutput {
1556 output: MintOutput::new_v0(amount, blind_nonce),
1557 amounts: Amounts::new_bitcoin(amount),
1558 });
1559
1560 issuance_requests.push((amount, issuance_request));
1561 }
1562 }
1563
1564 let state_generator = Arc::new(move |out_point_range: OutPointRange| {
1565 assert_eq!(out_point_range.count(), issuance_requests.len());
1566 vec![MintClientStateMachines::Output(MintOutputStateMachine {
1567 common: MintOutputCommon {
1568 operation_id,
1569 out_point_range,
1570 },
1571 state: MintOutputStates::CreatedMulti(MintOutputStatesCreatedMulti {
1572 issuance_requests: out_point_range
1573 .into_iter()
1574 .map(|out_point| out_point.out_idx)
1575 .zip(issuance_requests.clone())
1576 .collect(),
1577 }),
1578 })]
1579 });
1580
1581 ClientOutputBundle::new(
1582 outputs,
1583 vec![ClientOutputSM {
1584 state_machines: state_generator,
1585 }],
1586 )
1587 }
1588
1589 pub async fn get_note_counts_by_denomination(
1591 &self,
1592 dbtx: &mut DatabaseTransaction<'_>,
1593 ) -> TieredCounts {
1594 dbtx.find_by_prefix(&NoteKeyPrefix)
1595 .await
1596 .fold(
1597 TieredCounts::default(),
1598 |mut acc, (key, _note)| async move {
1599 acc.inc(key.amount, 1);
1600 acc
1601 },
1602 )
1603 .await
1604 }
1605
1606 #[deprecated(
1608 since = "0.5.0",
1609 note = "Use `get_note_counts_by_denomination` instead"
1610 )]
1611 pub async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> TieredCounts {
1612 self.get_note_counts_by_denomination(dbtx).await
1613 }
1614
1615 pub async fn estimate_spend_all_fees(&self) -> Amount {
1621 let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1622 let note_counts = self.get_note_counts_by_denomination(&mut dbtx).await;
1623
1624 note_counts
1625 .iter()
1626 .filter_map(|(amount, count)| {
1627 let note_fee = self.cfg.fee_consensus.fee(amount);
1628 if note_fee < amount {
1629 note_fee.checked_mul(count as u64)
1630 } else {
1631 None
1632 }
1633 })
1634 .fold(Amount::ZERO, |acc, fee| {
1635 acc.checked_add(fee).expect("fee sum overflow")
1636 })
1637 }
1638
1639 pub async fn await_output_finalized(
1643 &self,
1644 operation_id: OperationId,
1645 out_point: OutPoint,
1646 ) -> Result<(), AwaitOutputFinalizedError> {
1647 let stream = self
1648 .notifier
1649 .subscribe(operation_id)
1650 .await
1651 .filter_map(|state| async {
1652 let MintClientStateMachines::Output(state) = state else {
1653 return None;
1654 };
1655
1656 if state.common.txid() != out_point.txid
1657 || !state
1658 .common
1659 .out_point_range
1660 .out_idx_iter()
1661 .contains(&out_point.out_idx)
1662 {
1663 return None;
1664 }
1665
1666 match state.state {
1667 MintOutputStates::Succeeded(_) => Some(Ok(())),
1668 MintOutputStates::Aborted(_) => {
1669 Some(Err(AwaitOutputFinalizedError::TransactionRejected))
1670 }
1671 MintOutputStates::Failed(failed) => {
1672 Some(Err(AwaitOutputFinalizedError::Failed {
1673 reason: failed.error,
1674 }))
1675 }
1676 MintOutputStates::Created(_) | MintOutputStates::CreatedMulti(_) => None,
1677 }
1678 });
1679 pin_mut!(stream);
1680
1681 stream.next_or_pending().await
1682 }
1683
1684 pub async fn consolidate_notes(
1691 &self,
1692 dbtx: &mut DatabaseTransaction<'_>,
1693 ) -> Result<Vec<(ClientInput<MintInput>, SpendableNote)>, ValidateNotesError> {
1694 const MAX_NOTES_PER_TIER_TRIGGER: usize = 8;
1697 const MIN_NOTES_PER_TIER: usize = 4;
1699 const MAX_NOTES_TO_CONSOLIDATE_IN_TX: usize = 20;
1702 #[allow(clippy::assertions_on_constants)]
1704 {
1705 assert!(MIN_NOTES_PER_TIER <= MAX_NOTES_PER_TIER_TRIGGER);
1706 }
1707
1708 let counts = self.get_note_counts_by_denomination(dbtx).await;
1709
1710 let should_consolidate = counts
1711 .iter()
1712 .any(|(_, count)| MAX_NOTES_PER_TIER_TRIGGER < count);
1713
1714 if !should_consolidate {
1715 return Ok(vec![]);
1716 }
1717
1718 let mut max_count = MAX_NOTES_TO_CONSOLIDATE_IN_TX;
1719
1720 let excessive_counts: TieredCounts = counts
1721 .iter()
1722 .map(|(amount, count)| {
1723 let take = (count.saturating_sub(MIN_NOTES_PER_TIER)).min(max_count);
1724
1725 max_count -= take;
1726 (amount, take)
1727 })
1728 .collect();
1729
1730 let (selected_notes, unavailable) = self
1731 .get_available_notes_by_tier_counts(dbtx, excessive_counts)
1732 .await;
1733
1734 debug_assert!(
1735 unavailable.is_empty(),
1736 "Can't have unavailable notes on a subset of all notes: {unavailable:?}"
1737 );
1738
1739 if !selected_notes.is_empty() {
1740 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");
1741 }
1742
1743 let mut selected_notes_decoded = vec![];
1744 for (amount, note) in selected_notes.iter_items() {
1745 let spendable_note_decoded = note.decode()?;
1746 debug!(target: LOG_CLIENT_MODULE_MINT, %amount, %note, "Consolidating note");
1747 Self::delete_spendable_note(&self.client_ctx, dbtx, amount, &spendable_note_decoded)
1748 .await;
1749 selected_notes_decoded.push((amount, spendable_note_decoded));
1750 }
1751
1752 let sender = self.balance_update_sender.clone();
1753 dbtx.on_commit(move || sender.send_replace(()));
1754
1755 self.create_input_from_notes(selected_notes_decoded.into_iter().collect())
1756 }
1757
1758 #[allow(clippy::type_complexity)]
1760 pub fn create_input_from_notes(
1761 &self,
1762 notes: TieredMulti<SpendableNote>,
1763 ) -> Result<Vec<(ClientInput<MintInput>, SpendableNote)>, ValidateNotesError> {
1764 let mut inputs_and_notes = Vec::new();
1765
1766 for (index, (amount, spendable_note)) in notes.into_iter_items().enumerate() {
1767 let key = self
1768 .cfg
1769 .tbs_pks
1770 .get(amount)
1771 .ok_or(ValidateNotesError::InvalidAmountTier { index, amount })?;
1772
1773 let note = spendable_note.note();
1774
1775 if !note.verify(*key) {
1776 return Err(ValidateNotesError::InvalidSignature { index });
1777 }
1778
1779 inputs_and_notes.push((
1780 ClientInput {
1781 input: MintInput::new_v0(amount, note),
1782 keys: vec![spendable_note.spend_key],
1783 amounts: Amounts::new_bitcoin(amount),
1784 },
1785 spendable_note,
1786 ));
1787 }
1788
1789 Ok(inputs_and_notes)
1790 }
1791
1792 async fn spend_notes_oob(
1793 &self,
1794 dbtx: &mut DatabaseTransaction<'_>,
1795 notes_selector: &impl NotesSelector,
1796 amount: Amount,
1797 try_cancel_after: Option<Duration>,
1798 ) -> Result<
1799 (
1800 OperationId,
1801 Vec<MintClientStateMachines>,
1802 TieredMulti<SpendableNote>,
1803 ),
1804 SpendOOBError,
1805 > {
1806 if amount == Amount::ZERO {
1807 return Err(SpendOOBError::ZeroAmount);
1808 }
1809
1810 let selected_notes =
1811 Self::select_notes(dbtx, notes_selector, amount, FeeConsensus::zero()).await?;
1812
1813 let operation_id = spendable_notes_to_operation_id(&selected_notes);
1814
1815 for (amount, note) in selected_notes.iter_items() {
1816 debug!(target: LOG_CLIENT_MODULE_MINT, %amount, %note, "Spending note as oob");
1817 MintClientModule::delete_spendable_note(&self.client_ctx, dbtx, amount, note).await;
1818 }
1819
1820 let sender = self.balance_update_sender.clone();
1821 dbtx.on_commit(move || sender.send_replace(()));
1822
1823 let try_cancel_after = try_cancel_after.unwrap_or(OOB_SPEND_NO_TIMEOUT);
1824 let state_machines = if try_cancel_after == OOB_SPEND_NO_TIMEOUT {
1825 vec![]
1826 } else {
1827 vec![MintClientStateMachines::OOB(MintOOBStateMachine {
1828 operation_id,
1829 state: MintOOBStates::CreatedMulti(MintOOBStatesCreatedMulti {
1830 spendable_notes: selected_notes.clone().into_iter_items().collect(),
1831 timeout: fedimint_core::time::now() + try_cancel_after,
1832 }),
1833 })]
1834 };
1835
1836 Ok((operation_id, state_machines, selected_notes))
1837 }
1838
1839 async fn is_no_timeout_oob_spend(
1840 &self,
1841 operation_id: OperationId,
1842 ) -> Result<bool, SubscribeSpendNotesError> {
1843 let operation = self.mint_operation(operation_id).await?;
1844 let MintOperationMetaVariant::SpendOOB { no_timeout, .. } =
1845 operation.meta::<MintOperationMeta>().variant
1846 else {
1847 return Err(SubscribeSpendNotesError::NotAnOutOfBandSpend);
1848 };
1849
1850 Ok(no_timeout)
1851 }
1852
1853 pub async fn await_spend_oob_refund(&self, operation_id: OperationId) -> SpendOOBRefund {
1854 if self
1855 .is_no_timeout_oob_spend(operation_id)
1856 .await
1857 .unwrap_or(false)
1858 {
1859 return SpendOOBRefund {
1860 user_triggered: false,
1861 transaction_ids: vec![],
1862 };
1863 }
1864
1865 Box::pin(
1866 self.notifier
1867 .subscribe(operation_id)
1868 .await
1869 .filter_map(|state| async {
1870 let MintClientStateMachines::OOB(state) = state else {
1871 return None;
1872 };
1873
1874 match state.state {
1875 MintOOBStates::TimeoutRefund(refund) => Some(SpendOOBRefund {
1876 user_triggered: false,
1877 transaction_ids: vec![refund.refund_txid],
1878 }),
1879 MintOOBStates::UserRefund(refund) => Some(SpendOOBRefund {
1880 user_triggered: true,
1881 transaction_ids: vec![refund.refund_txid],
1882 }),
1883 MintOOBStates::UserRefundMulti(refund) => Some(SpendOOBRefund {
1884 user_triggered: true,
1885 transaction_ids: vec![refund.refund_txid],
1886 }),
1887 MintOOBStates::Created(_) | MintOOBStates::CreatedMulti(_) => None,
1888 }
1889 }),
1890 )
1891 .next_or_pending()
1892 .await
1893 }
1894
1895 async fn select_notes(
1897 dbtx: &mut DatabaseTransaction<'_>,
1898 notes_selector: &impl NotesSelector,
1899 requested_amount: Amount,
1900 fee_consensus: FeeConsensus,
1901 ) -> Result<TieredMulti<SpendableNote>, SelectNotesError> {
1902 let note_stream = dbtx
1903 .find_by_prefix_sorted_descending(&NoteKeyPrefix)
1904 .await
1905 .map(|(key, note)| (key.amount, note));
1906
1907 notes_selector
1908 .select_notes(note_stream, requested_amount, fee_consensus)
1909 .await?
1910 .into_iter_items()
1911 .map(|(amt, snote)| Ok((amt, snote.decode()?)))
1912 .collect::<Result<TieredMulti<_>, SelectNotesError>>()
1913 }
1914
1915 async fn get_all_spendable_notes(
1916 dbtx: &mut DatabaseTransaction<'_>,
1917 ) -> TieredMulti<SpendableNoteUndecoded> {
1918 (dbtx
1919 .find_by_prefix(&NoteKeyPrefix)
1920 .await
1921 .map(|(key, note)| (key.amount, note))
1922 .collect::<Vec<_>>()
1923 .await)
1924 .into_iter()
1925 .collect()
1926 }
1927
1928 async fn get_next_note_index(
1929 &self,
1930 dbtx: &mut DatabaseTransaction<'_>,
1931 amount: Amount,
1932 ) -> NoteIndex {
1933 NoteIndex(
1934 dbtx.get_value(&NextECashNoteIndexKey(amount))
1935 .await
1936 .unwrap_or(0),
1937 )
1938 }
1939
1940 pub fn new_note_secret_static(
1956 secret: &DerivableSecret,
1957 amount: Amount,
1958 note_idx: NoteIndex,
1959 ) -> DerivableSecret {
1960 assert_eq!(secret.level(), 2);
1961 debug!(?secret, %amount, %note_idx, "Deriving new mint note");
1962 secret
1963 .child_key(MINT_E_CASH_TYPE_CHILD_ID) .child_key(ChildId(note_idx.as_u64()))
1965 .child_key(ChildId(amount.msats))
1966 }
1967
1968 async fn new_note_secret(
1972 &self,
1973 amount: Amount,
1974 dbtx: &mut DatabaseTransaction<'_>,
1975 ) -> DerivableSecret {
1976 let new_idx = self.get_next_note_index(dbtx, amount).await;
1977 dbtx.insert_entry(&NextECashNoteIndexKey(amount), &new_idx.next().as_u64())
1978 .await;
1979 Self::new_note_secret_static(&self.secret, amount, new_idx)
1980 }
1981
1982 pub async fn new_ecash_note(
1983 &self,
1984 amount: Amount,
1985 dbtx: &mut DatabaseTransaction<'_>,
1986 ) -> (NoteIssuanceRequest, BlindNonce) {
1987 let secret = self.new_note_secret(amount, dbtx).await;
1988 NoteIssuanceRequest::new(&self.secp, &secret)
1989 }
1990
1991 pub async fn reissue_fee_quote(
2001 &self,
2002 oob_notes: &OOBNotes,
2003 ) -> Result<FeeQuote, TransactionSubmitError> {
2004 let input_amount = oob_notes.total_amount();
2009 let input_fee: Amount = oob_notes
2010 .notes()
2011 .iter_items()
2012 .map(|(amount, _)| self.cfg.fee_consensus.fee(amount))
2013 .sum();
2014
2015 self.client_ctx
2016 .fee_quote(
2017 OperationId::new_random(),
2018 FeeQuoteRequest {
2019 input_amount: Amounts::new_bitcoin(input_amount),
2020 output_amount: Amounts::ZERO,
2021 input_fee: Amounts::new_bitcoin(input_fee),
2022 output_fee: Amounts::ZERO,
2023 },
2024 )
2025 .await
2026 }
2027
2028 pub async fn send_fee_quote(&self, amount: Amount) -> Result<FeeQuote, TransactionSubmitError> {
2042 let amount = self.cfg.fee_consensus.round_up(amount);
2043
2044 let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
2045
2046 if Self::select_notes(
2050 &mut dbtx,
2051 &SelectNotesWithExactAmount,
2052 amount,
2053 FeeConsensus::zero(),
2054 )
2055 .await
2056 .is_ok()
2057 {
2058 return Ok(FeeQuote::ZERO);
2059 }
2060
2061 drop(dbtx);
2062
2063 let denominations = self.represent_exact_amount(amount);
2069
2070 let output_amount = denominations.total_amount();
2071 let output_fee: Amount = denominations
2072 .iter()
2073 .map(|(denomination, count)| self.cfg.fee_consensus.fee(denomination) * count as u64)
2074 .sum();
2075
2076 self.client_ctx
2077 .fee_quote(
2078 OperationId::new_random(),
2079 FeeQuoteRequest {
2080 input_amount: Amounts::ZERO,
2081 output_amount: Amounts::new_bitcoin(output_amount),
2082 input_fee: Amounts::ZERO,
2083 output_fee: Amounts::new_bitcoin(output_fee),
2084 },
2085 )
2086 .await
2087 }
2088
2089 pub async fn reissue_external_notes<M: Serialize + Send>(
2105 &self,
2106 oob_notes: OOBNotes,
2107 extra_meta: M,
2108 ) -> Result<OperationId, ReissueExternalNotesError> {
2109 let notes = oob_notes.notes().clone();
2110 let federation_id_prefix = oob_notes.federation_id_prefix();
2111
2112 debug!(
2113 target: LOG_CLIENT_MODULE_MINT,
2114 notes = ?notes
2115 .iter_items()
2116 .map(|(amount, note)| (amount, note.nonce()))
2117 .collect::<Vec<_>>(),
2118 "Reissuing external notes"
2119 );
2120
2121 if notes.total_amount() == Amount::ZERO {
2122 return Err(ReissueExternalNotesError::ZeroAmount);
2123 }
2124
2125 if federation_id_prefix != self.federation_id.to_prefix() {
2126 return Err(ReissueExternalNotesError::WrongFederationId {
2127 expected: self.federation_id.to_prefix(),
2128 found: federation_id_prefix,
2129 });
2130 }
2131
2132 let operation_id = OperationId(
2133 notes
2134 .consensus_hash::<sha256t::Hash<OOBReissueTag>>()
2135 .to_byte_array(),
2136 );
2137
2138 let amount = notes.total_amount();
2139 let mint_inputs = self.create_input_from_notes(notes)?;
2140
2141 let tx = TransactionBuilder::new().with_inputs(
2142 self.client_ctx
2143 .make_dyn(create_bundle_for_inputs(mint_inputs, operation_id)),
2144 );
2145
2146 let extra_meta = serde_json::to_value(extra_meta)
2147 .expect("MintClientModule::reissue_external_notes extra_meta is serializable");
2148 let operation_meta_gen = move |change_range: OutPointRange| MintOperationMeta {
2149 variant: MintOperationMetaVariant::Reissuance {
2150 legacy_out_point: None,
2151 txid: Some(change_range.txid()),
2152 out_point_indices: change_range
2153 .into_iter()
2154 .map(|out_point| out_point.out_idx)
2155 .collect(),
2156 },
2157 amount,
2158 extra_meta: extra_meta.clone(),
2159 };
2160
2161 self.client_ctx
2162 .finalize_and_submit_transaction(
2163 operation_id,
2164 MintCommonInit::KIND.as_str(),
2165 operation_meta_gen,
2166 tx,
2167 )
2168 .await
2169 .map_err(|error| match error {
2170 TransactionSubmitError::OperationAlreadyExists(_) => {
2171 ReissueExternalNotesError::AlreadyReissued
2172 }
2173 error => ReissueExternalNotesError::Transaction(error),
2174 })?;
2175
2176 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
2177
2178 self.client_ctx
2179 .log_event(&mut dbtx, OOBNotesReissued { amount })
2180 .await;
2181
2182 self.client_ctx
2183 .log_event(
2184 &mut dbtx,
2185 ReceivePaymentEvent {
2186 operation_id,
2187 amount,
2188 },
2189 )
2190 .await;
2191
2192 dbtx.commit_tx().await;
2193
2194 Ok(operation_id)
2195 }
2196
2197 pub async fn subscribe_reissue_external_notes(
2200 &self,
2201 operation_id: OperationId,
2202 ) -> SubscribeReissueExternalNotesResult {
2203 let operation = self.mint_operation(operation_id).await?;
2204 let (txid, out_points) = match operation.meta::<MintOperationMeta>().variant {
2205 MintOperationMetaVariant::Reissuance {
2206 legacy_out_point,
2207 txid,
2208 out_point_indices,
2209 } => {
2210 let txid = txid
2213 .or(legacy_out_point.map(|out_point| out_point.txid))
2214 .ok_or(SubscribeReissueExternalNotesError::NoTransaction)?;
2215
2216 let out_points = out_point_indices
2217 .into_iter()
2218 .map(|out_idx| OutPoint { txid, out_idx })
2219 .chain(legacy_out_point)
2220 .collect::<Vec<_>>();
2221
2222 (txid, out_points)
2223 }
2224 MintOperationMetaVariant::SpendOOB { .. } => {
2225 return Err(SubscribeReissueExternalNotesError::NotAReissuance);
2226 }
2227 };
2228
2229 let client_ctx = self.client_ctx.clone();
2230
2231 Ok(self.client_ctx.outcome_or_updates(
2232 &operation,
2233 operation_id,
2234 |state| match state {
2235 ReissueExternalNotesState::Created | ReissueExternalNotesState::Issuing => false,
2236 ReissueExternalNotesState::Done | ReissueExternalNotesState::Failed(_) => true,
2237 },
2238 move || {
2239 stream! {
2240 yield ReissueExternalNotesState::Created;
2241
2242 match client_ctx
2243 .transaction_updates(operation_id)
2244 .await
2245 .await_tx_accepted(txid)
2246 .await
2247 {
2248 Ok(()) => {
2249 yield ReissueExternalNotesState::Issuing;
2250 }
2251 Err(e) => {
2252 yield ReissueExternalNotesState::Failed(format!("Transaction not accepted {e:?}"));
2253 return;
2254 }
2255 }
2256
2257 for out_point in out_points {
2258 if let Err(e) = client_ctx.self_ref().await_output_finalized(operation_id, out_point).await {
2259 yield ReissueExternalNotesState::Failed(e.fmt_compact().to_string());
2260 return;
2261 }
2262 }
2263 yield ReissueExternalNotesState::Done;
2264 }}
2265 ))
2266 }
2267
2268 #[deprecated(
2281 since = "0.5.0",
2282 note = "Use `spend_notes_with_selector` instead, with `SelectNotesWithAtleastAmount` to maintain the same behavior"
2283 )]
2284 pub async fn spend_notes<M: Serialize + Send>(
2285 &self,
2286 min_amount: Amount,
2287 try_cancel_after: Option<Duration>,
2288 include_invite: bool,
2289 extra_meta: M,
2290 ) -> Result<(OperationId, OOBNotes), SpendOOBError> {
2291 self.spend_notes_with_selector(
2292 &SelectNotesWithAtleastAmount,
2293 min_amount,
2294 try_cancel_after,
2295 include_invite,
2296 extra_meta,
2297 )
2298 .await
2299 }
2300
2301 pub async fn spend_notes_with_selector<M: Serialize + Send>(
2318 &self,
2319 notes_selector: &impl NotesSelector,
2320 requested_amount: Amount,
2321 try_cancel_after: Option<Duration>,
2322 include_invite: bool,
2323 extra_meta: M,
2324 ) -> Result<(OperationId, OOBNotes), SpendOOBError> {
2325 let federation_id_prefix = self.federation_id.to_prefix();
2326 let extra_meta = serde_json::to_value(extra_meta)
2327 .expect("MintClientModule::spend_notes extra_meta is serializable");
2328
2329 self.client_ctx
2330 .module_db()
2331 .autocommit(
2332 |dbtx, _| {
2333 let extra_meta = extra_meta.clone();
2334 Box::pin(async {
2335 let no_timeout = try_cancel_after.is_none();
2336 let (operation_id, states, notes) = self
2337 .spend_notes_oob(
2338 dbtx,
2339 notes_selector,
2340 requested_amount,
2341 try_cancel_after,
2342 )
2343 .await?;
2344
2345 let oob_notes = if include_invite {
2346 OOBNotes::new_with_invite(
2347 notes,
2348 &self.client_ctx.get_invite_code().await,
2349 )
2350 } else {
2351 OOBNotes::new(federation_id_prefix, notes)
2352 };
2353
2354 self.client_ctx
2355 .add_state_machines_dbtx(
2356 dbtx,
2357 self.client_ctx.map_dyn(states).collect(),
2358 )
2359 .await?;
2360 self.client_ctx
2361 .add_operation_log_entry_dbtx(
2362 dbtx,
2363 operation_id,
2364 MintCommonInit::KIND.as_str(),
2365 MintOperationMeta {
2366 variant: MintOperationMetaVariant::SpendOOB {
2367 requested_amount,
2368 oob_notes: oob_notes.clone(),
2369 no_timeout,
2370 },
2371 amount: oob_notes.total_amount(),
2372 extra_meta,
2373 },
2374 )
2375 .await;
2376 self.client_ctx
2377 .log_event(
2378 dbtx,
2379 OOBNotesSpent {
2380 requested_amount,
2381 spent_amount: oob_notes.total_amount(),
2382 timeout: try_cancel_after,
2383 include_invite,
2384 },
2385 )
2386 .await;
2387
2388 self.client_ctx
2389 .log_event(
2390 dbtx,
2391 SendPaymentEvent {
2392 operation_id,
2393 amount: oob_notes.total_amount(),
2394 oob_notes: encode_prefixed(FEDIMINT_PREFIX, &oob_notes),
2395 },
2396 )
2397 .await;
2398
2399 Ok::<_, SpendOOBError>((operation_id, oob_notes))
2400 })
2401 },
2402 Some(100),
2403 )
2404 .await
2405 .map_err(|e| match e {
2406 AutocommitError::ClosureError { error, .. } => error,
2407 AutocommitError::CommitFailed { last_error, .. } => {
2408 SpendOOBError::Database(last_error)
2409 }
2410 })
2411 }
2412
2413 pub async fn send_oob_notes<M: Serialize + Send>(
2440 &self,
2441 amount: Amount,
2442 extra_meta: M,
2443 ) -> Result<OOBNotes, SendOOBNotesError> {
2444 let amount = self.cfg.fee_consensus.round_up(amount);
2445
2446 let extra_meta = serde_json::to_value(extra_meta)
2447 .expect("MintClientModule::send_oob_notes extra_meta is serializable");
2448
2449 let oob_notes: Option<OOBNotes> = self
2451 .client_ctx
2452 .module_db()
2453 .autocommit(
2454 |dbtx, _| {
2455 let extra_meta = extra_meta.clone();
2456 Box::pin(async {
2457 Ok::<Option<OOBNotes>, SendOOBNotesError>(
2458 self.try_spend_exact_notes_dbtx(
2459 dbtx,
2460 amount,
2461 self.federation_id,
2462 extra_meta,
2463 )
2464 .await,
2465 )
2466 })
2467 },
2468 Some(100),
2469 )
2470 .await
2471 .map_err(|e| match e {
2472 AutocommitError::ClosureError { error, .. } => error,
2473 AutocommitError::CommitFailed { last_error, .. } => {
2474 SendOOBNotesError::Database(last_error)
2475 }
2476 })?;
2477
2478 if let Some(oob_notes) = oob_notes {
2479 return Ok(oob_notes);
2480 }
2481
2482 self.client_ctx.global_api().session_count().await?;
2484
2485 let operation_id = OperationId::new_random();
2486
2487 let output_bundle = self
2494 .client_ctx
2495 .module_db()
2496 .autocommit(
2497 |dbtx, _| {
2498 Box::pin(async {
2499 Ok::<_, SendOOBNotesError>(
2500 self.create_exact_output(dbtx, operation_id, amount).await,
2501 )
2502 })
2503 },
2504 Some(100),
2505 )
2506 .await
2507 .map_err(|e| match e {
2508 AutocommitError::ClosureError { error, .. } => error,
2509 AutocommitError::CommitFailed { last_error, .. } => {
2510 SendOOBNotesError::Database(last_error)
2511 }
2512 })?;
2513
2514 let explicit_output_count = output_bundle.outputs().len() as u64;
2520
2521 let combined_bundle = ClientOutputBundle::new(
2523 output_bundle.outputs().to_vec(),
2524 output_bundle.sms().to_vec(),
2525 );
2526
2527 let outputs = self.client_ctx.make_client_outputs(combined_bundle);
2528
2529 let em_clone = extra_meta.clone();
2530
2531 let out_point_range = self
2533 .client_ctx
2534 .finalize_and_submit_transaction(
2535 operation_id,
2536 MintCommonInit::KIND.as_str(),
2537 move |change_range: OutPointRange| MintOperationMeta {
2538 variant: MintOperationMetaVariant::Reissuance {
2539 legacy_out_point: None,
2540 txid: Some(change_range.txid()),
2541 out_point_indices: change_range
2542 .into_iter()
2543 .map(|out_point| out_point.out_idx)
2544 .collect(),
2545 },
2546 amount,
2547 extra_meta: em_clone.clone(),
2548 },
2549 TransactionBuilder::new().with_outputs(outputs),
2550 )
2551 .await?;
2552
2553 let txid = out_point_range.txid();
2560 let total_output_count = explicit_output_count + out_point_range.count() as u64;
2561 let all_outputs = OutPointRange::new(txid, IdxRange::from(0..total_output_count));
2562 self.client_ctx
2563 .await_primary_module_outputs(operation_id, all_outputs.into_iter().collect())
2564 .await?;
2565
2566 Box::pin(self.send_oob_notes(amount, extra_meta)).await
2568 }
2569
2570 async fn try_spend_exact_notes_dbtx(
2573 &self,
2574 dbtx: &mut DatabaseTransaction<'_>,
2575 amount: Amount,
2576 federation_id: FederationId,
2577 extra_meta: serde_json::Value,
2578 ) -> Option<OOBNotes> {
2579 let selected_notes = Self::select_notes(
2580 dbtx,
2581 &SelectNotesWithExactAmount,
2582 amount,
2583 FeeConsensus::zero(),
2584 )
2585 .await
2586 .ok()?;
2587
2588 for (note_amount, note) in selected_notes.iter_items() {
2590 MintClientModule::delete_spendable_note(&self.client_ctx, dbtx, note_amount, note)
2591 .await;
2592 }
2593
2594 let sender = self.balance_update_sender.clone();
2595 dbtx.on_commit(move || sender.send_replace(()));
2596
2597 let operation_id = spendable_notes_to_operation_id(&selected_notes);
2598
2599 let oob_notes = OOBNotes::new(federation_id.to_prefix(), selected_notes);
2600
2601 self.client_ctx
2603 .add_operation_log_entry_dbtx(
2604 dbtx,
2605 operation_id,
2606 MintCommonInit::KIND.as_str(),
2607 MintOperationMeta {
2608 variant: MintOperationMetaVariant::SpendOOB {
2609 requested_amount: amount,
2610 oob_notes: oob_notes.clone(),
2611 no_timeout: true,
2612 },
2613 amount: oob_notes.total_amount(),
2614 extra_meta,
2615 },
2616 )
2617 .await;
2618
2619 self.client_ctx
2620 .log_event(
2621 dbtx,
2622 SendPaymentEvent {
2623 operation_id,
2624 amount: oob_notes.total_amount(),
2625 oob_notes: encode_prefixed(FEDIMINT_PREFIX, &oob_notes),
2626 },
2627 )
2628 .await;
2629
2630 Some(oob_notes)
2631 }
2632
2633 pub fn validate_notes(&self, oob_notes: &OOBNotes) -> Result<Amount, ValidateNotesError> {
2639 let federation_id_prefix = oob_notes.federation_id_prefix();
2640 let notes = oob_notes.notes().clone();
2641
2642 let expected = self.federation_id.to_prefix();
2643 if federation_id_prefix != expected {
2644 return Err(ValidateNotesError::WrongFederationId {
2645 expected,
2646 found: federation_id_prefix,
2647 });
2648 }
2649
2650 let tbs_pks = &self.cfg.tbs_pks;
2651
2652 for (index, (amt, snote)) in notes.iter_items().enumerate() {
2653 let key = tbs_pks
2654 .get(amt)
2655 .ok_or(ValidateNotesError::InvalidAmountTier { index, amount: amt })?;
2656
2657 let note = snote.note();
2658 if !note.verify(*key) {
2659 return Err(ValidateNotesError::InvalidSignature { index });
2660 }
2661
2662 let expected_nonce = Nonce(snote.spend_key.public_key());
2663 if note.nonce != expected_nonce {
2664 return Err(ValidateNotesError::WrongSpendKey { index });
2665 }
2666 }
2667
2668 Ok(notes.total_amount())
2669 }
2670
2671 pub async fn check_note_spent(&self, oob_notes: &OOBNotes) -> FederationResult<bool> {
2677 use crate::api::MintFederationApi;
2678
2679 let api_client = self.client_ctx.module_api();
2680 let any_spent = try_join_all(oob_notes.notes().iter().flat_map(|(_, notes)| {
2681 notes
2682 .iter()
2683 .map(|note| api_client.check_note_spent(note.nonce()))
2684 }))
2685 .await?
2686 .into_iter()
2687 .any(|spent| spent);
2688
2689 Ok(any_spent)
2690 }
2691
2692 pub async fn try_cancel_spend_notes(&self, operation_id: OperationId) {
2697 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
2698 dbtx.insert_entry(&CancelledOOBSpendKey(operation_id), &())
2699 .await;
2700 if let Err(e) = dbtx.commit_tx_result().await {
2701 warn!("We tried to cancel the same OOB spend multiple times concurrently: {e}");
2702 }
2703 }
2704
2705 pub async fn subscribe_spend_notes(
2708 &self,
2709 operation_id: OperationId,
2710 ) -> Result<UpdateStreamOrOutcome<SpendOOBState>, SubscribeSpendNotesError> {
2711 let operation = self.mint_operation(operation_id).await?;
2712 let MintOperationMetaVariant::SpendOOB { no_timeout, .. } =
2713 operation.meta::<MintOperationMeta>().variant
2714 else {
2715 return Err(SubscribeSpendNotesError::NotAnOutOfBandSpend);
2716 };
2717
2718 let client_ctx = self.client_ctx.clone();
2719
2720 Ok(self.client_ctx.outcome_or_updates(
2721 &operation,
2722 operation_id,
2723 |state| match state {
2724 SpendOOBState::Created | SpendOOBState::UserCanceledProcessing => false,
2725 SpendOOBState::UserCanceledSuccess
2726 | SpendOOBState::UserCanceledFailure
2727 | SpendOOBState::Success
2728 | SpendOOBState::Refunded => true,
2729 },
2730 move || {
2731 stream! {
2732 yield SpendOOBState::Created;
2733
2734 if no_timeout {
2735 yield SpendOOBState::Success;
2736 return;
2737 }
2738
2739 let self_ref = client_ctx.self_ref();
2740
2741 let refund = self_ref
2742 .await_spend_oob_refund(operation_id)
2743 .await;
2744
2745 if refund.user_triggered {
2746 yield SpendOOBState::UserCanceledProcessing;
2747 }
2748
2749 let mut success = true;
2750
2751 for txid in refund.transaction_ids {
2752 debug!(
2753 target: LOG_CLIENT_MODULE_MINT,
2754 %txid,
2755 operation_id=%operation_id.fmt_short(),
2756 "Waiting for oob refund txid"
2757 );
2758 if client_ctx
2759 .transaction_updates(operation_id)
2760 .await
2761 .await_tx_accepted(txid)
2762 .await.is_err() {
2763 success = false;
2764 }
2765 }
2766
2767 debug!(
2768 target: LOG_CLIENT_MODULE_MINT,
2769 operation_id=%operation_id.fmt_short(),
2770 %success,
2771 "Done waiting for all refund oob txids"
2772 );
2773
2774 match (refund.user_triggered, success) {
2775 (true, true) => {
2776 yield SpendOOBState::UserCanceledSuccess;
2777 },
2778 (true, false) => {
2779 yield SpendOOBState::UserCanceledFailure;
2780 },
2781 (false, true) => {
2782 yield SpendOOBState::Refunded;
2783 },
2784 (false, false) => {
2785 yield SpendOOBState::Success;
2786 }
2787 }
2788 }
2789 },
2790 ))
2791 }
2792
2793 async fn mint_operation(
2794 &self,
2795 operation_id: OperationId,
2796 ) -> Result<OperationLogEntry, OperationLookupError> {
2797 self.client_ctx.get_operation(operation_id).await
2798 }
2799
2800 async fn delete_spendable_note(
2801 client_ctx: &ClientContext<MintClientModule>,
2802 dbtx: &mut DatabaseTransaction<'_>,
2803 amount: Amount,
2804 note: &SpendableNote,
2805 ) {
2806 client_ctx
2807 .log_event(
2808 dbtx,
2809 NoteSpent {
2810 nonce: note.nonce(),
2811 },
2812 )
2813 .await;
2814 dbtx.remove_entry(&NoteKey {
2815 amount,
2816 nonce: note.nonce(),
2817 })
2818 .await
2819 .expect("Must deleted existing spendable note");
2820 }
2821
2822 pub async fn advance_note_idx(&self, amount: Amount) -> DerivableSecret {
2823 let db = self.client_ctx.module_db().clone();
2824
2825 db.autocommit(
2826 |dbtx, _| {
2827 Box::pin(async {
2828 Ok::<DerivableSecret, std::convert::Infallible>(
2829 self.new_note_secret(amount, dbtx).await,
2830 )
2831 })
2832 },
2833 None,
2834 )
2835 .await
2836 .expect("The commit is retried until it succeeds and the closure cannot fail")
2837 }
2838
2839 pub async fn reused_note_secrets(&self) -> Vec<(Amount, NoteIssuanceRequest, BlindNonce)> {
2842 self.client_ctx
2843 .module_db()
2844 .begin_transaction_nc()
2845 .await
2846 .get_value(&ReusedNoteIndices)
2847 .await
2848 .unwrap_or_default()
2849 .into_iter()
2850 .map(|(amount, note_idx)| {
2851 let secret = Self::new_note_secret_static(&self.secret, amount, note_idx);
2852 let (request, blind_nonce) =
2853 NoteIssuanceRequest::new(fedimint_core::secp256k1::SECP256K1, &secret);
2854 (amount, request, blind_nonce)
2855 })
2856 .collect()
2857 }
2858}
2859
2860pub fn spendable_notes_to_operation_id(
2861 spendable_selected_notes: &TieredMulti<SpendableNote>,
2862) -> OperationId {
2863 OperationId(
2864 spendable_selected_notes
2865 .consensus_hash::<sha256t::Hash<OOBSpendTag>>()
2866 .to_byte_array(),
2867 )
2868}
2869
2870#[derive(Debug, Serialize, Deserialize, Clone)]
2871pub struct SpendOOBRefund {
2872 pub user_triggered: bool,
2873 pub transaction_ids: Vec<TransactionId>,
2876}
2877
2878#[apply(async_trait_maybe_send!)]
2881pub trait NotesSelector<Note = SpendableNoteUndecoded>: Send + Sync {
2882 async fn select_notes(
2885 &self,
2886 #[cfg(not(target_family = "wasm"))] stream: impl futures::Stream<Item = (Amount, Note)> + Send,
2888 #[cfg(target_family = "wasm")] stream: impl futures::Stream<Item = (Amount, Note)>,
2889 requested_amount: Amount,
2890 fee_consensus: FeeConsensus,
2891 ) -> Result<TieredMulti<Note>, SelectNotesError>;
2892}
2893
2894pub struct SelectNotesWithAtleastAmount;
2900
2901#[apply(async_trait_maybe_send!)]
2902impl<Note: Send> NotesSelector<Note> for SelectNotesWithAtleastAmount {
2903 async fn select_notes(
2904 &self,
2905 #[cfg(not(target_family = "wasm"))] stream: impl futures::Stream<Item = (Amount, Note)> + Send,
2906 #[cfg(target_family = "wasm")] stream: impl futures::Stream<Item = (Amount, Note)>,
2907 requested_amount: Amount,
2908 fee_consensus: FeeConsensus,
2909 ) -> Result<TieredMulti<Note>, SelectNotesError> {
2910 Ok(select_notes_from_stream(stream, requested_amount, fee_consensus).await?)
2911 }
2912}
2913
2914pub struct SelectNotesWithExactAmount;
2918
2919#[apply(async_trait_maybe_send!)]
2920impl<Note: Send> NotesSelector<Note> for SelectNotesWithExactAmount {
2921 async fn select_notes(
2922 &self,
2923 #[cfg(not(target_family = "wasm"))] stream: impl futures::Stream<Item = (Amount, Note)> + Send,
2924 #[cfg(target_family = "wasm")] stream: impl futures::Stream<Item = (Amount, Note)>,
2925 requested_amount: Amount,
2926 fee_consensus: FeeConsensus,
2927 ) -> Result<TieredMulti<Note>, SelectNotesError> {
2928 let notes = select_notes_from_stream(stream, requested_amount, fee_consensus).await?;
2929
2930 let selected = notes.total_amount();
2931 if selected != requested_amount {
2932 return Err(SelectNotesError::NoExactAmount {
2933 requested: requested_amount,
2934 selected,
2935 });
2936 }
2937
2938 Ok(notes)
2939 }
2940}
2941
2942async fn select_notes_from_stream<Note>(
2948 stream: impl futures::Stream<Item = (Amount, Note)>,
2949 requested_amount: Amount,
2950 fee_consensus: FeeConsensus,
2951) -> Result<TieredMulti<Note>, InsufficientBalanceError> {
2952 if requested_amount == Amount::ZERO {
2953 return Ok(TieredMulti::default());
2954 }
2955 let mut stream = Box::pin(stream);
2956 let mut selected = vec![];
2957 let mut last_big_note_checkpoint: Option<(Amount, Note, usize)> = None;
2962 let mut pending_amount = requested_amount;
2963 let mut previous_amount: Option<Amount> = None; loop {
2965 if let Some((note_amount, note)) = stream.next().await {
2966 assert!(
2967 previous_amount.is_none_or(|previous| previous >= note_amount),
2968 "notes are not sorted in descending order"
2969 );
2970 previous_amount = Some(note_amount);
2971
2972 if note_amount <= fee_consensus.fee(note_amount) {
2973 continue;
2974 }
2975
2976 match note_amount.cmp(&(pending_amount + fee_consensus.fee(note_amount))) {
2977 Ordering::Less => {
2978 pending_amount += fee_consensus.fee(note_amount);
2980 pending_amount -= note_amount;
2981 selected.push((note_amount, note));
2982 }
2983 Ordering::Greater => {
2984 last_big_note_checkpoint = Some((note_amount, note, selected.len()));
2988 }
2989 Ordering::Equal => {
2990 selected.push((note_amount, note));
2992
2993 let notes: TieredMulti<Note> = selected.into_iter().collect();
2994
2995 assert!(
2996 notes.total_amount().msats
2997 >= requested_amount.msats
2998 + notes
2999 .iter()
3000 .map(|note| fee_consensus.fee(note.0))
3001 .sum::<Amount>()
3002 .msats
3003 );
3004
3005 return Ok(notes);
3006 }
3007 }
3008 } else {
3009 assert!(pending_amount > Amount::ZERO);
3010 if let Some((big_note_amount, big_note, checkpoint)) = last_big_note_checkpoint {
3011 selected.truncate(checkpoint);
3014 selected.push((big_note_amount, big_note));
3016
3017 let notes: TieredMulti<Note> = selected.into_iter().collect();
3018
3019 assert!(
3020 notes.total_amount().msats
3021 >= requested_amount.msats
3022 + notes
3023 .iter()
3024 .map(|note| fee_consensus.fee(note.0))
3025 .sum::<Amount>()
3026 .msats
3027 );
3028
3029 return Ok(notes);
3031 }
3032
3033 let total_amount = requested_amount.saturating_sub(pending_amount);
3034 return Err(InsufficientBalanceError {
3036 requested_amount,
3037 total_amount,
3038 });
3039 }
3040 }
3041}
3042
3043#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
3045enum MintRestoreStates {
3046 #[encodable_default]
3047 Default { variant: u64, bytes: Vec<u8> },
3048}
3049
3050#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
3052pub struct MintRestoreStateMachine {
3053 operation_id: OperationId,
3054 state: MintRestoreStates,
3055}
3056
3057#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
3058pub enum MintClientStateMachines {
3059 Output(MintOutputStateMachine),
3060 Input(MintInputStateMachine),
3061 OOB(MintOOBStateMachine),
3062 Restore(MintRestoreStateMachine),
3064}
3065
3066impl IntoDynInstance for MintClientStateMachines {
3067 type DynType = DynState;
3068
3069 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
3070 DynState::from_typed(instance_id, self)
3071 }
3072}
3073
3074impl State for MintClientStateMachines {
3075 type ModuleContext = MintClientContext;
3076
3077 fn transitions(
3078 &self,
3079 context: &Self::ModuleContext,
3080 global_context: &DynGlobalClientContext,
3081 ) -> Vec<StateTransition<Self>> {
3082 match self {
3083 MintClientStateMachines::Output(issuance_state) => {
3084 sm_enum_variant_translation!(
3085 issuance_state.transitions(context, global_context),
3086 MintClientStateMachines::Output
3087 )
3088 }
3089 MintClientStateMachines::Input(redemption_state) => {
3090 sm_enum_variant_translation!(
3091 redemption_state.transitions(context, global_context),
3092 MintClientStateMachines::Input
3093 )
3094 }
3095 MintClientStateMachines::OOB(oob_state) => {
3096 sm_enum_variant_translation!(
3097 oob_state.transitions(context, global_context),
3098 MintClientStateMachines::OOB
3099 )
3100 }
3101 MintClientStateMachines::Restore(_) => {
3102 sm_enum_variant_translation!(vec![], MintClientStateMachines::Restore)
3103 }
3104 }
3105 }
3106
3107 fn operation_id(&self) -> OperationId {
3108 match self {
3109 MintClientStateMachines::Output(issuance_state) => issuance_state.operation_id(),
3110 MintClientStateMachines::Input(redemption_state) => redemption_state.operation_id(),
3111 MintClientStateMachines::OOB(oob_state) => oob_state.operation_id(),
3112 MintClientStateMachines::Restore(r) => r.operation_id,
3113 }
3114 }
3115
3116 fn fmt_visualization(&self, f: &mut dyn std::fmt::Write, indent: &str) -> std::fmt::Result {
3117 match self {
3118 MintClientStateMachines::Output(s) => s.fmt_visualization(f, indent),
3119 MintClientStateMachines::Input(s) => s.fmt_visualization(f, indent),
3120 MintClientStateMachines::OOB(s) => s.fmt_visualization(f, indent),
3121 MintClientStateMachines::Restore(_) => write!(f, "{indent}{self:?}"),
3122 }
3123 }
3124}
3125
3126#[derive(Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, Encodable, Decodable)]
3129pub struct SpendableNote {
3130 pub signature: tbs::Signature,
3131 pub spend_key: Keypair,
3132}
3133
3134impl fmt::Debug for SpendableNote {
3135 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3136 f.debug_struct("SpendableNote")
3137 .field("nonce", &self.nonce())
3138 .field("signature", &self.signature)
3139 .field("spend_key", &self.spend_key)
3140 .finish()
3141 }
3142}
3143impl fmt::Display for SpendableNote {
3144 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3145 write!(f, "{}", self.nonce().fmt_short())
3146 }
3147}
3148
3149impl SpendableNote {
3150 pub fn nonce(&self) -> Nonce {
3151 Nonce(self.spend_key.public_key())
3152 }
3153
3154 fn note(&self) -> Note {
3155 Note {
3156 nonce: self.nonce(),
3157 signature: self.signature,
3158 }
3159 }
3160
3161 pub fn to_undecoded(&self) -> SpendableNoteUndecoded {
3162 SpendableNoteUndecoded {
3163 signature: self
3164 .signature
3165 .consensus_encode_to_vec()
3166 .try_into()
3167 .expect("Encoded size always correct"),
3168 spend_key: self.spend_key,
3169 }
3170 }
3171}
3172
3173#[derive(Clone, Copy, PartialEq, Eq, Hash, Encodable, Decodable, Serialize)]
3185pub struct SpendableNoteUndecoded {
3186 #[serde(serialize_with = "serdect::array::serialize_hex_lower_or_bin")]
3189 pub signature: [u8; 48],
3190 pub spend_key: Keypair,
3191}
3192
3193impl fmt::Display for SpendableNoteUndecoded {
3194 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3195 write!(f, "{}", self.nonce().fmt_short())
3196 }
3197}
3198
3199impl fmt::Debug for SpendableNoteUndecoded {
3200 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3201 f.debug_struct("SpendableNote")
3202 .field("nonce", &self.nonce())
3203 .field("signature", &"[raw]")
3204 .field("spend_key", &self.spend_key)
3205 .finish()
3206 }
3207}
3208
3209impl SpendableNoteUndecoded {
3210 fn nonce(&self) -> Nonce {
3211 Nonce(self.spend_key.public_key())
3212 }
3213
3214 pub fn decode(self) -> Result<SpendableNote, DecodeError> {
3215 Ok(SpendableNote {
3216 signature: Decodable::consensus_decode_partial_from_finite_reader(
3217 &mut self.signature.as_slice(),
3218 &ModuleRegistry::default(),
3219 )?,
3220 spend_key: self.spend_key,
3221 })
3222 }
3223}
3224
3225#[derive(
3231 Copy,
3232 Clone,
3233 Debug,
3234 Serialize,
3235 Deserialize,
3236 PartialEq,
3237 Eq,
3238 Encodable,
3239 Decodable,
3240 Default,
3241 PartialOrd,
3242 Ord,
3243)]
3244pub struct NoteIndex(u64);
3245
3246impl NoteIndex {
3247 pub fn next(self) -> Self {
3248 Self(self.0 + 1)
3249 }
3250
3251 fn prev(self) -> Option<Self> {
3252 self.0.checked_sub(0).map(Self)
3253 }
3254
3255 pub fn as_u64(self) -> u64 {
3256 self.0
3257 }
3258
3259 #[allow(unused)]
3263 pub fn from_u64(v: u64) -> Self {
3264 Self(v)
3265 }
3266
3267 pub fn advance(&mut self) {
3268 *self = self.next();
3269 }
3270}
3271
3272impl std::fmt::Display for NoteIndex {
3273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3274 self.0.fmt(f)
3275 }
3276}
3277
3278struct OOBSpendTag;
3279
3280impl sha256t::Tag for OOBSpendTag {
3281 fn engine() -> sha256::HashEngine {
3282 let mut engine = sha256::HashEngine::default();
3283 engine.input(b"oob-spend");
3284 engine
3285 }
3286}
3287
3288struct OOBReissueTag;
3289
3290impl sha256t::Tag for OOBReissueTag {
3291 fn engine() -> sha256::HashEngine {
3292 let mut engine = sha256::HashEngine::default();
3293 engine.input(b"oob-reissue");
3294 engine
3295 }
3296}
3297
3298pub fn represent_amount<K>(
3304 amount: Amount,
3305 current_denominations: &TieredCounts,
3306 tiers: &Tiered<K>,
3307 denomination_sets: u16,
3308 fee_consensus: &FeeConsensus,
3309) -> TieredCounts {
3310 let mut remaining_amount = amount;
3311 let mut denominations = TieredCounts::default();
3312
3313 for tier in tiers.tiers() {
3315 let notes = current_denominations.get(*tier);
3316 let missing_notes = u64::from(denomination_sets).saturating_sub(notes as u64);
3317 let possible_notes = remaining_amount / (*tier + fee_consensus.fee(*tier));
3318
3319 let add_notes = min(possible_notes, missing_notes);
3320 denominations.inc(*tier, add_notes as usize);
3321 remaining_amount -= (*tier + fee_consensus.fee(*tier)) * add_notes;
3322 }
3323
3324 for tier in tiers.tiers().rev() {
3326 let res = remaining_amount / (*tier + fee_consensus.fee(*tier));
3327 remaining_amount -= (*tier + fee_consensus.fee(*tier)) * res;
3328 denominations.inc(*tier, res as usize);
3329 }
3330
3331 let represented: u64 = denominations
3332 .iter()
3333 .map(|(k, v)| (k + fee_consensus.fee(k)).msats * (v as u64))
3334 .sum();
3335
3336 assert!(represented <= amount.msats);
3337 assert!(represented + fee_consensus.fee(Amount::from_msats(1)).msats >= amount.msats);
3338
3339 denominations
3340}
3341
3342pub(crate) fn create_bundle_for_inputs(
3343 inputs_and_notes: Vec<(ClientInput<MintInput>, SpendableNote)>,
3344 operation_id: OperationId,
3345) -> ClientInputBundle<MintInput, MintClientStateMachines> {
3346 let mut inputs = Vec::new();
3347 let mut input_states = Vec::new();
3348
3349 for (input, spendable_note) in inputs_and_notes {
3350 input_states.push((input.amounts.clone(), spendable_note));
3351 inputs.push(input);
3352 }
3353
3354 let input_sm = Arc::new(move |out_point_range: OutPointRange| {
3355 debug_assert_eq!(out_point_range.into_iter().count(), input_states.len());
3356
3357 vec![MintClientStateMachines::Input(MintInputStateMachine {
3358 common: MintInputCommon {
3359 operation_id,
3360 out_point_range,
3361 },
3362 state: MintInputStates::CreatedBundle(MintInputStateCreatedBundle {
3363 notes: input_states
3364 .iter()
3365 .map(|(amounts, note)| (amounts.expect_only_bitcoin(), *note))
3366 .collect(),
3367 }),
3368 })]
3369 });
3370
3371 ClientInputBundle::new(
3372 inputs,
3373 vec![ClientInputSM {
3374 state_machines: input_sm,
3375 }],
3376 )
3377}
3378
3379#[cfg(test)]
3380mod tests;