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