mint_module_tests/
mint-module-tests.rs

1use anyhow::{Context as _, Result};
2use clap::Parser;
3use devimint::cmd;
4use devimint::federation::Federation;
5use fedimint_logging::LOG_DEVIMINT;
6use rand::Rng;
7use tracing::info;
8
9#[derive(Debug, Parser)]
10enum Cmd {
11    Restore,
12    Sanity,
13}
14
15#[tokio::main]
16async fn main() -> anyhow::Result<()> {
17    match Cmd::parse() {
18        Cmd::Restore => restore().await,
19        Cmd::Sanity => sanity().await,
20    }
21}
22
23async fn restore() -> anyhow::Result<()> {
24    devimint::run_devfed_test()
25        .call(|fed, _process_mgr| async move {
26            let fed = fed.fed().await?;
27
28            test_restore_gap_test(fed).await?;
29            Ok(())
30        })
31        .await
32}
33
34pub async fn test_restore_gap_test(fed: &Federation) -> Result<()> {
35    let client = fed.new_joined_client("restore-gap-test").await?;
36    const PEGIN_SATS: u64 = 300000;
37    fed.pegin_client(PEGIN_SATS, &client).await?;
38
39    for i in 0..20 {
40        let gap = rand::thread_rng().gen_range(0..20);
41        info!(target: LOG_DEVIMINT, gap, "Gap");
42        cmd!(
43            client,
44            "dev",
45            "advance-note-idx",
46            "--amount",
47            "1024msat",
48            "--count",
49            // we are not guarantted to use a 1024 note on every payment,
50            // so create some random small gaps, so it's very unlikely we
51            // would cross the default gap limit accidentally
52            &gap.to_string()
53        )
54        .run()
55        .await?;
56
57        let reissure_amount_sats = if i % 2 == 0 {
58            // half of the time, reissue everything
59            PEGIN_SATS
60        } else {
61            // other half, random amount
62            rand::thread_rng().gen_range(10..PEGIN_SATS)
63        };
64        info!(target: LOG_DEVIMINT, i, reissure_amount_sats, "Reissue");
65
66        let notes = cmd!(client, "spend", reissure_amount_sats * 1000)
67            .out_json()
68            .await?
69            .get("notes")
70            .expect("Output didn't contain e-cash notes")
71            .as_str()
72            .unwrap()
73            .to_owned();
74
75        // Test we can reissue our own notes
76        cmd!(client, "reissue", notes).out_json().await?;
77    }
78
79    let secret = cmd!(client, "print-secret").out_json().await?["secret"]
80        .as_str()
81        .map(ToOwned::to_owned)
82        .unwrap();
83
84    let pre_notes = cmd!(client, "info").out_json().await?;
85
86    let pre_balance = pre_notes["total_amount_msat"].as_u64().unwrap();
87
88    info!(target: LOG_DEVIMINT, %pre_notes, pre_balance, "State before backup");
89
90    // we need to have some funds
91    assert!(0 < pre_balance);
92
93    // without existing backup
94    {
95        let client =
96            devimint::federation::Client::create("restore-gap-test-without-backup").await?;
97        let _ = cmd!(
98            client,
99            "restore",
100            "--mnemonic",
101            &secret,
102            "--invite-code",
103            fed.invite_code()?
104        )
105        .out_json()
106        .await?;
107
108        let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
109        let post_notes = cmd!(client, "info").out_json().await?;
110        let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
111        info!(target: LOG_DEVIMINT, %post_notes, post_balance, "State after backup");
112        assert_eq!(pre_balance, post_balance);
113        assert_eq!(pre_notes, post_notes);
114    }
115
116    Ok(())
117}
118
119async fn sanity() -> anyhow::Result<()> {
120    devimint::run_devfed_test()
121        .call(|fed, _process_mgr| async move {
122            let fed = fed.fed().await?;
123
124            test_note_consoliation(fed).await?;
125            Ok(())
126        })
127        .await
128}
129
130/// Test note consolidation, which at the time of writing basically means that
131/// once client accumulates too many notes of certain denomination, any
132/// transaction building will include excessive notes as extra inputs, to
133/// consolidate them into higher denominations.
134///
135/// In the future we will probably change the whole thing and delete this thing.
136async fn test_note_consoliation(fed: &devimint::federation::Federation) -> anyhow::Result<()> {
137    let sender = fed.new_joined_client("sender").await?;
138    let receiver = fed.new_joined_client("receiver").await?;
139
140    let can_no_wait = cmd!(sender, "reissue", "--help")
141        .out_string()
142        .await?
143        .contains("no-wait");
144
145    if !can_no_wait {
146        info!("Version before `--no-wait` didn't have consolidation implemented");
147        return Ok(());
148    }
149    fed.pegin_client(10_000, &sender).await?;
150
151    let mut all_notes = vec![];
152    for i in 0..20 {
153        let info = cmd!(sender, "info").out_json().await?;
154        info!(%info, "sender info");
155        // remint sender notes from time to time to make sure it have 1msat notes
156        if i % 2 == 1 {
157            let notes = cmd!(sender, "spend", "1sat",).out_json().await?["notes"]
158                .as_str()
159                .context("invoice must be string")?
160                .to_owned();
161
162            cmd!(sender, "reissue", notes).run().await?;
163        }
164
165        let notes = cmd!(sender, "spend", "1msat",).out_json().await?["notes"]
166            .as_str()
167            .context("invoice must be string")?
168            .to_owned();
169
170        all_notes.push(notes);
171    }
172
173    for notes in &all_notes[..all_notes.len() - 1] {
174        cmd!(receiver, "reissue", "--no-wait", notes).run().await?;
175    }
176
177    // wait for all at the same time to make things go faster
178    cmd!(receiver, "dev", "wait-complete").run().await?;
179
180    // reissuance of last note will trigger consolidation
181    cmd!(receiver, "reissue")
182        .args(&all_notes[all_notes.len() - 1..])
183        .run()
184        .await?;
185
186    let info = cmd!(receiver, "info").out_json().await?;
187    info!(%info, "receiver info");
188    // receiver has the balance
189    assert_eq!(info["total_amount_msat"].as_i64().unwrap(), 20);
190    // without the consolidation, this would be 20 1msat notes
191    assert!(info["denominations_msat"]["1"].as_i64().unwrap() < 20);
192
193    Ok(())
194}