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