Skip to main content

fedimint_mint_client/
cli.rs

1use 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 out of band notes
21    Reissue { notes: OOBNotes },
22    /// Prepare notes to send to a third party as a payment
23    Spend {
24        /// The amount of e-cash to spend
25        amount: Amount,
26        /// If the exact amount cannot be represented, return e-cash of a higher
27        /// value instead of failing
28        #[clap(long)]
29        allow_overpay: bool,
30        /// After how many seconds we will try to reclaim the e-cash if it
31        /// hasn't been redeemed by the recipient. Defaults to one week.
32        #[clap(long, default_value_t = 60 * 60 * 24 * 7)]
33        timeout: u64,
34        /// If the necessary information to join the federation the e-cash
35        /// belongs to should be included in the serialized notes
36        #[clap(long)]
37        include_invite: bool,
38    },
39    /// Splits a string containing multiple e-cash notes (e.g. from the `spend`
40    /// command) into ones that contain exactly one.
41    Split { oob_notes: OOBNotes },
42    /// Combines two or more serialized e-cash notes strings
43    Combine {
44        #[clap(required = true)]
45        oob_notes: Vec<OOBNotes>,
46    },
47    /// Verifies the signatures of e-cash notes, if the online flag is specified
48    /// it also checks with the mint if the notes were already spent
49    Validate {
50        /// Whether to check with the mint if the notes were already spent
51        /// (CAUTION: this hurts privacy)
52        #[clap(long)]
53        online: bool,
54        /// E-Cash note to validate
55        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}