1use std::str::FromStr;
2
3use anyhow::{Result, bail};
4use bitcoin::Transaction;
5use bitcoincore_rpc::bitcoin::Txid;
6use bitcoincore_rpc::bitcoin::address::Address;
7use clap::Parser;
8use devimint::cmd;
9use devimint::federation::Client;
10use devimint::util::{FedimintCli, FedimintdCmd};
11use devimint::version_constants::{VERSION_0_3_0_ALPHA, VERSION_0_4_0, VERSION_0_6_0_ALPHA};
12use fedimint_core::encoding::Decodable;
13use fedimint_core::util::{backoff_util, retry};
14use fedimint_logging::LOG_TEST;
15use tokio::try_join;
16use tracing::{debug, info};
17
18#[derive(Parser, Debug)]
19pub enum TestCli {
20 Recovery1,
21 Recovery2,
22 CircularDeposit,
23}
24
25#[tokio::main]
26async fn main() -> anyhow::Result<()> {
27 let opts = TestCli::parse();
28
29 match opts {
30 TestCli::Recovery1 => wallet_recovery_test_1().await,
31 TestCli::Recovery2 => wallet_recovery_test_2().await,
32 TestCli::CircularDeposit => circular_deposit_test().await,
33 }
34}
35
36async fn wallet_recovery_test_1() -> anyhow::Result<()> {
37 devimint::run_devfed_test(|dev_fed, _process_mgr| async move {
38 let fedimint_cli_version = FedimintCli::version_or_default().await;
39 let fedimintd_version = FedimintdCmd::version_or_default().await;
40 if fedimint_cli_version < *VERSION_0_4_0 || fedimintd_version < *VERSION_0_4_0 {
43 info!("Skipping whole test on old fedimint-cli/fedimintd that is missing some irrelevant bolts");
44 return Ok(());
45 }
46
47 let (fed, _bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
48
49 let peg_in_amount_sats = 100_000;
50
51 let client_slow = fed
53 .new_joined_client("wallet-client-recovery-origin")
54 .await?;
55 info!("Join and claim");
56 fed.pegin_client(peg_in_amount_sats, &client_slow).await?;
57
58 let client_slow_pegin_session_count = client_slow.get_session_count().await?;
59
60 info!("### Test wallet restore without a backup");
61 {
62 let client = fed
63 .new_joined_client("wallet-client-recovery-origin")
64 .await?;
65
66 info!("Join, but not claim");
67 let operation_id = fed
68 .pegin_client_no_wait(peg_in_amount_sats, &client)
69 .await?;
70
71 info!("Restore without backup");
72 let restored = client
73 .new_restored("restored-without-backup", fed.invite_code()?)
74 .await?;
75
76 if fedimint_cli_version < *VERSION_0_6_0_ALPHA {
77 cmd!(restored, "module", "wallet", "await-deposit", operation_id)
78 .run()
79 .await?;
80 } else {
81 cmd!(restored, "module", "wallet", "await-deposit", "--operation-id", operation_id)
82 .run()
83 .await?;
84 }
85
86 info!("Check if claimed");
87 assert_eq!(peg_in_amount_sats * 1000, restored.balance().await?);
88 }
89
90 info!("### Test wallet restore with a backup");
91 {
92 let client = fed
93 .new_joined_client("wallet-client-recovery-origin")
94 .await?;
95 assert_eq!(0, client.balance().await?);
96
97 info!("Join and claim");
98 fed.pegin_client(peg_in_amount_sats, &client).await?;
99
100 info!("Make a backup");
101 cmd!(client, "backup").run().await?;
102
103 info!("Join more, but not claim");
104 let operation_id = fed
105 .pegin_client_no_wait(peg_in_amount_sats, &client)
106 .await?;
107
108 info!("Restore with backup");
109 let restored = client
110 .new_restored("restored-with-backup", fed.invite_code()?)
111 .await?;
112
113 if fedimint_cli_version < *VERSION_0_6_0_ALPHA {
114 cmd!(restored, "module", "wallet", "await-deposit", operation_id)
115 .run()
116 .await?;
117 } else {
118 cmd!(restored, "module", "wallet", "await-deposit", "--operation-id", operation_id)
119 .run()
120 .await?;
121 }
122
123 info!("Check if claimed");
124 assert_eq!(peg_in_amount_sats * 1000 * 2, restored.balance().await?);
125 }
126
127 info!("### Test wallet restore with a history and no backup");
128 {
129 let client = client_slow;
130
131 retry("wait for next session", backoff_util::aggressive_backoff(), || async {
132 if client_slow_pegin_session_count < client.get_session_count().await? {
133 return Ok(());
134 }
135 bail!("Session didn't close")
136 })
137 .await
138 .expect("timeouted waiting for session to close");
139
140 let operation_id = fed
141 .pegin_client_no_wait(peg_in_amount_sats, &client)
142 .await?;
143
144 info!("Client slow: Restore without backup");
145 let restored = client
146 .new_restored("client-slow-restored-without-backup", fed.invite_code()?)
147 .await?;
148
149 if fedimint_cli_version < *VERSION_0_6_0_ALPHA {
150 cmd!(restored, "module", "wallet", "await-deposit", operation_id)
151 .run()
152 .await?;
153 } else {
154 cmd!(restored, "module", "wallet", "await-deposit", "--operation-id", operation_id)
155 .run()
156 .await?;
157 }
158
159 info!("Client slow: Check if claimed");
160 assert_eq!(peg_in_amount_sats * 1000 * 2, restored.balance().await?);
161 }
162
163 Ok(())
164 })
165 .await
166}
167
168async fn wallet_recovery_test_2() -> anyhow::Result<()> {
169 devimint::run_devfed_test(|dev_fed, _process_mgr| async move {
170 let fedimint_cli_version = FedimintCli::version_or_default().await;
171 let fedimintd_version = FedimintdCmd::version_or_default().await;
172 if fedimint_cli_version < *VERSION_0_4_0 || fedimintd_version < *VERSION_0_4_0 {
175 info!(target: LOG_TEST, "Skipping whole test on old fedimint-cli/fedimintd that is missing some irrelevant bolts");
176 return Ok(());
177 }
178
179 let (fed, _bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
180
181 let peg_in_amount_sats = 100_000;
182
183 let reference_client = fed
185 .new_joined_client("wallet-client-recovery-origin")
186 .await?;
187 info!(target: LOG_TEST, "Join and claim");
188 fed.pegin_client(peg_in_amount_sats, &reference_client).await?;
189
190
191 let secret = cmd!(reference_client, "print-secret").out_json().await?["secret"]
192 .as_str()
193 .map(ToOwned::to_owned)
194 .unwrap();
195
196 let pre_notes = cmd!(reference_client, "info").out_json().await?;
197
198 let pre_balance = pre_notes["total_amount_msat"].as_u64().unwrap();
199
200 debug!(target: LOG_TEST, %pre_notes, pre_balance, "State before backup");
201
202 assert!(0 < pre_balance);
205
206 {
214 let post_balance = if fedimint_cli_version >= *VERSION_0_3_0_ALPHA {
215 let client = Client::create("restore-without-backup").await?;
216 let _ = cmd!(
217 client,
218 "restore",
219 "--mnemonic",
220 &secret,
221 "--invite-code",
222 fed.invite_code()?
223 )
224 .out_json()
225 .await?;
226
227 let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
229 let post_notes = cmd!(client, "info").out_json().await?;
230 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
231 debug!(target: LOG_TEST, %post_notes, post_balance, "State after backup");
232
233 post_balance
234 } else {
235 let client = reference_client
236 .new_forked("restore-without-backup")
237 .await?;
238 let _ = cmd!(client, "wipe", "--force",).out_json().await?;
239
240 assert_eq!(
241 0,
242 cmd!(client, "info").out_json().await?["total_amount_msat"]
243 .as_u64()
244 .unwrap()
245 );
246
247 let post_balance = cmd!(client, "restore", &secret,)
248 .out_json()
249 .await?
250 .as_u64()
251 .unwrap();
252 let post_notes = cmd!(client, "info").out_json().await?;
253 debug!(target: LOG_TEST, %post_notes, post_balance, "State after backup");
254
255 post_balance
256 };
257 assert_eq!(pre_balance, post_balance);
258 }
259
260 {
262 if fedimint_cli_version >= *VERSION_0_3_0_ALPHA {
263 let _ = cmd!(reference_client, "backup",).out_json().await?;
264 let client = Client::create("restore-with-backup").await?;
265
266 {
267 let _ = cmd!(
268 client,
269 "restore",
270 "--mnemonic",
271 &secret,
272 "--invite-code",
273 fed.invite_code()?
274 )
275 .out_json()
276 .await?;
277
278 let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
279 let post_notes = cmd!(client, "info").out_json().await?;
280 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
281 debug!(target: LOG_TEST, %post_notes, post_balance, "State after backup");
282
283 assert_eq!(pre_balance, post_balance);
284 }
285
286 let _ = cmd!(client, "backup",).out_json().await?;
289
290 const EXTRA_PEGIN_SATS: u64 = 1000;
291 fed.pegin_client(EXTRA_PEGIN_SATS, &client).await?;
292
293 {
294 let client = Client::create("restore-with-backup-again").await?;
295 let _ = cmd!(
296 client,
297 "restore",
298 "--mnemonic",
299 &secret,
300 "--invite-code",
301 fed.invite_code()?
302 )
303 .out_json()
304 .await?;
305
306 let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
307 let post_notes = cmd!(client, "info").out_json().await?;
308 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
309 debug!(target: LOG_TEST, %post_notes, post_balance, "State after (subsequent) backup");
310
311 assert_eq!(pre_balance + EXTRA_PEGIN_SATS * 1000, post_balance);
312 }
313 } else {
314 let client = reference_client.new_forked("restore-with-backup").await?;
315 let _ = cmd!(client, "backup",).out_json().await?;
316 let _ = cmd!(client, "wipe", "--force",).out_json().await?;
317 assert_eq!(
318 0,
319 cmd!(client, "info").out_json().await?["total_amount_msat"]
320 .as_u64()
321 .unwrap()
322 );
323 let _ = cmd!(client, "restore", &secret,).out_json().await?;
324 let post_notes = cmd!(client, "info").out_json().await?;
325 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
326 debug!(target: LOG_TEST, %post_notes, post_balance, "State after backup");
327
328 assert_eq!(pre_balance, post_balance);
329 }
330 }
331
332 Ok(())
333 })
334 .await
335}
336async fn assert_withdrawal(
337 send_client: &Client,
338 receive_client: &Client,
339 bitcoind: &devimint::external::Bitcoind,
340 fed: &devimint::federation::Federation,
341) -> Result<()> {
342 let withdrawal_amount_sats = 50_000;
343 let withdrawal_amount_msats = withdrawal_amount_sats * 1000;
344
345 if send_client.balance().await? < withdrawal_amount_msats {
346 fed.pegin_client(withdrawal_amount_sats * 2, send_client)
347 .await?;
348 }
349
350 let send_client_pre_balance = send_client.balance().await?;
351 let receive_client_pre_balance = receive_client.balance().await?;
352
353 let (deposit_address, operation_id) = receive_client.get_deposit_addr().await?;
354 let withdraw_res = cmd!(
355 send_client,
356 "withdraw",
357 "--address",
358 &deposit_address,
359 "--amount",
360 "{withdrawal_amount_sats} sat"
361 )
362 .out_json()
363 .await?;
364
365 let txid: Txid = withdraw_res["txid"].as_str().unwrap().parse().unwrap();
367 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
368
369 let parsed_address = Address::from_str(&deposit_address)?;
370 let tx = Transaction::consensus_decode_hex(&tx_hex, &Default::default())?;
371 assert!(tx.output.iter().any(|o| o.script_pubkey
372 == parsed_address.clone().assume_checked().script_pubkey()
373 && o.value.to_sat() == withdrawal_amount_sats));
374
375 try_join!(
377 bitcoind.mine_blocks(21),
378 receive_client.await_deposit(&operation_id),
379 )?;
380
381 let send_client_post_balance = send_client.balance().await?;
383 let receive_client_post_balance = receive_client.balance().await?;
384 let fed_deposit_fees_msats = fed.deposit_fees()?.msats;
385 let onchain_fees_msats = withdraw_res["fees_sat"].as_u64().unwrap() * 1000;
386
387 let expected_send_client_balance = if send_client.get_name() == receive_client.get_name() {
388 send_client_pre_balance - onchain_fees_msats - fed_deposit_fees_msats
389 } else {
390 send_client_pre_balance - withdrawal_amount_msats - onchain_fees_msats
391 };
392
393 let expected_receive_client_balance = if send_client.get_name() == receive_client.get_name() {
394 receive_client_pre_balance - onchain_fees_msats - fed_deposit_fees_msats
395 } else {
396 receive_client_pre_balance + withdrawal_amount_msats - fed_deposit_fees_msats
397 };
398
399 assert_eq!(send_client_post_balance, expected_send_client_balance);
400 assert_eq!(receive_client_post_balance, expected_receive_client_balance);
401
402 Ok(())
403}
404
405async fn circular_deposit_test() -> anyhow::Result<()> {
406 devimint::run_devfed_test(|dev_fed, _process_mgr| async move {
407 let (fed, bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
408
409 let send_client = fed
410 .new_joined_client("circular-deposit-send-client")
411 .await?;
412
413 assert_withdrawal(&send_client, &send_client, bitcoind, fed).await?;
415
416 let receive_client = fed
418 .new_joined_client("circular-deposit-receive-client")
419 .await?;
420 assert_withdrawal(&send_client, &receive_client, bitcoind, fed).await?;
421
422 Ok(())
423 })
424 .await
425}