Skip to main content

fedimint_walletv2_devimint_tests/
tests.rs

1use std::time::Duration;
2
3use anyhow::ensure;
4use bitcoin::address::NetworkUnchecked;
5use bitcoin::{Address, Txid};
6use devimint::external::Bitcoind;
7use devimint::federation::Client;
8use devimint::version_constants::VERSION_0_11_0_ALPHA;
9use devimint::{cmd, util};
10use fedimint_core::runtime::sleep;
11use fedimint_core::task::sleep_in_test;
12use serde::Deserialize;
13use tokio::task::JoinHandle;
14use tokio::try_join;
15use tracing::info;
16
17/// Spawns a background task that mines a block every 100ms, simulating
18/// continuous block production. This prevents deadlocks where the federation's
19/// pending bitcoin transactions block further progress because no blocks are
20/// being mined to confirm them.
21fn spawn_block_miner(bitcoind: Bitcoind) -> JoinHandle<()> {
22    fedimint_core::runtime::spawn("background-block-miner", async move {
23        loop {
24            if let Err(e) = bitcoind.mine_blocks(1).await {
25                tracing::warn!("Background block miner failed to mine block: {e}");
26            }
27
28            sleep(Duration::from_millis(100)).await;
29        }
30    })
31}
32
33async fn module_is_present(client: &Client, kind: &str) -> anyhow::Result<bool> {
34    let modules = cmd!(client, "module").out_json().await?;
35
36    let modules = modules["list"].as_array().expect("module list is an array");
37
38    Ok(modules.iter().any(|m| m["kind"].as_str() == Some(kind)))
39}
40
41#[derive(Debug, Deserialize, PartialEq, Eq)]
42enum FinalSendState {
43    Success(Txid),
44    Aborted,
45    Failure,
46}
47
48async fn await_consensus_block_count(client: &Client, block_count: u64) -> anyhow::Result<()> {
49    loop {
50        let value = cmd!(client, "module", "walletv2", "info", "block-count")
51            .out_json()
52            .await?;
53
54        if block_count <= serde_json::from_value(value)? {
55            return Ok(());
56        }
57
58        sleep_in_test(
59            format!("Waiting for consensus to reach block count {block_count}"),
60            Duration::from_secs(1),
61        )
62        .await;
63    }
64}
65
66async fn ensure_federation_total_value(client: &Client, min_value: u64) -> anyhow::Result<()> {
67    let value = cmd!(client, "module", "walletv2", "info", "total-value")
68        .out_json()
69        .await?;
70
71    ensure!(
72        min_value <= serde_json::from_value(value)?,
73        "Total federation total value is below {min_value}"
74    );
75
76    Ok(())
77}
78
79async fn await_client_balance(client: &Client, min_balance: u64) -> anyhow::Result<()> {
80    loop {
81        cmd!(client, "dev", "wait", "3").out_json().await?;
82
83        let balance = client.balance().await?;
84
85        // Client balance is in msats, min_balance is in sats
86        if balance >= min_balance * 1000 {
87            return Ok(());
88        }
89
90        info!("Waiting for client balance {balance} to reach {min_balance}");
91    }
92}
93
94async fn await_no_pending_txs(client: &Client) -> anyhow::Result<()> {
95    loop {
96        let value = cmd!(client, "module", "walletv2", "info", "pending-tx-chain")
97            .out_json()
98            .await?;
99
100        let pending: Vec<serde_json::Value> = serde_json::from_value(value)?;
101
102        if pending.is_empty() {
103            return Ok(());
104        }
105
106        sleep_in_test(
107            format!(
108                "Waiting for {} pending transactions to clear",
109                pending.len()
110            ),
111            Duration::from_secs(1),
112        )
113        .await;
114    }
115}
116
117async fn ensure_tx_chain_length(client: &Client, expected: usize) -> anyhow::Result<()> {
118    let value = cmd!(
119        client,
120        "module",
121        "walletv2",
122        "info",
123        "tx-chain",
124        expected.to_string()
125    )
126    .out_json()
127    .await?;
128
129    let chain: Vec<serde_json::Value> = serde_json::from_value(value)?;
130
131    ensure!(chain.len() == expected,);
132
133    Ok(())
134}
135
136async fn get_deposit_address(client: &Client) -> anyhow::Result<Address> {
137    let address = serde_json::from_value::<Address<NetworkUnchecked>>(
138        cmd!(client, "module", "walletv2", "receive")
139            .out_json()
140            .await?,
141    )?
142    .assume_checked();
143
144    Ok(address)
145}
146
147#[tokio::main]
148async fn main() -> anyhow::Result<()> {
149    // Enable walletv2 module instead of wallet v1
150    unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLETV2", "true") };
151    unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLET", "false") };
152
153    devimint::run_devfed_test()
154        .call(|dev_fed, _process_mgr| async move {
155            let fedimint_cli_version = util::FedimintCli::version_or_default().await;
156            let fedimintd_version = util::FedimintdCmd::version_or_default().await;
157
158            if fedimint_cli_version < *VERSION_0_11_0_ALPHA {
159                info!(%fedimint_cli_version, "Version did not support walletv2 module, skipping");
160                return Ok(());
161            }
162
163            if fedimintd_version < *VERSION_0_11_0_ALPHA {
164                info!(%fedimintd_version, "Version did not support walletv2 module, skipping");
165                return Ok(());
166            }
167
168            let (fed, bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
169
170            let client = fed
171                .new_joined_client("walletv2-test-send-and-receive-client")
172                .await?;
173
174            info!("Verify that walletv1 is not present...");
175
176            ensure!(
177                !module_is_present(&client, "wallet").await?,
178                "walletv1 module should not be present"
179            );
180
181            ensure!(
182                module_is_present(&client, "walletv2").await?,
183                "walletv2 module should be present"
184            );
185
186            // Spawn a background task that continuously mines blocks. This simulates
187            // real bitcoin block production and prevents deadlocks where pending
188            // federation bitcoin transactions block deposit claims via congestion
189            // control while no blocks are being mined to confirm them.
190            let block_miner = spawn_block_miner(bitcoind.clone());
191
192            // We need the consensus block count to reach a non-zero value before we send
193            // in any funds such that the UTXO is tracked by the federation.
194
195            info!("Wait for the consensus to reach block count one");
196
197            await_consensus_block_count(&client, 1).await?;
198
199            info!("Deposit funds into the federation...");
200
201            let federation_address_1 = get_deposit_address(&client).await?;
202
203            fed.bitcoind
204                .send_to(federation_address_1.to_string(), 100_000)
205                .await?;
206
207            fed.bitcoind
208                .send_to(federation_address_1.to_string(), 200_000)
209                .await?;
210
211            info!("Wait for deposits to be claimed...");
212
213            await_client_balance(&client, 290_000).await?;
214
215            ensure_federation_total_value(&client, 290_000).await?;
216
217            let federation_address_2 = get_deposit_address(&client).await?;
218
219            assert_ne!(federation_address_1, federation_address_2);
220
221            fed.bitcoind
222                .send_to(federation_address_2.to_string(), 300_000)
223                .await?;
224
225            fed.bitcoind
226                .send_to(federation_address_2.to_string(), 400_000)
227                .await?;
228
229            info!("Wait for deposits to be claimed...");
230
231            await_client_balance(&client, 980_000).await?;
232
233            ensure_federation_total_value(&client, 980_000).await?;
234
235            let federation_address_3 = get_deposit_address(&client).await?;
236
237            assert_ne!(federation_address_2, federation_address_3);
238
239            info!("Send funds back on-chain...");
240
241            let withdraw_address = bitcoind.get_new_address().await?;
242
243            let value = cmd!(
244                client,
245                "module",
246                "walletv2",
247                "send",
248                withdraw_address,
249                "500000 sat"
250            )
251            .out_json()
252            .await?;
253
254            let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
255                panic!("Send operation failed");
256            };
257
258            bitcoind.poll_get_transaction(txid).await?;
259
260            let total_value: u64 = serde_json::from_value(
261                cmd!(client, "module", "walletv2", "info", "total-value")
262                    .out_json()
263                    .await?,
264            )?;
265
266            assert!(
267                total_value < 500_000,
268                "Federation total value should be less than 500_000 sats"
269            );
270
271            await_no_pending_txs(&client).await?;
272
273            ensure_tx_chain_length(&client, 4).await?;
274
275            info!("Verify that a send with zero fee aborts...");
276
277            let abort_address = bitcoind.get_new_address().await?;
278
279            let value = cmd!(
280                client,
281                "module",
282                "walletv2",
283                "send",
284                abort_address,
285                "100000 sat",
286                "--fee",
287                "0 sat"
288            )
289            .out_json()
290            .await?;
291
292            assert_eq!(
293                FinalSendState::Aborted,
294                serde_json::from_value(value)?,
295                "Send with zero fee should abort"
296            );
297
298            info!("Test circular deposit (send to second client's federation address)...");
299
300            let client_two = fed
301                .new_joined_client("walletv2-test-circular-deposit-client")
302                .await?;
303
304            let circular_address = get_deposit_address(&client_two).await?;
305
306            let value = cmd!(
307                client,
308                "module",
309                "walletv2",
310                "send",
311                circular_address.to_string(),
312                "100000 sat"
313            )
314            .out_json()
315            .await?;
316
317            let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
318                panic!("Circular deposit send operation failed");
319            };
320
321            bitcoind.poll_get_transaction(txid).await?;
322
323            await_client_balance(&client_two, 99_000).await?;
324
325            await_no_pending_txs(&client).await?;
326
327            ensure_tx_chain_length(&client, 6).await?;
328
329            block_miner.abort();
330
331            info!("Wallet V2 send and receive test successful");
332
333            Ok(())
334        })
335        .await
336}