fedimint_mint_client/
repair_wallet.rs1use std::collections::BTreeMap;
2
3use fedimint_core::TieredCounts;
4use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
5use fedimint_core::util::backoff_util::aggressive_backoff;
6use fedimint_core::util::retry;
7use futures::{StreamExt, TryStreamExt, stream};
8
9use crate::api::MintFederationApi;
10use crate::client_db::{
11 NextECashNoteIndexKey, NextECashNoteIndexKeyPrefix, NoteKey, NoteKeyPrefix,
12};
13use crate::output::NoteIssuanceRequest;
14use crate::{MintClientModule, NoteIndex};
15
16const CHECK_PARALLELISM: usize = 16;
17
18#[derive(Debug, Clone, Default)]
19pub struct RepairSummary {
20 pub spent_notes: TieredCounts,
23 pub used_indices: TieredCounts,
30}
31
32impl MintClientModule {
33 pub async fn try_repair_wallet(&self) -> anyhow::Result<RepairSummary> {
45 let mut summary = RepairSummary::default();
46
47 let module_api = self.client_ctx.module_api();
48 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
49
50 let spent_notes: Vec<NoteKey> = dbtx
52 .find_by_prefix_sorted_descending(&NoteKeyPrefix)
53 .await
54 .map(|(key, _)| {
55 let module_api_inner = module_api.clone();
56 async move {
57 let spent = retry("fetch e-cash spentness", aggressive_backoff(), || async {
58 Ok(module_api_inner.check_note_spent(key.nonce).await?)
59 })
60 .await?;
61 anyhow::Ok(if spent { Some(key) } else { None })
62 }
63 })
64 .buffer_unordered(CHECK_PARALLELISM)
65 .try_filter_map(|result| async move { Ok(result) })
66 .try_collect()
67 .await?;
68
69 for note_key in spent_notes {
70 summary.spent_notes.inc(note_key.amount, 1);
71 dbtx.remove_entry(¬e_key).await;
72 }
73
74 let next_indices: BTreeMap<_, _> = {
75 let mut db_next_indexes = dbtx
76 .find_by_prefix_sorted_descending(&NextECashNoteIndexKeyPrefix)
77 .await
78 .map(|(key, idx)| (key.0, idx))
79 .collect::<BTreeMap<_, _>>()
80 .await;
81
82 self.cfg
83 .tbs_pks
84 .tiers()
85 .map(|&denomination| {
86 (
87 denomination,
88 db_next_indexes.remove(&denomination).unwrap_or_default(),
89 )
90 })
91 .collect()
92 };
93
94 let used_nonces = stream::iter(next_indices.into_iter())
96 .map(|(amount, original_next_index)| {
97 let module_api_inner = module_api.clone();
98 async move {
99 let mut next_index = original_next_index;
100 let maybe_advanced_index = loop {
101 let note_secret = Self::new_note_secret_static(
102 &self.secret,
103 amount,
104 NoteIndex(next_index),
105 );
106 let (_, blind_nonce) = NoteIssuanceRequest::new(&self.secp, ¬e_secret);
107 let nonce_used = retry(
108 "checking if blind nonce was already used",
109 aggressive_backoff(),
110 || async {
111 Ok(module_api_inner.check_blind_nonce_used(blind_nonce).await?)
112 },
113 )
114 .await?;
115
116 if nonce_used {
117 next_index += 1;
119 } else if original_next_index == next_index {
120 break None;
122 } else {
123 break Some((amount, next_index));
126 }
127 };
128
129 Result::<_, anyhow::Error>::Ok(maybe_advanced_index)
130 }
131 })
132 .buffer_unordered(CHECK_PARALLELISM)
133 .try_filter_map(|advanced_index| async move { Ok(advanced_index) })
134 .try_collect::<Vec<_>>()
135 .await?;
136
137 for (amount, next_index) in used_nonces {
138 let old_index = dbtx
139 .insert_entry(&NextECashNoteIndexKey(amount), &next_index)
140 .await
141 .unwrap_or_default();
142 summary
143 .used_indices
144 .inc(amount, (next_index - old_index) as usize);
145 }
146
147 dbtx.commit_tx().await;
148 Ok(summary)
149 }
150}