1use std::collections::HashMap;
10use std::fmt;
11
12use fedimint_client::Client;
13use fedimint_client::visualize::usecs_to_iso8601_secs;
14use fedimint_client_module::sm::IState;
15use fedimint_client_module::transaction::{
16 TRANSACTION_SUBMISSION_MODULE_INSTANCE, TxSubmissionStates, TxSubmissionStatesSM,
17};
18use fedimint_core::core::OperationId;
19use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
20use fedimint_core::{Amount, TransactionId};
21use futures::StreamExt;
22
23use crate::client_db::NoteKeyPrefix;
24use crate::events::{NoteCreated, NoteSpent};
25use crate::{BlindNonce, MintClientModule, MintClientStateMachines, MintInput, Nonce};
26
27pub struct NoteVisData {
29 pub nonce: Nonce,
30 pub amount: Option<Amount>,
31 pub blind_nonce: Option<BlindNonce>,
32 pub created_op: Option<OperationId>,
33 pub created_txid: Option<TransactionId>,
34 pub created_out_idx: Option<u64>,
35 pub created_ts: Option<u64>,
36 pub spent_op: Option<OperationId>,
37 pub spent_txid: Option<TransactionId>,
38 pub spent_in_idx: Option<usize>,
39 pub spent_ts: Option<u64>,
40 pub in_wallet: bool,
41}
42
43#[derive(Default)]
44struct NoteRecord {
45 amount: Option<Amount>,
46 blind_nonce: Option<BlindNonce>,
47 created_op: Option<OperationId>,
48 created_txid: Option<TransactionId>,
49 created_out_idx: Option<u64>,
50 created_ts: Option<u64>,
51 spent_op: Option<OperationId>,
52 spent_txid: Option<TransactionId>,
53 spent_in_idx: Option<usize>,
54 spent_ts: Option<u64>,
55 in_wallet: bool,
56}
57
58pub async fn get_notes_vis(client: &Client, limit: Option<usize>) -> NotesVisOutput {
63 let ops = client
64 .operation_log()
65 .paginate_operations_rev(limit.unwrap_or(usize::MAX), None)
66 .await;
67 let ops_count = ops.len();
68
69 let mut notes: HashMap<Nonce, NoteRecord> = HashMap::new();
70
71 if let Ok(mint_instance) = client.get_first_module::<MintClientModule>() {
73 let mut dbtx = mint_instance.db.begin_transaction_nc().await;
74 let wallet_notes: Vec<_> = dbtx.find_by_prefix(&NoteKeyPrefix).await.collect().await;
75 for (key, _note) in wallet_notes {
76 let record = notes.entry(key.nonce).or_default();
77 record.amount = Some(key.amount);
78 record.in_wallet = true;
79 }
80 }
81
82 for (key, _entry) in &ops {
84 scan_operation_states(client, key.operation_id, &mut notes).await;
85 }
86
87 scan_event_log(client, &mut notes).await;
89
90 let mut entries: Vec<_> = notes.into_iter().collect();
92 entries.sort_by_key(|(_, r)| r.created_ts.unwrap_or(0));
93
94 let notes = entries
95 .into_iter()
96 .map(|(nonce, r)| NoteVisData {
97 nonce,
98 amount: r.amount,
99 blind_nonce: r.blind_nonce,
100 created_op: r.created_op,
101 created_txid: r.created_txid,
102 created_out_idx: r.created_out_idx,
103 created_ts: r.created_ts,
104 spent_op: r.spent_op,
105 spent_txid: r.spent_txid,
106 spent_in_idx: r.spent_in_idx,
107 spent_ts: r.spent_ts,
108 in_wallet: r.in_wallet,
109 })
110 .collect();
111
112 NotesVisOutput { notes, ops_count }
113}
114
115async fn scan_operation_states(
117 client: &Client,
118 op_id: OperationId,
119 notes: &mut HashMap<Nonce, NoteRecord>,
120) {
121 let (active, inactive) = client.executor().get_operation_states(op_id).await;
122
123 let all_states = active
124 .iter()
125 .map(|(s, _)| s)
126 .chain(inactive.iter().map(|(s, _)| s));
127
128 for state in all_states {
129 if state.module_instance_id() == TRANSACTION_SUBMISSION_MODULE_INSTANCE {
131 let Some(tx_sm) = state.as_any().downcast_ref::<TxSubmissionStatesSM>() else {
132 continue;
133 };
134 let TxSubmissionStates::Created(tx) = &tx_sm.state else {
135 continue;
136 };
137
138 let txid: TransactionId = tx.tx_hash();
139
140 for (idx, input) in tx.inputs.iter().enumerate() {
141 if let Some(mint_input) = input.as_any().downcast_ref::<MintInput>()
142 && let Some(v0) = mint_input.maybe_v0_ref()
143 {
144 let nonce = v0.note.nonce;
145 let record = notes.entry(nonce).or_default();
146 record.amount = Some(v0.amount);
147 record.spent_op = Some(op_id);
148 record.spent_txid = Some(txid);
149 record.spent_in_idx = Some(idx);
150 }
151 }
152 continue;
153 }
154
155 if let Some(MintClientStateMachines::Output(sm)) =
157 state.as_any().downcast_ref::<MintClientStateMachines>()
158 {
159 let txid = sm.txid();
160 for (out_idx, amount, nonce, blind_nonce) in sm.created_nonces() {
161 let record = notes.entry(nonce).or_default();
162 record.amount = Some(amount);
163 record.blind_nonce = Some(blind_nonce);
164 record.created_op = Some(op_id);
165 record.created_txid = Some(txid);
166 record.created_out_idx = Some(out_idx);
167 }
168 }
169 }
170}
171
172async fn scan_event_log(client: &Client, notes: &mut HashMap<Nonce, NoteRecord>) {
174 const PAGE_SIZE: u64 = 10000;
175 let mut cursor = None;
176 loop {
177 let events = client.get_event_log(cursor, PAGE_SIZE).await;
178 if events.is_empty() {
179 break;
180 }
181 for event in &events {
182 if event.kind == fedimint_eventlog::EventKind::from_static("note-created") {
183 if let Some(nc) = event.to_event::<NoteCreated>() {
184 let record = notes.entry(nc.nonce).or_default();
185 record.created_ts = Some(event.ts_usecs);
186 }
187 } else if event.kind == fedimint_eventlog::EventKind::from_static("note-spent")
188 && let Some(ns) = event.to_event::<NoteSpent>()
189 {
190 let record = notes.entry(ns.nonce).or_default();
191 record.spent_ts = Some(event.ts_usecs);
192 }
193 }
194 cursor = events.last().map(|e| e.id().next());
195 }
196}
197
198pub struct NotesVisOutput {
200 pub notes: Vec<NoteVisData>,
201 pub ops_count: usize,
202}
203
204impl fmt::Display for NoteVisData {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 let amount_str = self.amount.map_or("?".to_string(), |a| a.msats.to_string());
207 let blind_nonce_str = self.blind_nonce.map_or(String::new(), |bn| {
208 format!(" blind_nonce={}", bn.fmt_short())
209 });
210 writeln!(
211 f,
212 "nonce={}{} amount={amount_str}",
213 self.nonce.fmt_short(),
214 blind_nonce_str
215 )?;
216
217 let when_created = self
219 .created_ts
220 .map_or("?".to_string(), usecs_to_iso8601_secs);
221 if let (Some(op), Some(txid), Some(idx)) =
222 (self.created_op, self.created_txid, self.created_out_idx)
223 {
224 writeln!(
225 f,
226 " created: {when_created} op={} tx={}:{idx}",
227 op.fmt_short(),
228 txid.fmt_short()
229 )?;
230 } else {
231 writeln!(f, " created: {when_created}")?;
232 }
233
234 if let (Some(op), Some(txid), Some(idx)) =
236 (self.spent_op, self.spent_txid, self.spent_in_idx)
237 {
238 let when_spent = self.spent_ts.map_or("?".to_string(), usecs_to_iso8601_secs);
239 writeln!(
240 f,
241 " spent: {when_spent} op={} tx={}:{idx}",
242 op.fmt_short(),
243 txid.fmt_short()
244 )?;
245 } else if let Some(ts) = self.spent_ts {
246 writeln!(
247 f,
248 " spent: {} (no tx info in recent ops)",
249 usecs_to_iso8601_secs(ts)
250 )?;
251 }
252 Ok(())
253 }
254}
255
256impl fmt::Display for NotesVisOutput {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 writeln!(
259 f,
260 "### Notes ({} found, from {} most recent operations + event log)\n",
261 self.notes.len(),
262 self.ops_count
263 )?;
264
265 if self.notes.is_empty() {
266 writeln!(f, " (no notes found)")?;
267 return Ok(());
268 }
269
270 for note in &self.notes {
271 write!(f, "{note}")?;
272 }
273 writeln!(f)
274 }
275}