fedimint_walletv2_devimint_tests/
tests.rs1use 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 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 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 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}