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