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_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 assert_eq!(peg_in_amount_sats * 1000, restored.balance().await?);
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 assert_eq!(peg_in_amount_sats * 1000 * 2, restored.balance().await?);
120 }
121
122 info!("### Test wallet restore with a history and no backup");
123 {
124 let client = client_slow;
125
126 retry(
127 "wait for next session",
128 backoff_util::aggressive_backoff(),
129 || async {
130 if client_slow_pegin_session_count < client.get_session_count().await? {
131 return Ok(());
132 }
133 bail!("Session didn't close")
134 },
135 )
136 .await
137 .expect("timeouted waiting for session to close");
138
139 let operation_id = fed
140 .pegin_client_no_wait(peg_in_amount_sats, &client)
141 .await?;
142
143 info!("Client slow: Restore without backup");
144 let restored = client
145 .new_restored("client-slow-restored-without-backup", fed.invite_code()?)
146 .await?;
147
148 cmd!(
149 restored,
150 "module",
151 "wallet",
152 "await-deposit",
153 "--operation-id",
154 operation_id
155 )
156 .run()
157 .await?;
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().call(|dev_fed, _process_mgr| async move {
170 let (fed, _bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
171
172 let peg_in_amount_sats = 100_000;
173
174 let reference_client = fed
176 .new_joined_client("wallet-client-recovery-origin")
177 .await?;
178 info!(target: LOG_TEST, "Join and claim");
179 fed.pegin_client(peg_in_amount_sats, &reference_client).await?;
180
181
182 let secret = cmd!(reference_client, "print-secret").out_json().await?["secret"]
183 .as_str()
184 .map(ToOwned::to_owned)
185 .unwrap();
186
187 let pre_notes = cmd!(reference_client, "info").out_json().await?;
188
189 let pre_balance = pre_notes["total_amount_msat"].as_u64().unwrap();
190
191 debug!(target: LOG_TEST, %pre_notes, pre_balance, "State before backup");
192
193 assert!(0 < pre_balance);
196
197 {
205 let client = Client::create("restore-without-backup").await?;
206 let _ = cmd!(
207 client,
208 "restore",
209 "--mnemonic",
210 &secret,
211 "--invite-code",
212 fed.invite_code()?
213 )
214 .out_json()
215 .await?;
216
217 let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
218 let post_notes = cmd!(client, "info").out_json().await?;
219 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
220 debug!(target: LOG_TEST, %post_notes, post_balance, "State after backup");
221 assert_eq!(pre_balance, post_balance);
222 }
223
224 {
226 let _ = cmd!(reference_client, "backup",).out_json().await?;
227 let client = Client::create("restore-with-backup").await?;
228
229 {
230 let _ = cmd!(
231 client,
232 "restore",
233 "--mnemonic",
234 &secret,
235 "--invite-code",
236 fed.invite_code()?
237 )
238 .out_json()
239 .await?;
240
241 let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
242 let post_notes = cmd!(client, "info").out_json().await?;
243 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
244 debug!(target: LOG_TEST, %post_notes, post_balance, "State after backup");
245
246 assert_eq!(pre_balance, post_balance);
247 }
248
249 let _ = cmd!(client, "backup",).out_json().await?;
252
253 const EXTRA_PEGIN_SATS: u64 = 1000;
254 fed.pegin_client(EXTRA_PEGIN_SATS, &client).await?;
255
256 {
257 let client = Client::create("restore-with-backup-again").await?;
258 let _ = cmd!(
259 client,
260 "restore",
261 "--mnemonic",
262 &secret,
263 "--invite-code",
264 fed.invite_code()?
265 )
266 .out_json()
267 .await?;
268
269 let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
270 let post_notes = cmd!(client, "info").out_json().await?;
271 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
272 debug!(target: LOG_TEST, %post_notes, post_balance, "State after (subsequent) backup");
273
274 assert_eq!(pre_balance + EXTRA_PEGIN_SATS * 1000, post_balance);
275 }
276 }
277
278 Ok(())
279 })
280 .await
281}
282
283async fn transfer(
284 send_client: &Client,
285 receive_client: &Client,
286 bitcoind: &devimint::external::Bitcoind,
287 amount_sat: u64,
288) -> Result<serde_json::Value> {
289 debug!(target: LOG_TEST, %amount_sat, "Transferring on-chain funds between clients");
290 let (deposit_address, operation_id) = receive_client.get_deposit_addr().await?;
291 let withdraw_res = cmd!(
292 send_client,
293 "withdraw",
294 "--address",
295 &deposit_address,
296 "--amount",
297 "{amount_sat} sat"
298 )
299 .out_json()
300 .await?;
301
302 let txid: Txid = withdraw_res["txid"].as_str().unwrap().parse().unwrap();
304 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
305
306 let parsed_address = Address::from_str(&deposit_address)?;
307 let tx = Transaction::consensus_decode_hex(&tx_hex, &Default::default())?;
308 assert!(tx.output.iter().any(|o| o.script_pubkey
309 == parsed_address.clone().assume_checked().script_pubkey()
310 && o.value.to_sat() == amount_sat));
311
312 debug!(target: LOG_TEST, %txid, "Awaiting transaction");
313 try_join!(
315 bitcoind.mine_blocks(21),
316 receive_client.await_deposit(&operation_id),
317 )?;
318
319 Ok(withdraw_res)
320}
321
322async fn assert_withdrawal(
323 send_client: &Client,
324 receive_client: &Client,
325 bitcoind: &devimint::external::Bitcoind,
326 fed: &devimint::federation::Federation,
327) -> Result<()> {
328 let withdrawal_amount_sats = 50_000;
329 let withdrawal_amount_msats = withdrawal_amount_sats * 1000;
330
331 if send_client.balance().await? < withdrawal_amount_msats {
332 fed.pegin_client(withdrawal_amount_sats * 2, send_client)
333 .await?;
334 }
335
336 let send_client_pre_balance = send_client.balance().await?;
337 let receive_client_pre_balance = receive_client.balance().await?;
338
339 let withdraw_res = transfer(
340 send_client,
341 receive_client,
342 bitcoind,
343 withdrawal_amount_sats,
344 )
345 .await?;
346
347 let send_client_post_balance = send_client.balance().await?;
349 let receive_client_post_balance = receive_client.balance().await?;
350 let fed_deposit_fees_msats = fed.deposit_fees()?.msats;
351 let onchain_fees_msats = withdraw_res["fees_sat"].as_u64().unwrap() * 1000;
352
353 let expected_send_client_balance = if send_client.get_name() == receive_client.get_name() {
354 send_client_pre_balance - onchain_fees_msats - fed_deposit_fees_msats
355 } else {
356 send_client_pre_balance - withdrawal_amount_msats - onchain_fees_msats
357 };
358
359 let expected_receive_client_balance = if send_client.get_name() == receive_client.get_name() {
360 receive_client_pre_balance - onchain_fees_msats - fed_deposit_fees_msats
361 } else {
362 receive_client_pre_balance + withdrawal_amount_msats - fed_deposit_fees_msats
363 };
364
365 assert_eq!(send_client_post_balance, expected_send_client_balance);
366 assert_eq!(receive_client_post_balance, expected_receive_client_balance);
367
368 Ok(())
369}
370
371async fn circular_deposit_test() -> anyhow::Result<()> {
372 devimint::run_devfed_test()
373 .call(|dev_fed, _process_mgr| async move {
374 let (fed, bitcoind) = try_join!(dev_fed.fed(), dev_fed.bitcoind())?;
375
376 let send_client = fed
377 .new_joined_client("circular-deposit-send-client")
378 .await?;
379
380 assert_withdrawal(&send_client, &send_client, bitcoind, fed).await?;
382
383 let receive_client = fed
385 .new_joined_client("circular-deposit-receive-client")
386 .await?;
387 assert_withdrawal(&send_client, &receive_client, bitcoind, fed).await?;
388
389 let fedimint_cli_version = FedimintCli::version_or_default().await;
390 if fedimint_cli_version >= *VERSION_0_8_0_ALPHA {
391 let dust_receive_client = fed
393 .new_joined_client("circular-deposit-dust-receive-client")
394 .await?;
395 transfer(&send_client, &dust_receive_client, bitcoind, 900).await?;
396 assert_eq!(dust_receive_client.balance().await?, 0);
397 }
398
399 Ok(())
400 })
401 .await
402}