fedimint_walletv2_devimint_tests/
tests.rs1use 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
17fn 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 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 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 let block_miner = spawn_block_miner(bitcoind.clone());
184
185 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}