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::{
9 VERSION_0_11_0_ALPHA, VERSION_0_12_0_ALPHA, VERSION_0_13_0_ALPHA,
10};
11use devimint::{cmd, util};
12use fedimint_core::runtime::sleep;
13use fedimint_core::task::sleep_in_test;
14use fedimint_eventlog::EventLogId;
15use serde::Deserialize;
16use tokio::task::JoinHandle;
17use tokio::try_join;
18use tracing::info;
19
20fn spawn_block_miner(bitcoind: Bitcoind) -> JoinHandle<()> {
25 fedimint_core::runtime::spawn("background-block-miner", async move {
26 loop {
27 if let Err(e) = bitcoind.mine_blocks(1).await {
28 tracing::warn!("Background block miner failed to mine block: {e}");
29 }
30
31 sleep(Duration::from_millis(100)).await;
32 }
33 })
34}
35
36async fn module_is_present(client: &Client, kind: &str) -> anyhow::Result<bool> {
37 let modules = cmd!(client, "module").out_json().await?;
38
39 let modules = modules["list"].as_array().expect("module list is an array");
40
41 Ok(modules.iter().any(|m| m["kind"].as_str() == Some(kind)))
42}
43
44#[derive(Debug, Deserialize, PartialEq, Eq)]
45enum FinalSendState {
46 Success(Txid),
47 Aborted,
48 Failure,
49}
50
51async fn await_consensus_block_count(client: &Client, block_count: u64) -> anyhow::Result<()> {
52 loop {
53 let value = cmd!(client, "module", "walletv2", "info", "block-count")
54 .out_json()
55 .await?;
56
57 if block_count <= serde_json::from_value(value)? {
58 return Ok(());
59 }
60
61 sleep_in_test(
62 format!("Waiting for consensus to reach block count {block_count}"),
63 Duration::from_secs(1),
64 )
65 .await;
66 }
67}
68
69async fn ensure_federation_total_value(client: &Client, min_value: u64) -> anyhow::Result<()> {
70 let value = cmd!(client, "module", "walletv2", "info", "total-value")
71 .out_json()
72 .await?;
73
74 ensure!(
75 min_value <= serde_json::from_value(value)?,
76 "Total federation total value is below {min_value}"
77 );
78
79 Ok(())
80}
81
82async fn await_deposits(
89 client: &Client,
90 position: EventLogId,
91 receives: usize,
92 min_balance: u64,
93) -> anyhow::Result<()> {
94 if util::FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA {
95 let mut position = position;
96
97 for _ in 0..receives {
98 position = await_receive(client, position).await?;
99 }
100
101 ensure_client_balance(client, min_balance).await?;
102 } else {
103 await_client_balance(client, min_balance).await?;
104 }
105
106 Ok(())
107}
108
109async fn await_receive(client: &Client, position: EventLogId) -> anyhow::Result<EventLogId> {
112 let output = cmd!(
113 client,
114 "module",
115 "walletv2",
116 "await-receive",
117 position.to_string()
118 )
119 .out_json()
120 .await?;
121
122 serde_json::from_value(output[1].clone())
124 .context("await-receive should return the next event log position")
125}
126
127async fn ensure_client_balance(client: &Client, min_balance: u64) -> anyhow::Result<()> {
129 let balance = client.balance().await?;
130
131 ensure!(
133 balance >= min_balance * 1000,
134 "Client balance {balance} is below {min_balance}"
135 );
136
137 Ok(())
138}
139
140async fn await_client_balance(client: &Client, min_balance: u64) -> anyhow::Result<()> {
143 loop {
144 cmd!(client, "dev", "wait", "3").out_json().await?;
145
146 let balance = client.balance().await?;
147
148 if balance >= min_balance * 1000 {
150 return Ok(());
151 }
152
153 info!("Waiting for client balance {balance} to reach {min_balance}");
154 }
155}
156
157async fn await_no_pending_txs(client: &Client) -> anyhow::Result<()> {
158 loop {
159 let value = cmd!(client, "module", "walletv2", "info", "pending-tx-chain")
160 .out_json()
161 .await?;
162
163 let pending: Vec<serde_json::Value> = serde_json::from_value(value)?;
164
165 if pending.is_empty() {
166 return Ok(());
167 }
168
169 sleep_in_test(
170 format!(
171 "Waiting for {} pending transactions to clear",
172 pending.len()
173 ),
174 Duration::from_secs(1),
175 )
176 .await;
177 }
178}
179
180async fn ensure_tx_chain_length(client: &Client, expected: usize) -> anyhow::Result<()> {
181 let value = cmd!(client, "module", "walletv2", "info", "tx-chain")
182 .out_json()
183 .await?;
184
185 let chain: Vec<serde_json::Value> = serde_json::from_value(value)?;
186
187 ensure!(chain.len() == expected,);
188
189 Ok(())
190}
191
192async fn get_deposit_address(client: &Client) -> anyhow::Result<(Address, EventLogId)> {
193 if util::FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA {
194 let position =
197 serde_json::from_value(cmd!(client, "dev", "next-event-log-id").out_json().await?)
198 .context("dev next-event-log-id should return an event log position")?;
199
200 let address = serde_json::from_value::<Address<NetworkUnchecked>>(
201 cmd!(client, "module", "walletv2", "receive")
202 .out_json()
203 .await?,
204 )?
205 .assume_checked();
206
207 Ok((address, position))
208 } else {
209 let address = serde_json::from_value::<Address<NetworkUnchecked>>(
212 cmd!(client, "module", "walletv2", "receive")
213 .out_json()
214 .await?,
215 )?
216 .assume_checked();
217
218 Ok((address, EventLogId::LOG_START))
219 }
220}
221
222#[tokio::main]
223async fn main() -> anyhow::Result<()> {
224 unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLETV2", "true") };
226 unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLET", "false") };
227
228 devimint::run_devfed_test()
229 .call(|dev_fed, _process_mgr| async move {
230 let fedimint_cli_version = util::FedimintCli::version_or_default().await;
231 let fedimintd_version = util::FedimintdCmd::version_or_default().await;
232
233 if fedimint_cli_version < *VERSION_0_11_0_ALPHA {
234 info!(%fedimint_cli_version, "Version did not support walletv2 module, skipping");
235 return Ok(());
236 }
237
238 if fedimintd_version < *VERSION_0_11_0_ALPHA {
239 info!(%fedimintd_version, "Version did not support walletv2 module, skipping");
240 return Ok(());
241 }
242
243 let (fed, bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
244
245 let client = fed
246 .new_joined_client("walletv2-test-send-and-receive-client")
247 .await?;
248
249 info!("Verify that walletv1 is not present...");
250
251 ensure!(
252 !module_is_present(&client, "wallet").await?,
253 "walletv1 module should not be present"
254 );
255
256 ensure!(
257 module_is_present(&client, "walletv2").await?,
258 "walletv2 module should be present"
259 );
260
261 let block_miner = spawn_block_miner(bitcoind.clone());
266
267 info!("Wait for the consensus to reach block count one");
271
272 await_consensus_block_count(&client, 1).await?;
273
274 info!("Deposit funds into the federation...");
275
276 let (federation_address_1, position) = get_deposit_address(&client).await?;
277
278 fed.bitcoind
279 .send_to(federation_address_1.to_string(), 100_000)
280 .await?;
281
282 fed.bitcoind
283 .send_to(federation_address_1.to_string(), 200_000)
284 .await?;
285
286 info!("Wait for deposits to be claimed...");
287
288 await_deposits(&client, position, 2, 290_000).await?;
290
291 ensure_federation_total_value(&client, 290_000).await?;
292
293 let (federation_address_2, position) = get_deposit_address(&client).await?;
294
295 assert_ne!(federation_address_1, federation_address_2);
296
297 fed.bitcoind
298 .send_to(federation_address_2.to_string(), 300_000)
299 .await?;
300
301 fed.bitcoind
302 .send_to(federation_address_2.to_string(), 400_000)
303 .await?;
304
305 info!("Wait for deposits to be claimed...");
306
307 await_deposits(&client, position, 2, 980_000).await?;
308
309 ensure_federation_total_value(&client, 980_000).await?;
310
311 let (federation_address_3, _) = get_deposit_address(&client).await?;
312
313 assert_ne!(federation_address_2, federation_address_3);
314
315 info!("Send funds back onchain...");
316
317 let withdraw_address = bitcoind.get_new_address().await?;
318
319 let value = cmd!(
320 client,
321 "module",
322 "walletv2",
323 "send",
324 withdraw_address,
325 "500000 sat"
326 )
327 .out_json()
328 .await?;
329
330 let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
331 panic!("Send operation failed");
332 };
333
334 bitcoind.poll_get_transaction(txid).await?;
335
336 let total_value: u64 = serde_json::from_value(
337 cmd!(client, "module", "walletv2", "info", "total-value")
338 .out_json()
339 .await?,
340 )?;
341
342 assert!(
343 total_value < 500_000,
344 "Federation total value should be less than 500_000 sats"
345 );
346
347 await_no_pending_txs(&client).await?;
348
349 ensure_tx_chain_length(&client, 4).await?;
350
351 info!("Verify that a send with zero fee aborts...");
352
353 let abort_address = bitcoind.get_new_address().await?;
354
355 let value = cmd!(
356 client,
357 "module",
358 "walletv2",
359 "send",
360 abort_address,
361 "100000 sat",
362 "--fee",
363 "0 sat"
364 )
365 .out_json()
366 .await?;
367
368 assert_eq!(
369 FinalSendState::Aborted,
370 serde_json::from_value(value)?,
371 "Send with zero fee should abort"
372 );
373
374 info!("Test circular deposit (send to second client's federation address)...");
375
376 let client_two = fed
377 .new_joined_client("walletv2-test-circular-deposit-client")
378 .await?;
379
380 let (circular_address, position) = get_deposit_address(&client_two).await?;
381
382 let value = cmd!(
383 client,
384 "module",
385 "walletv2",
386 "send",
387 circular_address.to_string(),
388 "100000 sat"
389 )
390 .out_json()
391 .await?;
392
393 let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
394 panic!("Circular deposit send operation failed");
395 };
396
397 bitcoind.poll_get_transaction(txid).await?;
398
399 await_deposits(&client_two, position, 1, 99_000).await?;
400
401 await_no_pending_txs(&client).await?;
402
403 ensure_tx_chain_length(&client, 6).await?;
404
405 if fedimint_cli_version >= *VERSION_0_13_0_ALPHA {
408 info!("Sweep the entire remaining balance onchain...");
409
410 let sweep_address = bitcoind.get_new_address().await?;
411
412 cmd!(client, "dev", "wait-complete").run().await?;
421
422 let pre_sweep_balance = client.balance().await?;
423
424 let value = cmd!(client, "module", "walletv2", "send", sweep_address, "all")
430 .out_json()
431 .await?;
432
433 let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
434 panic!("Sweep send operation failed");
435 };
436
437 bitcoind.poll_get_transaction(txid).await?;
438
439 let post_sweep_balance = client.balance().await?;
444
445 ensure!(
446 post_sweep_balance < pre_sweep_balance / 100,
447 "Sweep left {post_sweep_balance} msats of {pre_sweep_balance} msats behind"
448 );
449
450 await_no_pending_txs(&client).await?;
451 }
452
453 block_miner.abort();
454
455 info!("Wallet V2 send and receive test successful");
456
457 Ok(())
458 })
459 .await
460}