Skip to main content

fedimint_walletv2_devimint_tests/
tests.rs

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