1use anyhow::Result;
2use clap::Parser;
3use devimint::cmd;
4use devimint::federation::Federation;
5use devimint::util::almost_equal;
6use fedimint_logging::LOG_DEVIMINT;
7use rand::Rng;
8use tokio::try_join;
9use tracing::info;
10
11#[derive(Debug, Parser)]
12enum Cmd {
13 Restore,
14 RecoveryV1,
17 RecoveryV2,
19 Sanity,
20}
21
22#[tokio::main]
23async fn main() -> anyhow::Result<()> {
24 match Cmd::parse() {
25 Cmd::Restore => restore().await,
26 Cmd::RecoveryV1 => mint_recovery_test().await,
27 Cmd::RecoveryV2 => mint_recovery_test().await,
28 Cmd::Sanity => sanity().await,
29 }
30}
31
32async fn restore() -> anyhow::Result<()> {
33 devimint::run_devfed_test()
34 .call(|fed, _process_mgr| async move {
35 let fed = fed.fed().await?;
36
37 test_restore_gap_test(fed).await?;
38 Ok(())
39 })
40 .await
41}
42
43pub async fn test_restore_gap_test(fed: &Federation) -> Result<()> {
44 let client = fed.new_joined_client("restore-gap-test").await?;
45 const PEGIN_SATS: u64 = 300000;
46 fed.pegin_client(PEGIN_SATS, &client).await?;
47
48 for i in 0..20 {
49 let gap = rand::thread_rng().gen_range(0..20);
50 info!(target: LOG_DEVIMINT, gap, "Gap");
51 cmd!(
52 client,
53 "dev",
54 "advance-note-idx",
55 "--amount",
56 "1024msat",
57 "--count",
58 &gap.to_string()
62 )
63 .run()
64 .await?;
65
66 let notes = cmd!(client, "info").out_json().await?;
69 let balance = notes["total_amount_msat"].as_u64().unwrap();
70
71 let reissure_amount = if i % 2 == 0 {
72 balance
74 } else {
75 rand::thread_rng().gen_range(10..(balance))
77 };
78 info!(target: LOG_DEVIMINT, i, reissure_amount, "Reissue");
79
80 let notes = cmd!(client, "spend", reissure_amount)
81 .out_json()
82 .await?
83 .get("notes")
84 .expect("Output didn't contain e-cash notes")
85 .as_str()
86 .unwrap()
87 .to_owned();
88
89 cmd!(client, "reissue", notes).out_json().await?;
91 }
92
93 let secret = cmd!(client, "print-secret").out_json().await?["secret"]
94 .as_str()
95 .map(ToOwned::to_owned)
96 .unwrap();
97
98 let pre_notes = cmd!(client, "info").out_json().await?;
99
100 let pre_balance = pre_notes["total_amount_msat"].as_u64().unwrap();
101
102 info!(target: LOG_DEVIMINT, %pre_notes, pre_balance, "State before backup");
103
104 assert!(0 < pre_balance);
106
107 {
109 let client =
110 devimint::federation::Client::create("restore-gap-test-without-backup").await?;
111 let _ = cmd!(
112 client,
113 "restore",
114 "--mnemonic",
115 &secret,
116 "--invite-code",
117 fed.invite_code()?
118 )
119 .out_json()
120 .await?;
121
122 let _ = cmd!(client, "dev", "wait-complete").out_json().await?;
123 let post_notes = cmd!(client, "info").out_json().await?;
124 let post_balance = post_notes["total_amount_msat"].as_u64().unwrap();
125 info!(target: LOG_DEVIMINT, %post_notes, post_balance, "State after backup");
126 assert_eq!(pre_balance, post_balance);
127 assert_eq!(pre_notes, post_notes);
128 }
129
130 Ok(())
131}
132
133async fn mint_recovery_test() -> anyhow::Result<()> {
141 devimint::run_devfed_test()
142 .call(|dev_fed, _process_mgr| async move {
143 let fed = dev_fed.fed().await?;
144
145 try_join!(
146 test_recovery_with_backup(fed),
147 test_recovery_without_backup(fed),
148 test_recovery_after_activity(fed),
149 test_recovery_with_post_backup_activity(fed),
150 )?;
151
152 Ok(())
153 })
154 .await
155}
156
157const PEGIN_SATS: u64 = 1_000_000;
158
159async fn test_recovery_with_backup(fed: &Federation) -> Result<()> {
160 info!(target: LOG_DEVIMINT, "### Test mint recovery with backup");
161 let client = fed.new_joined_client("mint-recovery-backup").await?;
162 fed.pegin_client(PEGIN_SATS, &client).await?;
163
164 let pre_balance = client.balance().await?;
165 info!(target: LOG_DEVIMINT, pre_balance, "Balance before backup");
166 assert!(pre_balance > 0);
167
168 cmd!(client, "backup").run().await?;
169
170 let restored = client
171 .new_restored("mint-restored-with-backup", fed.invite_code()?)
172 .await?;
173 cmd!(restored, "dev", "wait-complete").out_json().await?;
174
175 let post_balance = restored.balance().await?;
176 info!(target: LOG_DEVIMINT, post_balance, "Balance after recovery with backup");
177 almost_equal(pre_balance, post_balance, 25_000).unwrap();
178 Ok(())
179}
180
181async fn test_recovery_without_backup(fed: &Federation) -> Result<()> {
182 info!(target: LOG_DEVIMINT, "### Test mint recovery without backup");
183 let client = fed.new_joined_client("mint-recovery-no-backup").await?;
184 fed.pegin_client(PEGIN_SATS, &client).await?;
185
186 let pre_balance = client.balance().await?;
187 assert!(pre_balance > 0);
188
189 let restored = client
190 .new_restored("mint-restored-no-backup", fed.invite_code()?)
191 .await?;
192 cmd!(restored, "dev", "wait-complete").out_json().await?;
193
194 let post_balance = restored.balance().await?;
195 info!(target: LOG_DEVIMINT, post_balance, "Balance after recovery without backup");
196 almost_equal(pre_balance, post_balance, 25_000).unwrap();
197 Ok(())
198}
199
200async fn test_recovery_after_activity(fed: &Federation) -> Result<()> {
201 info!(target: LOG_DEVIMINT, "### Test mint recovery after spend+reissue activity");
202 let client = fed
203 .new_joined_client("mint-recovery-after-activity")
204 .await?;
205 fed.pegin_client(PEGIN_SATS, &client).await?;
206
207 for i in 0..3 {
208 let balance = client.balance().await?;
209 let spend_amount = balance / 3;
210
211 let notes = cmd!(client, "spend", spend_amount)
212 .out_json()
213 .await?
214 .get("notes")
215 .expect("Output didn't contain e-cash notes")
216 .as_str()
217 .unwrap()
218 .to_owned();
219
220 cmd!(client, "reissue", notes).out_json().await?;
221 info!(target: LOG_DEVIMINT, i, spend_amount, "Spent and reissued to self");
222 }
223
224 let pre_balance = client.balance().await?;
225 info!(target: LOG_DEVIMINT, pre_balance, "Balance after activity");
226 assert!(pre_balance > 0);
227
228 cmd!(client, "backup").run().await?;
229
230 let restored = client
231 .new_restored("mint-restored-after-activity", fed.invite_code()?)
232 .await?;
233 cmd!(restored, "dev", "wait-complete").out_json().await?;
234
235 let post_balance = restored.balance().await?;
236 info!(target: LOG_DEVIMINT, post_balance, "Balance after recovery post-activity");
237 almost_equal(pre_balance, post_balance, 25_000).unwrap();
238 Ok(())
239}
240
241async fn test_recovery_with_post_backup_activity(fed: &Federation) -> Result<()> {
242 info!(target: LOG_DEVIMINT, "### Test mint recovery with post-backup activity");
243 let client = fed.new_joined_client("mint-recovery-post-backup").await?;
244 fed.pegin_client(PEGIN_SATS, &client).await?;
245
246 cmd!(client, "backup").run().await?;
247
248 let balance = client.balance().await?;
249 let spend_amount = balance / 2;
250 let notes = cmd!(client, "spend", spend_amount)
251 .out_json()
252 .await?
253 .get("notes")
254 .expect("Output didn't contain e-cash notes")
255 .as_str()
256 .unwrap()
257 .to_owned();
258 cmd!(client, "reissue", notes).out_json().await?;
259
260 let pre_balance = client.balance().await?;
261 info!(target: LOG_DEVIMINT, pre_balance, "Balance after post-backup activity");
262
263 let restored = client
264 .new_restored("mint-restored-post-backup", fed.invite_code()?)
265 .await?;
266 cmd!(restored, "dev", "wait-complete").out_json().await?;
267
268 let post_balance = restored.balance().await?;
269 info!(target: LOG_DEVIMINT, post_balance, "Balance after recovery with post-backup activity");
270 almost_equal(pre_balance, post_balance, 25_000).unwrap();
271 Ok(())
272}
273
274async fn sanity() -> anyhow::Result<()> {
275 devimint::run_devfed_test()
276 .call(|_fed, _process_mgr| async move { Ok(()) })
277 .await
278}