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