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