mint_module_tests/
mint-module-tests.rs

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