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