1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7#![allow(clippy::return_self_not_must_use)]
8#![allow(clippy::too_many_lines)]
9
10pub use fedimint_mintv2_common as common;
11
12mod api;
13#[cfg(feature = "cli")]
14mod cli;
15mod client_db;
16mod ecash;
17mod events;
18mod input;
19pub mod issuance;
20mod output;
21mod receive;
22
23use std::collections::{BTreeMap, BTreeSet};
24use std::convert::Infallible;
25use std::sync::{Arc, RwLock};
26use std::time::Duration;
27
28use anyhow::{Context as _, anyhow};
29use bitcoin_hashes::sha256;
30use client_db::{RecoveryState, RecoveryStateKey, SpendableNoteAmountPrefix, SpendableNotePrefix};
31pub use events::*;
32use fedimint_api_client::api::DynModuleApi;
33use fedimint_client::module::ClientModule;
34use fedimint_client::transaction::{
35 ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
36 ClientOutputSM, FeeQuote, FeeQuoteRequest, TransactionBuilder,
37};
38use fedimint_client_module::db::ClientModuleMigrationFn;
39use fedimint_client_module::module::init::{
40 ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs,
41};
42use fedimint_client_module::module::recovery::{NoModuleBackup, RecoveryProgress};
43use fedimint_client_module::module::{
44 ClientContext, OutPointRange, PrimaryModulePriority, PrimaryModuleSupport,
45};
46use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
47use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
48use fedimint_core::base32::{self, FEDIMINT_PREFIX};
49use fedimint_core::config::FederationId;
50use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
51use fedimint_core::db::{DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped};
52use fedimint_core::encoding::{Decodable, Encodable};
53use fedimint_core::module::{
54 AmountUnit, Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
55};
56use fedimint_core::secp256k1::rand::{Rng, thread_rng};
57use fedimint_core::secp256k1::{Keypair, PublicKey};
58use fedimint_core::util::{BoxStream, NextOrPending};
59use fedimint_core::{Amount, OutPoint, PeerId, apply, async_trait_maybe_send};
60use fedimint_derive_secret::DerivableSecret;
61use fedimint_mintv2_common::config::{FeeConsensus, MintClientConfig, client_denominations};
62use fedimint_mintv2_common::{
63 Denomination, KIND, MintCommonInit, MintInput, MintModuleTypes, MintOutput, Note, RecoveryItem,
64};
65use futures::{StreamExt, pin_mut};
66use itertools::Itertools;
67use rand::seq::IteratorRandom;
68use serde::{Deserialize, Serialize};
69use serde_json::Value;
70use tbs::AggregatePublicKey;
71use thiserror::Error;
72
73use crate::api::MintV2ModuleApi;
74use crate::client_db::SpendableNoteKey;
75pub use crate::ecash::ECash;
76use crate::input::{InputSMCommon, InputSMState, InputStateMachine};
77use crate::issuance::NoteIssuanceRequest;
78use crate::output::{MintOutputStateMachine, OutputSMCommon, OutputSMState};
79use crate::receive::{ReceiveSMState, ReceiveStateMachine};
80
81const TARGET_PER_DENOMINATION: usize = 3;
82const SLICE_SIZE: u64 = 10000;
83const PARALLEL_HASH_REQUESTS: usize = 10;
84const PARALLEL_SLICE_REQUESTS: usize = 10;
85
86#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable)]
87pub struct SpendableNote {
88 pub denomination: Denomination,
89 pub keypair: Keypair,
90 pub signature: tbs::Signature,
91}
92
93impl SpendableNote {
94 pub fn amount(&self) -> Amount {
95 self.denomination.amount()
96 }
97}
98
99impl SpendableNote {
100 fn nonce(&self) -> PublicKey {
101 self.keypair.public_key()
102 }
103
104 fn note(&self) -> Note {
105 Note {
106 denomination: self.denomination,
107 nonce: self.nonce(),
108 signature: self.signature,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub enum MintOperationMeta {
115 Send {
116 ecash: String,
117 custom_meta: Value,
118 },
119 Reissue {
120 change_outpoint_range: OutPointRange,
121 amount: Amount,
122 custom_meta: Value,
123 },
124 Receive {
125 change_outpoint_range: OutPointRange,
126 ecash: String,
127 custom_meta: Value,
128 },
129}
130
131#[derive(Debug, Clone)]
132pub struct MintClientInit;
133
134impl ModuleInit for MintClientInit {
135 type Common = MintCommonInit;
136
137 async fn dump_database(
138 &self,
139 _dbtx: &mut DatabaseTransaction<'_>,
140 _prefix_names: Vec<String>,
141 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
142 Box::new(BTreeMap::new().into_iter())
143 }
144}
145
146#[apply(async_trait_maybe_send!)]
147impl ClientModuleInit for MintClientInit {
148 type Module = MintClientModule;
149
150 fn supported_api_versions(&self) -> MultiApiVersion {
151 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 1 }])
152 .expect("no version conflicts")
153 }
154
155 async fn recover(
156 &self,
157 args: &ClientModuleRecoverArgs<Self>,
158 _snapshot: Option<&NoModuleBackup>,
159 ) -> anyhow::Result<Option<Amount>> {
160 let mut state = if let Some(state) = args
161 .db()
162 .begin_transaction_nc()
163 .await
164 .get_value(&RecoveryStateKey)
165 .await
166 {
167 state
168 } else {
169 RecoveryState {
170 next_index: 0,
171 total_items: args.module_api().fetch_recovery_count().await?,
172 requests: BTreeMap::new(),
173 nonces: BTreeSet::new(),
174 }
175 };
176
177 if state.next_index == state.total_items {
178 return Ok(None);
179 }
180
181 let peer_selector = PeerSelector::new(args.api().all_peers().clone());
182
183 let mut recovery_stream = futures::stream::iter(
184 (state.next_index..state.total_items).step_by(SLICE_SIZE as usize),
185 )
186 .map(|start| {
187 let api = args.module_api().clone();
188 let end = std::cmp::min(start + SLICE_SIZE, state.total_items);
189
190 async move { (start, end, api.fetch_recovery_slice_hash(start, end).await) }
191 })
192 .buffered(PARALLEL_HASH_REQUESTS)
193 .map(|(start, end, hash)| {
194 download_slice_with_hash(
195 args.module_api().clone(),
196 peer_selector.clone(),
197 start,
198 end,
199 hash,
200 )
201 })
202 .buffered(PARALLEL_SLICE_REQUESTS);
203
204 let tweak_filter = issuance::tweak_filter(args.module_root_secret());
205
206 loop {
207 let items = recovery_stream
208 .next()
209 .await
210 .context("Recovery stream finished before recovery is complete")?;
211
212 for item in &items {
213 match item {
214 RecoveryItem::Output {
215 denomination,
216 nonce_hash,
217 tweak,
218 } => {
219 if !issuance::check_tweak(*tweak, tweak_filter) {
220 continue;
221 }
222 let output_secret = issuance::output_secret(
223 *denomination,
224 *tweak,
225 args.module_root_secret(),
226 );
227
228 if !issuance::check_nonce(&output_secret, *nonce_hash) {
229 continue;
230 }
231
232 let computed_nonce_hash = issuance::nonce(&output_secret).consensus_hash();
233
234 if !state.nonces.insert(computed_nonce_hash) {
236 continue;
237 }
238
239 state.requests.insert(
240 computed_nonce_hash,
241 NoteIssuanceRequest::new(
242 *denomination,
243 *tweak,
244 args.module_root_secret(),
245 ),
246 );
247 }
248 RecoveryItem::Input { nonce_hash } => {
249 state.requests.remove(nonce_hash);
250 state.nonces.remove(nonce_hash);
251 }
252 }
253 }
254
255 state.next_index += items.len() as u64;
256
257 let mut dbtx = args.db().begin_transaction().await;
258
259 dbtx.insert_entry(&RecoveryStateKey, &state).await;
260
261 if state.next_index == state.total_items {
262 let recovered_amount = state
264 .requests
265 .values()
266 .map(|request| request.denomination.amount())
267 .sum::<Amount>();
268
269 let state_machines = args
270 .context()
271 .map_dyn(vec![MintClientStateMachines::Output(
272 MintOutputStateMachine {
273 common: OutputSMCommon {
274 operation_id: OperationId::new_random(),
275 range: None,
276 issuance_requests: state.requests.into_values().collect(),
277 },
278 state: OutputSMState::Pending,
279 },
280 )])
281 .collect();
282
283 args.context()
284 .add_state_machines_dbtx(&mut dbtx.to_ref_nc(), state_machines)
285 .await
286 .expect("state machine is valid");
287
288 dbtx.commit_tx().await;
289
290 return Ok(Some(recovered_amount));
291 }
292
293 dbtx.commit_tx().await;
294
295 args.update_recovery_progress(RecoveryProgress {
296 complete: state.next_index.try_into().unwrap_or(u32::MAX),
297 total: state.total_items.try_into().unwrap_or(u32::MAX),
298 });
299 }
300 }
301
302 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
303 let (tweak_sender, tweak_receiver) = async_channel::bounded(50);
304
305 let filter = issuance::tweak_filter(args.module_root_secret());
306
307 fedimint_core::task::spawn("mintv2-tweak-grinder", async move {
314 loop {
315 let tweak: [u8; 16] = thread_rng().r#gen();
316
317 if !issuance::check_tweak(tweak, filter) {
318 continue;
319 }
320
321 if tweak_sender.send(tweak).await.is_err() {
322 return;
323 }
324
325 fedimint_core::task::sleep(Duration::ZERO).await;
326 }
327 });
328
329 Ok(MintClientModule {
330 federation_id: *args.federation_id(),
331 cfg: args.cfg().clone(),
332 root_secret: args.module_root_secret().clone(),
333 notifier: args.notifier().clone(),
334 client_ctx: args.context(),
335 balance_update_sender: tokio::sync::watch::channel(()).0,
336 tweak_receiver,
337 })
338 }
339
340 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
341 BTreeMap::new()
342 }
343}
344
345#[derive(Debug)]
346pub struct MintClientModule {
347 federation_id: FederationId,
348 cfg: MintClientConfig,
349 root_secret: DerivableSecret,
350 notifier: ModuleNotifier<MintClientStateMachines>,
351 client_ctx: ClientContext<Self>,
352 balance_update_sender: tokio::sync::watch::Sender<()>,
353 tweak_receiver: async_channel::Receiver<[u8; 16]>,
354}
355
356#[derive(Debug, Clone)]
357pub struct MintClientContext {
358 client_ctx: ClientContext<MintClientModule>,
359 tbs_agg_pks: BTreeMap<Denomination, AggregatePublicKey>,
360 tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, tbs::PublicKeyShare>>,
361 pub balance_update_sender: tokio::sync::watch::Sender<()>,
362}
363
364impl Context for MintClientContext {
365 const KIND: Option<ModuleKind> = Some(KIND);
366}
367
368#[apply(async_trait_maybe_send!)]
369impl ClientModule for MintClientModule {
370 type Init = MintClientInit;
371 type Common = MintModuleTypes;
372 type Backup = NoModuleBackup;
373 type ModuleStateMachineContext = MintClientContext;
374 type States = MintClientStateMachines;
375
376 fn context(&self) -> Self::ModuleStateMachineContext {
377 MintClientContext {
378 client_ctx: self.client_ctx.clone(),
379 tbs_agg_pks: self.cfg.tbs_agg_pks.clone(),
380 tbs_pks: self.cfg.tbs_pks.clone(),
381 balance_update_sender: self.balance_update_sender.clone(),
382 }
383 }
384
385 fn input_fee(
386 &self,
387 amounts: &Amounts,
388 _input: &<Self::Common as ModuleCommon>::Input,
389 ) -> Option<Amounts> {
390 let unit = self.cfg.amount_unit;
391 let amount = amounts.get(&unit).copied().unwrap_or_default();
392 let fee = self.cfg.fee_consensus.fee(amount);
393
394 Some(Amounts::new_custom(unit, fee))
395 }
396
397 fn output_fee(
398 &self,
399 amounts: &Amounts,
400 _output: &<Self::Common as ModuleCommon>::Output,
401 ) -> Option<Amounts> {
402 let unit = self.cfg.amount_unit;
403 let amount = amounts.get(&unit).copied().unwrap_or_default();
404 let fee = self.cfg.fee_consensus.fee(amount);
405
406 Some(Amounts::new_custom(unit, fee))
407 }
408
409 #[cfg(feature = "cli")]
410 async fn handle_cli_command(
411 &self,
412 args: &[std::ffi::OsString],
413 ) -> anyhow::Result<serde_json::Value> {
414 cli::handle_cli_command(self, args).await
415 }
416
417 fn supports_being_primary(&self) -> PrimaryModuleSupport {
418 PrimaryModuleSupport::selected(PrimaryModulePriority::HIGH, [self.cfg.amount_unit])
419 }
420
421 async fn create_final_inputs_and_outputs(
422 &self,
423 dbtx: &mut DatabaseTransaction<'_>,
424 operation_id: OperationId,
425 unit: AmountUnit,
426 mut input_amount: Amount,
427 mut output_amount: Amount,
428 ) -> anyhow::Result<(
429 ClientInputBundle<MintInput, MintClientStateMachines>,
430 ClientOutputBundle<MintOutput, MintClientStateMachines>,
431 )> {
432 if unit != self.cfg.amount_unit {
433 anyhow::bail!("Module can only handle its configured amount unit");
434 }
435
436 let funding_notes = self
437 .select_funding_input(dbtx, output_amount.saturating_sub(input_amount))
438 .await
439 .context("Insufficient funds")?;
440
441 for note in &funding_notes {
442 self.remove_spendable_note(dbtx, note).await;
443 }
444
445 input_amount += funding_notes.iter().map(SpendableNote::amount).sum();
446
447 output_amount += funding_notes
448 .iter()
449 .map(|input| self.cfg.fee_consensus.fee(input.amount()))
450 .sum();
451
452 assert!(output_amount <= input_amount);
453
454 let (input_notes, output_amounts) = self
455 .rebalance(dbtx, &self.cfg.fee_consensus, input_amount - output_amount)
456 .await;
457
458 for note in &input_notes {
459 self.remove_spendable_note(dbtx, note).await;
460 }
461
462 input_amount += input_notes.iter().map(SpendableNote::amount).sum();
463
464 output_amount += input_notes
465 .iter()
466 .map(|note| self.cfg.fee_consensus.fee(note.amount()))
467 .sum();
468
469 output_amount += output_amounts
470 .iter()
471 .map(|denomination| {
472 denomination.amount() + self.cfg.fee_consensus.fee(denomination.amount())
473 })
474 .sum();
475
476 assert!(output_amount <= input_amount);
477
478 let mut spendable_notes = funding_notes
479 .into_iter()
480 .chain(input_notes)
481 .collect::<Vec<SpendableNote>>();
482
483 spendable_notes.sort_by_key(|note| note.denomination);
485
486 let input_bundle =
487 Self::create_input_bundle(operation_id, spendable_notes, false, self.cfg.amount_unit);
488
489 let mut denominations = represent_amount_with_fees(
490 input_amount.saturating_sub(output_amount),
491 &self.cfg.fee_consensus,
492 )
493 .into_iter()
494 .chain(output_amounts)
495 .collect::<Vec<Denomination>>();
496
497 denominations.sort();
499
500 let output_bundle = self.create_output_bundle(operation_id, denominations).await;
501
502 let sender = self.balance_update_sender.clone();
503 dbtx.on_commit(move || sender.send_replace(()));
504
505 Ok((input_bundle, output_bundle))
506 }
507
508 async fn await_primary_module_output(
509 &self,
510 operation_id: OperationId,
511 outpoint: OutPoint,
512 ) -> anyhow::Result<()> {
513 self.await_output_sm_success(operation_id, outpoint).await
514 }
515
516 async fn get_balance(&self, dbtx: &mut DatabaseTransaction<'_>, unit: AmountUnit) -> Amount {
517 if unit != self.cfg.amount_unit {
518 return Amount::ZERO;
519 }
520
521 self.get_count_by_denomination_dbtx(dbtx)
522 .await
523 .into_iter()
524 .map(|(denomination, count)| denomination.amount().mul_u64(count))
525 .sum()
526 }
527
528 async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
529 Box::pin(tokio_stream::wrappers::WatchStream::new(
530 self.balance_update_sender.subscribe(),
531 ))
532 }
533}
534
535impl MintClientModule {
536 async fn select_funding_input(
537 &self,
538 dbtx: &mut DatabaseTransaction<'_>,
539 mut excess_output: Amount,
540 ) -> Option<Vec<SpendableNote>> {
541 let mut selected_notes = Vec::new();
542 let mut target_notes = Vec::new();
543 let mut excess_notes = Vec::new();
544
545 for amount in client_denominations().rev() {
546 let notes_amount = dbtx
547 .find_by_prefix(&SpendableNoteAmountPrefix(amount))
548 .await
549 .map(|entry| entry.0.0)
550 .collect::<Vec<SpendableNote>>()
551 .await;
552
553 target_notes.extend(notes_amount.iter().take(TARGET_PER_DENOMINATION).cloned());
554
555 if notes_amount.len() > 2 * TARGET_PER_DENOMINATION {
556 for note in notes_amount.into_iter().skip(TARGET_PER_DENOMINATION) {
557 let note_fee = self.cfg.fee_consensus.fee(note.amount());
558
559 let note_value = note
560 .amount()
561 .checked_sub(note_fee)
562 .expect("All our notes are economical");
563
564 excess_output = excess_output.saturating_sub(note_value);
565
566 selected_notes.push(note);
567 }
568 } else {
569 excess_notes.extend(notes_amount.into_iter().skip(TARGET_PER_DENOMINATION));
570 }
571 }
572
573 if excess_output == Amount::ZERO {
574 return Some(selected_notes);
575 }
576
577 for note in excess_notes.into_iter().chain(target_notes) {
578 let note_amount = note.amount();
579 let note_value = note_amount
580 .checked_sub(self.cfg.fee_consensus.fee(note_amount))
581 .expect("All our notes are economical");
582
583 excess_output = excess_output.saturating_sub(note_value);
584
585 selected_notes.push(note);
586
587 if excess_output == Amount::ZERO {
588 return Some(selected_notes);
589 }
590 }
591
592 None
593 }
594
595 async fn rebalance(
596 &self,
597 dbtx: &mut DatabaseTransaction<'_>,
598 fee: &FeeConsensus,
599 mut excess_input: Amount,
600 ) -> (Vec<SpendableNote>, Vec<Denomination>) {
601 let n_denominations = self.get_count_by_denomination_dbtx(dbtx).await;
602
603 let mut notes = dbtx
604 .find_by_prefix_sorted_descending(&SpendableNotePrefix)
605 .await
606 .map(|entry| entry.0.0)
607 .fuse();
608
609 let mut input_notes = Vec::new();
610 let mut output_denominations = Vec::new();
611
612 for d in client_denominations() {
613 let n_denomination = n_denominations.get(&d).copied().unwrap_or(0);
614
615 let n_missing = TARGET_PER_DENOMINATION.saturating_sub(n_denomination as usize);
616
617 for _ in 0..n_missing {
618 match excess_input.checked_sub(d.amount() + fee.fee(d.amount())) {
619 Some(remaining_excess) => excess_input = remaining_excess,
620 None => match notes.next().await {
621 Some(note) => {
622 if note.amount() <= d.amount() + fee.fee(d.amount()) {
623 break;
624 }
625
626 excess_input += note.amount() - (d.amount() + fee.fee(d.amount()));
627
628 input_notes.push(note);
629 }
630 None => break,
631 },
632 }
633
634 output_denominations.push(d);
635 }
636 }
637
638 (input_notes, output_denominations)
639 }
640
641 fn create_input_bundle(
642 operation_id: OperationId,
643 notes: Vec<SpendableNote>,
644 include_receive_sm: bool,
645 amount_unit: AmountUnit,
646 ) -> ClientInputBundle<MintInput, MintClientStateMachines> {
647 let inputs = notes
648 .iter()
649 .map(|spendable_note| ClientInput {
650 input: MintInput::new_v0(spendable_note.note()),
651 keys: vec![spendable_note.keypair],
652 amounts: Amounts::new_custom(amount_unit, spendable_note.amount()),
653 })
654 .collect();
655
656 let input_sms = vec![ClientInputSM {
657 state_machines: Arc::new(move |range: OutPointRange| {
658 let mut sms = vec![MintClientStateMachines::Input(InputStateMachine {
659 common: InputSMCommon {
660 operation_id,
661 txid: range.txid(),
662 spendable_notes: notes.clone(),
663 },
664 state: InputSMState::Pending,
665 })];
666
667 if include_receive_sm {
668 sms.push(MintClientStateMachines::Receive(ReceiveStateMachine {
669 common: crate::receive::ReceiveSMCommon {
670 operation_id,
671 txid: range.txid(),
672 },
673 state: crate::receive::ReceiveSMState::Pending,
674 }));
675 }
676
677 sms
678 }),
679 }];
680
681 ClientInputBundle::new(inputs, input_sms)
682 }
683
684 async fn create_output_bundle(
685 &self,
686 operation_id: OperationId,
687 requested_denominations: Vec<Denomination>,
688 ) -> ClientOutputBundle<MintOutput, MintClientStateMachines> {
689 let issuance_requests = futures::stream::iter(requested_denominations)
690 .zip(self.tweak_receiver.clone())
691 .map(|(d, tweak)| NoteIssuanceRequest::new(d, tweak, &self.root_secret))
692 .collect::<Vec<NoteIssuanceRequest>>()
693 .await;
694
695 let amount_unit = self.cfg.amount_unit;
696 let outputs = issuance_requests
697 .iter()
698 .map(|request| ClientOutput {
699 output: request.output(),
700 amounts: Amounts::new_custom(amount_unit, request.denomination.amount()),
701 })
702 .collect();
703
704 let output_sms = vec![ClientOutputSM {
705 state_machines: Arc::new(move |range: OutPointRange| {
706 vec![MintClientStateMachines::Output(MintOutputStateMachine {
707 common: OutputSMCommon {
708 operation_id,
709 range: Some(range),
710 issuance_requests: issuance_requests.clone(),
711 },
712 state: OutputSMState::Pending,
713 })]
714 }),
715 }];
716
717 ClientOutputBundle::new(outputs, output_sms)
718 }
719
720 async fn await_output_sm_success(
721 &self,
722 operation_id: OperationId,
723 outpoint: OutPoint,
724 ) -> anyhow::Result<()> {
725 let stream = self
726 .notifier
727 .subscribe(operation_id)
728 .await
729 .filter_map(|state| async {
730 let MintClientStateMachines::Output(state) = state else {
731 return None;
732 };
733
734 if !state.common.range?.into_iter().contains(&outpoint) {
735 return None;
736 }
737
738 match state.state {
739 OutputSMState::Pending => None,
740 OutputSMState::Success => Some(Ok(())),
741 OutputSMState::Aborted => Some(Err(anyhow!("Transaction was rejected"))),
742 OutputSMState::Failure => Some(Err(anyhow!("Failed to finalize notes",))),
743 }
744 });
745
746 pin_mut!(stream);
747
748 stream.next_or_pending().await
749 }
750
751 pub async fn get_count_by_denomination(&self) -> BTreeMap<Denomination, u64> {
753 self.get_count_by_denomination_dbtx(
754 &mut self.client_ctx.module_db().begin_transaction_nc().await,
755 )
756 .await
757 }
758
759 async fn get_count_by_denomination_dbtx(
760 &self,
761 dbtx: &mut DatabaseTransaction<'_>,
762 ) -> BTreeMap<Denomination, u64> {
763 dbtx.find_by_prefix(&SpendableNotePrefix)
764 .await
765 .fold(BTreeMap::new(), |mut acc, entry| async move {
766 acc.entry(entry.0.0.denomination)
767 .and_modify(|count| *count += 1)
768 .or_insert(1);
769
770 acc
771 })
772 .await
773 }
774
775 pub async fn send(
789 &self,
790 amount: Amount,
791 custom_meta: Value,
792 include_invite: bool,
793 ) -> Result<(OperationId, ECash), SendECashError> {
794 let amount = round_to_multiple(amount, client_denominations().next().unwrap().amount());
795
796 if let Some((operation_id, ecash)) = self
797 .client_ctx
798 .module_db()
799 .autocommit(
800 |dbtx, _| {
801 Box::pin(self.send_ecash_dbtx(
802 dbtx,
803 amount,
804 custom_meta.clone(),
805 include_invite,
806 ))
807 },
808 Some(100),
809 )
810 .await
811 .expect("Failed to commit dbtx after 100 retries")
812 {
813 return Ok((operation_id, ecash));
814 }
815
816 self.client_ctx
817 .global_api()
818 .session_count()
819 .await
820 .map_err(|_| SendECashError::Offline)?;
821
822 let operation_id = OperationId::new_random();
823
824 let output = self
825 .create_output_bundle(operation_id, represent_amount(amount))
826 .await;
827 let output = self.client_ctx.make_client_outputs(output);
828 let cm = custom_meta.clone();
829
830 let range = self
831 .client_ctx
832 .finalize_and_submit_transaction(
833 operation_id,
834 MintCommonInit::KIND.as_str(),
835 move |change_outpoint_range| MintOperationMeta::Reissue {
836 change_outpoint_range,
837 amount,
838 custom_meta: cm.clone(),
839 },
840 TransactionBuilder::new().with_outputs(output),
841 )
842 .await
843 .map_err(|_| SendECashError::InsufficientBalance)?;
844
845 for outpoint in range {
846 self.await_output_sm_success(operation_id, outpoint)
847 .await
848 .map_err(|_| SendECashError::Failure)?;
849 }
850
851 Box::pin(self.send(amount, custom_meta, include_invite)).await
852 }
853
854 async fn send_ecash_dbtx(
855 &self,
856 dbtx: &mut DatabaseTransaction<'_>,
857 remaining_amount: Amount,
858 custom_meta: Value,
859 include_invite: bool,
860 ) -> Result<Option<(OperationId, ECash)>, Infallible> {
861 let Some(notes) = Self::select_exact_change(&mut dbtx.to_ref_nc(), remaining_amount).await
862 else {
863 return Ok(None);
864 };
865
866 for spendable_note in ¬es {
867 self.remove_spendable_note(dbtx, spendable_note).await;
868 }
869
870 let ecash = if include_invite {
871 let invite = self.client_ctx.get_invite_code().await;
872 ECash::new_with_invite(notes, &invite)
873 } else {
874 ECash::new(self.federation_id, notes)
875 };
876 let amount = ecash.amount();
877 let operation_id = OperationId::new_random();
878
879 self.client_ctx
880 .add_operation_log_entry_dbtx(
881 dbtx,
882 operation_id,
883 MintCommonInit::KIND.as_str(),
884 MintOperationMeta::Send {
885 ecash: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
886 custom_meta,
887 },
888 )
889 .await;
890
891 self.client_ctx
892 .log_event(
893 dbtx,
894 SendPaymentEvent {
895 operation_id,
896 amount,
897 ecash: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
898 },
899 )
900 .await;
901
902 let sender = self.balance_update_sender.clone();
903 dbtx.on_commit(move || sender.send_replace(()));
904
905 Ok(Some((operation_id, ecash)))
906 }
907
908 pub async fn receive(
911 &self,
912 ecash: ECash,
913 custom_meta: Value,
914 ) -> Result<OperationId, ReceiveECashError> {
915 let operation_id = OperationId::from_encodable(&ecash);
916
917 if self.client_ctx.operation_exists(operation_id).await {
918 return Ok(operation_id);
919 }
920
921 if ecash.mint() != Some(self.federation_id) {
922 return Err(ReceiveECashError::WrongFederation);
923 }
924
925 if ecash
926 .notes()
927 .iter()
928 .any(|note| note.amount() <= self.cfg.fee_consensus.base_fee())
929 {
930 return Err(ReceiveECashError::UneconomicalDenomination);
931 }
932
933 let input =
934 Self::create_input_bundle(operation_id, ecash.notes(), true, self.cfg.amount_unit);
935 let input = self.client_ctx.make_client_inputs(input);
936 let ec = base32::encode_prefixed(FEDIMINT_PREFIX, &ecash);
937
938 self.client_ctx
939 .finalize_and_submit_transaction(
940 operation_id,
941 MintCommonInit::KIND.as_str(),
942 move |change_outpoint_range| MintOperationMeta::Receive {
943 change_outpoint_range,
944 ecash: ec.clone(),
945 custom_meta: custom_meta.clone(),
946 },
947 TransactionBuilder::new().with_inputs(input),
948 )
949 .await
950 .map_err(|_| ReceiveECashError::InsufficientFunds)?;
951
952 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
953
954 self.client_ctx
955 .log_event(
956 &mut dbtx,
957 ReceivePaymentEvent {
958 operation_id,
959 amount: ecash.amount(),
960 },
961 )
962 .await;
963
964 dbtx.commit_tx().await;
965
966 Ok(operation_id)
967 }
968
969 pub async fn receive_fee_quote(&self, ecash: &ECash) -> anyhow::Result<FeeQuote> {
979 let notes = ecash.notes();
983 let input_amount: Amount = notes.iter().map(SpendableNote::amount).sum();
984 let input_fee: Amount = notes
985 .iter()
986 .map(|note| self.cfg.fee_consensus.fee(note.amount()))
987 .sum();
988
989 self.client_ctx
990 .fee_quote(
991 OperationId::new_random(),
992 FeeQuoteRequest {
993 input_amount: Amounts::new_custom(self.cfg.amount_unit, input_amount),
994 output_amount: Amounts::ZERO,
995 input_fee: Amounts::new_custom(self.cfg.amount_unit, input_fee),
996 output_fee: Amounts::ZERO,
997 },
998 )
999 .await
1000 }
1001
1002 pub async fn send_fee_quote(&self, amount: Amount) -> anyhow::Result<FeeQuote> {
1016 let amount = round_to_multiple(amount, client_denominations().next().unwrap().amount());
1017
1018 if self.can_make_exact_change(amount).await {
1020 return Ok(FeeQuote::ZERO);
1021 }
1022
1023 let denominations = represent_amount(amount);
1027 let output_amount: Amount = denominations.iter().map(|d| d.amount()).sum();
1028 let output_fee: Amount = denominations
1029 .iter()
1030 .map(|d| self.cfg.fee_consensus.fee(d.amount()))
1031 .sum();
1032
1033 self.client_ctx
1034 .fee_quote(
1035 OperationId::new_random(),
1036 FeeQuoteRequest {
1037 input_amount: Amounts::ZERO,
1038 output_amount: Amounts::new_custom(self.cfg.amount_unit, output_amount),
1039 input_fee: Amounts::ZERO,
1040 output_fee: Amounts::new_custom(self.cfg.amount_unit, output_fee),
1041 },
1042 )
1043 .await
1044 }
1045
1046 async fn can_make_exact_change(&self, remaining_amount: Amount) -> bool {
1051 let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1052
1053 Self::select_exact_change(&mut dbtx, remaining_amount)
1054 .await
1055 .is_some()
1056 }
1057
1058 async fn select_exact_change(
1064 dbtx: &mut DatabaseTransaction<'_>,
1065 mut remaining_amount: Amount,
1066 ) -> Option<Vec<SpendableNote>> {
1067 let mut stream = dbtx
1068 .find_by_prefix_sorted_descending(&SpendableNotePrefix)
1069 .await
1070 .map(|entry| entry.0.0);
1071
1072 let mut notes = vec![];
1073
1074 while let Some(spendable_note) = stream.next().await {
1075 remaining_amount = match remaining_amount.checked_sub(spendable_note.amount()) {
1076 Some(amount) => amount,
1077 None => continue,
1078 };
1079
1080 notes.push(spendable_note);
1081
1082 if remaining_amount == Amount::ZERO {
1083 break;
1084 }
1085 }
1086
1087 (remaining_amount == Amount::ZERO).then_some(notes)
1088 }
1089
1090 pub async fn await_final_receive_operation_state(
1092 &self,
1093 operation_id: OperationId,
1094 ) -> anyhow::Result<FinalReceiveOperationState> {
1095 let operation = self.client_ctx.get_operation(operation_id).await?;
1096 let mut stream = self.notifier.subscribe(operation_id).await;
1097
1098 let mut stream = self
1099 .client_ctx
1100 .outcome_or_updates(operation, operation_id, move || {
1101 async_stream::stream! {
1102 loop {
1103 if let Some(MintClientStateMachines::Receive(state)) = stream.next().await {
1104 match state.state {
1105 ReceiveSMState::Pending => {}
1106 ReceiveSMState::Success => {
1107 yield FinalReceiveOperationState::Success;
1108 return;
1109 }
1110 ReceiveSMState::Rejected(..) => {
1111 yield FinalReceiveOperationState::Rejected;
1112 return;
1113 }
1114 }
1115 }
1116 }
1117 }
1118 })
1119 .into_stream();
1120
1121 let mut final_state = None;
1122
1123 while let Some(state) = stream.next().await {
1124 final_state = Some(state);
1125 }
1126
1127 Ok(final_state.expect("Stream contains one final state"))
1128 }
1129
1130 async fn remove_spendable_note(
1131 &self,
1132 dbtx: &mut DatabaseTransaction<'_>,
1133 spendable_note: &SpendableNote,
1134 ) {
1135 dbtx.remove_entry(&SpendableNoteKey(spendable_note.clone()))
1136 .await
1137 .expect("Must delete existing spendable note");
1138 }
1139}
1140
1141#[derive(Clone)]
1142struct PeerSelector {
1143 latency: Arc<RwLock<BTreeMap<PeerId, Duration>>>,
1144}
1145
1146impl PeerSelector {
1147 fn new(peers: BTreeSet<PeerId>) -> Self {
1148 let latency = peers
1149 .into_iter()
1150 .map(|peer| (peer, Duration::ZERO))
1151 .collect();
1152
1153 Self {
1154 latency: Arc::new(RwLock::new(latency)),
1155 }
1156 }
1157
1158 fn choose_peer(&self) -> PeerId {
1160 let latency = self.latency.read().unwrap();
1161
1162 let peer_a = latency.iter().choose(&mut thread_rng()).unwrap();
1163 let peer_b = latency.iter().choose(&mut thread_rng()).unwrap();
1164
1165 if peer_a.1 <= peer_b.1 {
1166 *peer_a.0
1167 } else {
1168 *peer_b.0
1169 }
1170 }
1171
1172 fn report(&self, peer: PeerId, duration: Duration) {
1174 self.latency
1175 .write()
1176 .unwrap()
1177 .entry(peer)
1178 .and_modify(|latency| *latency = *latency * 9 / 10 + duration * 1 / 10)
1179 .or_insert(duration);
1180 }
1181
1182 fn remove(&self, peer: PeerId) {
1183 self.latency.write().unwrap().remove(&peer);
1184 }
1185}
1186
1187async fn download_slice_with_hash(
1189 module_api: DynModuleApi,
1190 peer_selector: PeerSelector,
1191 start: u64,
1192 end: u64,
1193 expected_hash: sha256::Hash,
1194) -> Vec<RecoveryItem> {
1195 const TIMEOUT: Duration = Duration::from_secs(30);
1196
1197 loop {
1198 let peer = peer_selector.choose_peer();
1199 let start_time = fedimint_core::time::now();
1200
1201 if let Ok(data) = module_api
1202 .fetch_recovery_slice(peer, TIMEOUT, start, end)
1203 .await
1204 {
1205 let elapsed = fedimint_core::time::now()
1206 .duration_since(start_time)
1207 .unwrap_or_default();
1208
1209 peer_selector.report(peer, elapsed);
1210
1211 if data.consensus_hash::<sha256::Hash>() == expected_hash {
1212 return data;
1213 }
1214
1215 peer_selector.remove(peer);
1216 } else {
1217 peer_selector.report(peer, TIMEOUT);
1218 }
1219 }
1220}
1221
1222#[derive(Error, Debug, Clone, Eq, PartialEq)]
1223pub enum SendECashError {
1224 #[error("We need to reissue notes but the client is offline")]
1225 Offline,
1226 #[error("The clients balance is insufficient")]
1227 InsufficientBalance,
1228 #[error("A non-recoverable error has occurred")]
1229 Failure,
1230}
1231
1232#[derive(Error, Debug, Clone, Eq, PartialEq)]
1233pub enum ReceiveECashError {
1234 #[error("The ECash is from a different federation")]
1235 WrongFederation,
1236 #[error("ECash contains an uneconomical denomination")]
1237 UneconomicalDenomination,
1238 #[error("Receiving ecash requires additional funds")]
1239 InsufficientFunds,
1240}
1241
1242#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1243pub enum FinalReceiveOperationState {
1244 Success,
1246 Rejected,
1248}
1249
1250#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1251pub enum MintClientStateMachines {
1252 Input(InputStateMachine),
1253 Output(MintOutputStateMachine),
1254 Receive(ReceiveStateMachine),
1255}
1256
1257impl IntoDynInstance for MintClientStateMachines {
1258 type DynType = DynState;
1259
1260 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1261 DynState::from_typed(instance_id, self)
1262 }
1263}
1264
1265impl State for MintClientStateMachines {
1266 type ModuleContext = MintClientContext;
1267
1268 fn transitions(
1269 &self,
1270 context: &Self::ModuleContext,
1271 global_context: &DynGlobalClientContext,
1272 ) -> Vec<StateTransition<Self>> {
1273 match self {
1274 MintClientStateMachines::Input(redemption_state) => {
1275 sm_enum_variant_translation!(
1276 redemption_state.transitions(context, global_context),
1277 MintClientStateMachines::Input
1278 )
1279 }
1280 MintClientStateMachines::Output(issuance_state) => {
1281 sm_enum_variant_translation!(
1282 issuance_state.transitions(context, global_context),
1283 MintClientStateMachines::Output
1284 )
1285 }
1286 MintClientStateMachines::Receive(receive_state) => {
1287 sm_enum_variant_translation!(
1288 receive_state.transitions(context, global_context),
1289 MintClientStateMachines::Receive
1290 )
1291 }
1292 }
1293 }
1294
1295 fn operation_id(&self) -> OperationId {
1296 match self {
1297 MintClientStateMachines::Input(redemption_state) => redemption_state.operation_id(),
1298 MintClientStateMachines::Output(issuance_state) => issuance_state.operation_id(),
1299 MintClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
1300 }
1301 }
1302}
1303
1304fn round_to_multiple(amount: Amount, min_denomiation: Amount) -> Amount {
1305 Amount::from_msats(amount.msats.next_multiple_of(min_denomiation.msats))
1306}
1307
1308fn represent_amount_with_fees(
1309 mut remaining_amount: Amount,
1310 fee_consensus: &FeeConsensus,
1311) -> Vec<Denomination> {
1312 let mut denominations = Vec::new();
1313
1314 for denomination in client_denominations().rev() {
1316 let n_add =
1317 remaining_amount / (denomination.amount() + fee_consensus.fee(denomination.amount()));
1318
1319 denominations.extend(std::iter::repeat_n(denomination, n_add as usize));
1320
1321 remaining_amount -=
1322 n_add * (denomination.amount() + fee_consensus.fee(denomination.amount()));
1323 }
1324
1325 denominations.sort();
1327
1328 denominations
1329}
1330
1331fn represent_amount(mut remaining_amount: Amount) -> Vec<Denomination> {
1332 let mut denominations = Vec::new();
1333
1334 for denomination in client_denominations().rev() {
1336 let n_add = remaining_amount / denomination.amount();
1337
1338 denominations.extend(std::iter::repeat_n(denomination, n_add as usize));
1339
1340 remaining_amount -= n_add * denomination.amount();
1341 }
1342
1343 denominations
1344}