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!(client, "module", "walletv2", "info", "tx-chain")
119        .out_json()
120        .await?;
121
122    let chain: Vec<serde_json::Value> = serde_json::from_value(value)?;
123
124    ensure!(chain.len() == expected,);
125
126    Ok(())
127}
128
129async fn get_deposit_address(client: &Client) -> anyhow::Result<Address> {
130    let address = serde_json::from_value::<Address<NetworkUnchecked>>(
131        cmd!(client, "module", "walletv2", "receive")
132            .out_json()
133            .await?,
134    )?
135    .assume_checked();
136
137    Ok(address)
138}
139
140#[tokio::main]
141async fn main() -> anyhow::Result<()> {
142    // Enable walletv2 module instead of wallet v1
143    unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLETV2", "true") };
144    unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLET", "false") };
145
146    devimint::run_devfed_test()
147        .call(|dev_fed, _process_mgr| async move {
148            let fedimint_cli_version = util::FedimintCli::version_or_default().await;
149            let fedimintd_version = util::FedimintdCmd::version_or_default().await;
150
151            if fedimint_cli_version < *VERSION_0_11_0_ALPHA {
152                info!(%fedimint_cli_version, "Version did not support walletv2 module, skipping");
153                return Ok(());
154            }
155
156            if fedimintd_version < *VERSION_0_11_0_ALPHA {
157                info!(%fedimintd_version, "Version did not support walletv2 module, skipping");
158                return Ok(());
159            }
160
161            let (fed, bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
162
163            let client = fed
164                .new_joined_client("walletv2-test-send-and-receive-client")
165                .await?;
166
167            info!("Verify that walletv1 is not present...");
168
169            ensure!(
170                !module_is_present(&client, "wallet").await?,
171                "walletv1 module should not be present"
172            );
173
174            ensure!(
175                module_is_present(&client, "walletv2").await?,
176                "walletv2 module should be present"
177            );
178
179            // Spawn a background task that continuously mines blocks. This simulates
180            // real bitcoin block production and prevents deadlocks where pending
181            // federation bitcoin transactions block deposit claims via congestion
182            // control while no blocks are being mined to confirm them.
183            let block_miner = spawn_block_miner(bitcoind.clone());
184
185            // We need the consensus block count to reach a non-zero value before we send
186            // in any funds such that the UTXO is tracked by the federation.
187
188            info!("Wait for the consensus to reach block count one");
189
190            await_consensus_block_count(&client, 1).await?;
191
192            info!("Deposit funds into the federation...");
193
194            let federation_address_1 = get_deposit_address(&client).await?;
195
196            fed.bitcoind
197                .send_to(federation_address_1.to_string(), 100_000)
198                .await?;
199
200            fed.bitcoind
201                .send_to(federation_address_1.to_string(), 200_000)
202                .await?;
203
204            info!("Wait for deposits to be claimed...");
205
206            await_client_balance(&client, 290_000).await?;
207
208            ensure_federation_total_value(&client, 290_000).await?;
209
210            let federation_address_2 = get_deposit_address(&client).await?;
211
212            assert_ne!(federation_address_1, federation_address_2);
213
214            fed.bitcoind
215                .send_to(federation_address_2.to_string(), 300_000)
216                .await?;
217
218            fed.bitcoind
219                .send_to(federation_address_2.to_string(), 400_000)
220                .await?;
221
222            info!("Wait for deposits to be claimed...");
223
224            await_client_balance(&client, 980_000).await?;
225
226            ensure_federation_total_value(&client, 980_000).await?;
227
228            let federation_address_3 = get_deposit_address(&client).await?;
229
230            assert_ne!(federation_address_2, federation_address_3);
231
232            info!("Send funds back onchain...");
233
234            let withdraw_address = bitcoind.get_new_address().await?;
235
236            let value = cmd!(
237                client,
238                "module",
239                "walletv2",
240                "send",
241                withdraw_address,
242                "500000 sat"
243            )
244            .out_json()
245            .await?;
246
247            let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
248                panic!("Send operation failed");
249            };
250
251            bitcoind.poll_get_transaction(txid).await?;
252
253            let total_value: u64 = serde_json::from_value(
254                cmd!(client, "module", "walletv2", "info", "total-value")
255                    .out_json()
256                    .await?,
257            )?;
258
259            assert!(
260                total_value < 500_000,
261                "Federation total value should be less than 500_000 sats"
262            );
263
264            await_no_pending_txs(&client).await?;
265
266            ensure_tx_chain_length(&client, 4).await?;
267
268            info!("Verify that a send with zero fee aborts...");
269
270            let abort_address = bitcoind.get_new_address().await?;
271
272            let value = cmd!(
273                client,
274                "module",
275                "walletv2",
276                "send",
277                abort_address,
278                "100000 sat",
279                "--fee",
280                "0 sat"
281            )
282            .out_json()
283            .await?;
284
285            assert_eq!(
286                FinalSendState::Aborted,
287                serde_json::from_value(value)?,
288                "Send with zero fee should abort"
289            );
290
291            info!("Test circular deposit (send to second client's federation address)...");
292
293            let client_two = fed
294                .new_joined_client("walletv2-test-circular-deposit-client")
295                .await?;
296
297            let circular_address = get_deposit_address(&client_two).await?;
298
299            let value = cmd!(
300                client,
301                "module",
302                "walletv2",
303                "send",
304                circular_address.to_string(),
305                "100000 sat"
306            )
307            .out_json()
308            .await?;
309
310            let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
311                panic!("Circular deposit send operation failed");
312            };
313
314            bitcoind.poll_get_transaction(txid).await?;
315
316            await_client_balance(&client_two, 99_000).await?;
317
318            await_no_pending_txs(&client).await?;
319
320            ensure_tx_chain_length(&client, 6).await?;
321
322            block_miner.abort();
323
324            info!("Wallet V2 send and receive test successful");
325
326            Ok(())
327        })
328        .await
329}