Skip to main content

fedimint_mint_client/
visualize.rs

1//! Visualization data structures and data-fetching for mint e-cash notes.
2//!
3//! Provides [`NoteVisData`] with creation/spending provenance for each note,
4//! collected from the wallet DB, operation state machines, and event log.
5//!
6//! [`NoteVisData`] and [`NotesVisOutput`] implement [`fmt::Display`] for text
7//! rendering.
8
9use 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
27/// Per-nonce record with creation and spending provenance.
28pub 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
58/// Fetch note visualization data from client state.
59///
60/// Scans the wallet DB, operation state machines, and event log for the
61/// `limit` most recent operations.
62pub 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    // 1. Scan wallet DB for current notes (nonce + amount)
72    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    // 2. Scan operations' state machines
83    for (key, _entry) in &ops {
84        scan_operation_states(client, key.operation_id, &mut notes).await;
85    }
86
87    // 3. Scan entire event log for NoteCreated and NoteSpent events
88    scan_event_log(client, &mut notes).await;
89
90    // 4. Sort by creation time and convert to NoteVisData
91    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
115/// Extract note creation/spending info from an operation's state machines.
116async 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        // TxSubmission states → extract spending info from MintInput
130        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        // Mint output state machines → extract creation info
156        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
172/// Paginate through the entire event log, recording note timestamps.
173async 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
198/// Complete notes visualization output, ready for display.
199pub 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        // Creation info
218        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        // Spending info (only if spent)
235        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}