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