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!(
119 client,
120 "module",
121 "walletv2",
122 "info",
123 "tx-chain",
124 expected.to_string()
125 )
126 .out_json()
127 .await?;
128
129 let chain: Vec<serde_json::Value> = serde_json::from_value(value)?;
130
131 ensure!(chain.len() == expected,);
132
133 Ok(())
134}
135
136async fn get_deposit_address(client: &Client) -> anyhow::Result<Address> {
137 let address = serde_json::from_value::<Address<NetworkUnchecked>>(
138 cmd!(client, "module", "walletv2", "receive")
139 .out_json()
140 .await?,
141 )?
142 .assume_checked();
143
144 Ok(address)
145}
146
147#[tokio::main]
148async fn main() -> anyhow::Result<()> {
149 unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLETV2", "true") };
151 unsafe { std::env::set_var("FM_ENABLE_MODULE_WALLET", "false") };
152
153 devimint::run_devfed_test()
154 .call(|dev_fed, _process_mgr| async move {
155 let fedimint_cli_version = util::FedimintCli::version_or_default().await;
156 let fedimintd_version = util::FedimintdCmd::version_or_default().await;
157
158 if fedimint_cli_version < *VERSION_0_11_0_ALPHA {
159 info!(%fedimint_cli_version, "Version did not support walletv2 module, skipping");
160 return Ok(());
161 }
162
163 if fedimintd_version < *VERSION_0_11_0_ALPHA {
164 info!(%fedimintd_version, "Version did not support walletv2 module, skipping");
165 return Ok(());
166 }
167
168 let (fed, bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
169
170 let client = fed
171 .new_joined_client("walletv2-test-send-and-receive-client")
172 .await?;
173
174 info!("Verify that walletv1 is not present...");
175
176 ensure!(
177 !module_is_present(&client, "wallet").await?,
178 "walletv1 module should not be present"
179 );
180
181 ensure!(
182 module_is_present(&client, "walletv2").await?,
183 "walletv2 module should be present"
184 );
185
186 let block_miner = spawn_block_miner(bitcoind.clone());
191
192 info!("Wait for the consensus to reach block count one");
196
197 await_consensus_block_count(&client, 1).await?;
198
199 info!("Deposit funds into the federation...");
200
201 let federation_address_1 = get_deposit_address(&client).await?;
202
203 fed.bitcoind
204 .send_to(federation_address_1.to_string(), 100_000)
205 .await?;
206
207 fed.bitcoind
208 .send_to(federation_address_1.to_string(), 200_000)
209 .await?;
210
211 info!("Wait for deposits to be claimed...");
212
213 await_client_balance(&client, 290_000).await?;
214
215 ensure_federation_total_value(&client, 290_000).await?;
216
217 let federation_address_2 = get_deposit_address(&client).await?;
218
219 assert_ne!(federation_address_1, federation_address_2);
220
221 fed.bitcoind
222 .send_to(federation_address_2.to_string(), 300_000)
223 .await?;
224
225 fed.bitcoind
226 .send_to(federation_address_2.to_string(), 400_000)
227 .await?;
228
229 info!("Wait for deposits to be claimed...");
230
231 await_client_balance(&client, 980_000).await?;
232
233 ensure_federation_total_value(&client, 980_000).await?;
234
235 let federation_address_3 = get_deposit_address(&client).await?;
236
237 assert_ne!(federation_address_2, federation_address_3);
238
239 info!("Send funds back on-chain...");
240
241 let withdraw_address = bitcoind.get_new_address().await?;
242
243 let value = cmd!(
244 client,
245 "module",
246 "walletv2",
247 "send",
248 withdraw_address,
249 "500000 sat"
250 )
251 .out_json()
252 .await?;
253
254 let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
255 panic!("Send operation failed");
256 };
257
258 bitcoind.poll_get_transaction(txid).await?;
259
260 let total_value: u64 = serde_json::from_value(
261 cmd!(client, "module", "walletv2", "info", "total-value")
262 .out_json()
263 .await?,
264 )?;
265
266 assert!(
267 total_value < 500_000,
268 "Federation total value should be less than 500_000 sats"
269 );
270
271 await_no_pending_txs(&client).await?;
272
273 ensure_tx_chain_length(&client, 4).await?;
274
275 info!("Verify that a send with zero fee aborts...");
276
277 let abort_address = bitcoind.get_new_address().await?;
278
279 let value = cmd!(
280 client,
281 "module",
282 "walletv2",
283 "send",
284 abort_address,
285 "100000 sat",
286 "--fee",
287 "0 sat"
288 )
289 .out_json()
290 .await?;
291
292 assert_eq!(
293 FinalSendState::Aborted,
294 serde_json::from_value(value)?,
295 "Send with zero fee should abort"
296 );
297
298 info!("Test circular deposit (send to second client's federation address)...");
299
300 let client_two = fed
301 .new_joined_client("walletv2-test-circular-deposit-client")
302 .await?;
303
304 let circular_address = get_deposit_address(&client_two).await?;
305
306 let value = cmd!(
307 client,
308 "module",
309 "walletv2",
310 "send",
311 circular_address.to_string(),
312 "100000 sat"
313 )
314 .out_json()
315 .await?;
316
317 let FinalSendState::Success(txid) = serde_json::from_value(value)? else {
318 panic!("Circular deposit send operation failed");
319 };
320
321 bitcoind.poll_get_transaction(txid).await?;
322
323 await_client_balance(&client_two, 99_000).await?;
324
325 await_no_pending_txs(&client).await?;
326
327 ensure_tx_chain_length(&client, 6).await?;
328
329 block_miner.abort();
330
331 info!("Wallet V2 send and receive test successful");
332
333 Ok(())
334 })
335 .await
336}