fedimint_mint_client/
cli.rs1use std::collections::BTreeMap;
2use std::time::Duration;
3use std::{ffi, iter};
4
5use anyhow::bail;
6use clap::Parser;
7use fedimint_core::{Amount, TieredMulti};
8use futures::StreamExt;
9use serde::Serialize;
10use serde_json::json;
11use tracing::{info, warn};
12
13use crate::{
14 MintClientModule, OOBNotes, ReissueExternalNotesState, SelectNotesWithAtleastAmount,
15 SelectNotesWithExactAmount,
16};
17
18#[derive(Parser, Serialize)]
19enum Opts {
20 Reissue { notes: OOBNotes },
22 Spend {
24 amount: Amount,
26 #[clap(long)]
29 allow_overpay: bool,
30 #[clap(long, default_value_t = 60 * 60 * 24 * 7)]
33 timeout: u64,
34 #[clap(long)]
37 include_invite: bool,
38 },
39 Split { oob_notes: OOBNotes },
42 Combine {
44 #[clap(required = true)]
45 oob_notes: Vec<OOBNotes>,
46 },
47 Validate {
50 #[clap(long)]
53 online: bool,
54 oob_notes: OOBNotes,
56 },
57}
58
59async fn spend(
60 mint: &MintClientModule,
61 amount: Amount,
62 allow_overpay: bool,
63 timeout: u64,
64 include_invite: bool,
65) -> anyhow::Result<serde_json::Value> {
66 warn!(
67 "The client will try to double-spend these notes after the timeout to reclaim \
68 any unclaimed e-cash."
69 );
70
71 let timeout = Duration::from_secs(timeout);
72 let (operation, notes) = if allow_overpay {
73 let (operation, notes) = mint
74 .spend_notes_with_selector(
75 &SelectNotesWithAtleastAmount,
76 amount,
77 timeout,
78 include_invite,
79 (),
80 )
81 .await?;
82
83 let overspend_amount = notes.total_amount().saturating_sub(amount);
84 if overspend_amount != Amount::ZERO {
85 warn!("Selected notes {overspend_amount} worth more than requested");
86 }
87
88 (operation, notes)
89 } else {
90 mint.spend_notes_with_selector(
91 &SelectNotesWithExactAmount,
92 amount,
93 timeout,
94 include_invite,
95 (),
96 )
97 .await?
98 };
99 info!("Spend e-cash operation: {}", operation.fmt_short());
100
101 Ok(json!({ "notes": notes }))
102}
103
104fn split(oob_notes: &OOBNotes) -> serde_json::Value {
105 let federation = oob_notes.federation_id_prefix();
106 let notes = oob_notes
107 .notes()
108 .iter()
109 .map(|(amount, notes)| {
110 let notes = notes
111 .iter()
112 .map(|note| {
113 OOBNotes::new(
114 federation,
115 TieredMulti::new(vec![(amount, vec![*note])].into_iter().collect()),
116 )
117 })
118 .collect::<Vec<_>>();
119 (amount, notes)
120 })
121 .collect::<BTreeMap<_, _>>();
122
123 json!({ "notes": notes })
124}
125
126fn combine(oob_notes: &[OOBNotes]) -> anyhow::Result<serde_json::Value> {
127 let federation_id_prefix = {
128 let mut prefixes = oob_notes.iter().map(OOBNotes::federation_id_prefix);
129 let first = prefixes
130 .next()
131 .expect("At least one e-cash notes string expected");
132 for prefix in prefixes {
133 if prefix != first {
134 bail!("Trying to combine e-cash from different federations: {first} and {prefix}");
135 }
136 }
137 first
138 };
139
140 let combined_notes = oob_notes
141 .iter()
142 .flat_map(|notes| notes.notes().iter_items().map(|(amt, note)| (amt, *note)))
143 .collect();
144
145 let combined_oob_notes = OOBNotes::new(federation_id_prefix, combined_notes);
146
147 Ok(json!({ "notes": combined_oob_notes }))
148}
149
150pub(crate) async fn handle_cli_command(
151 mint: &MintClientModule,
152 args: &[ffi::OsString],
153) -> anyhow::Result<serde_json::Value> {
154 let opts = Opts::parse_from(iter::once(&ffi::OsString::from("mint")).chain(args.iter()));
155
156 match opts {
157 Opts::Reissue { notes } => {
158 let amount = notes.total_amount();
159
160 let operation_id = mint.reissue_external_notes(notes, ()).await?;
161
162 let mut updates = mint
163 .subscribe_reissue_external_notes(operation_id)
164 .await
165 .unwrap()
166 .into_stream();
167
168 while let Some(update) = updates.next().await {
169 if let ReissueExternalNotesState::Failed(e) = update {
170 bail!("Reissue failed: {e}");
171 }
172 }
173
174 Ok(serde_json::to_value(amount).expect("JSON serialization failed"))
175 }
176 Opts::Spend {
177 amount,
178 allow_overpay,
179 timeout,
180 include_invite,
181 } => spend(mint, amount, allow_overpay, timeout, include_invite).await,
182 Opts::Split { oob_notes } => Ok(split(&oob_notes)),
183 Opts::Combine { oob_notes } => combine(&oob_notes),
184 Opts::Validate { oob_notes, online } => {
185 let amount = mint.validate_notes(&oob_notes)?;
186
187 if online {
188 let any_spent = mint.check_note_spent(&oob_notes).await?;
189 Ok(json!({
190 "any_spent": any_spent,
191 "amount_msat": amount,
192 }))
193 } else {
194 Ok(json!({ "amount_msat": amount }))
195 }
196 }
197 }
198}