Skip to main content

fedimint_server_ui/dashboard/
consensus_explorer.rs

1use axum::extract::{Path, State};
2use axum::response::{Html, IntoResponse};
3use fedimint_core::epoch::ConsensusItem;
4use fedimint_core::hex;
5use fedimint_core::session_outcome::{AcceptedItem, SessionStatusV2};
6use fedimint_core::transaction::TransactionSignature;
7use fedimint_server_core::dashboard_ui::DynDashboardApi;
8use fedimint_ui_common::UiState;
9use fedimint_ui_common::auth::UserAuth;
10use maud::{Markup, html};
11
12use crate::dashboard::dashboard_layout;
13
14/// Handler for the consensus explorer view
15pub async fn consensus_explorer_view(
16    State(state): State<UiState<DynDashboardApi>>,
17    _auth: UserAuth,
18    session_idx: Option<Path<u64>>,
19) -> impl IntoResponse {
20    let session_count = state.api.session_count().await;
21    let last_sessin_idx = session_count.saturating_sub(1);
22
23    // If a specific session index was provided, show only that session
24    // Otherwise, show the current session
25    let session_idx = session_idx.map(|p| p.0).unwrap_or(last_sessin_idx);
26
27    let (_sigs, items) = match state.api.get_session_status(session_idx).await {
28        SessionStatusV2::Initial => (None, vec![]),
29        SessionStatusV2::Pending(items) => (None, items),
30        SessionStatusV2::Complete(signed_session_outcome) => (
31            Some(signed_session_outcome.signatures),
32            signed_session_outcome.session_outcome.items,
33        ),
34    };
35
36    let content = html! {
37        div class="row mb-4" {
38            div class="col-12" {
39                div class="d-flex justify-content-between align-items-center" {
40                    h2 { "Consensus Explorer" }
41                    a href="/" class="btn btn-outline-primary" { "Back to Dashboard" }
42                }
43            }
44        }
45
46        div class="row mb-4" {
47            div class="col-12" {
48                div class="d-flex justify-content-between align-items-center" {
49                    // Session navigation
50                    div class="btn-group" role="group" aria-label="Session navigation" {
51                        @if 0 < session_idx {
52                            a href={ "/explorer/" (session_idx - 1) } class="btn btn-outline-secondary" {
53                                "← Previous Session"
54                            }
55                        } @else {
56                            button class="btn btn-outline-secondary" disabled { "← Previous Session" }
57                        }
58
59                        @if session_idx < last_sessin_idx {
60                            a href={ "/explorer/" (session_idx + 1) } class="btn btn-outline-secondary" {
61                                "Next Session →"
62                            }
63                        } @else {
64                            button class="btn btn-outline-secondary" disabled { "Next Session →" }
65                        }
66                    }
67
68                    // Jump to session form
69                    form class="d-flex" action="javascript:void(0);" onsubmit="window.location.href='/explorer/' + document.getElementById('session-jump').value" {
70                        div class="input-group" {
71                            input type="number" class="form-control" id="session-jump" min="0" max=(session_count - 1) placeholder="Session #";
72                            button class="btn btn-outline-primary" type="submit" { "Go" }
73                        }
74                    }
75                }
76            }
77        }
78
79        div class="row" {
80            div class="col-12" {
81                div class="card mb-4" {
82                    div class="card-header" {
83                        div class="d-flex justify-content-between align-items-center" {
84                            h5 class="mb-0" { "Session #" (session_idx) }
85                            span class="badge bg-primary" { (items.len()) " items" }
86                        }
87                    }
88                    div class="card-body" {
89                        @if items.is_empty() {
90                            div class="alert alert-secondary" {
91                                "This session contains no consensus items."
92                            }
93                        } @else {
94                            div class="table-responsive" {
95                                table class="table table-striped table-hover" {
96                                    thead {
97                                        tr {
98                                            th { "Item #" }
99                                            th { "Type" }
100                                            th { "Peer" }
101                                            th { "Details" }
102                                        }
103                                    }
104                                    tbody {
105                                        @for (item_idx, item) in items.iter().enumerate() {
106                                            tr {
107                                                td { (item_idx) }
108                                                td { (format_item_type(&item.item)) }
109                                                td { (item.peer) }
110                                                td { (format_item_details(item)) }
111                                            }
112                                        }
113                                    }
114                                }
115                            }
116
117                            // Display signatures if available
118                            @if let Some(signatures) = _sigs {
119                                div class="mt-4" {
120                                    h5 { "Session Signatures" }
121                                    div class="alert alert-info" {
122                                        p { "This session was signed by the following peers:" }
123                                        ul class="mb-0" {
124                                            @for peer_id in signatures.keys() {
125                                                li { "Guardian " (peer_id.to_string()) }
126                                            }
127                                        }
128                                    }
129                                }
130                            }
131                        }
132                    }
133                }
134            }
135        }
136    };
137
138    let version = state.api.fedimintd_version().await;
139    let version_hash = state.api.fedimintd_version_hash().await;
140
141    Html(dashboard_layout(content, &version, version_hash.as_deref()).into_string()).into_response()
142}
143
144/// Format the type of consensus item for display
145fn format_item_type(item: &ConsensusItem) -> String {
146    match item {
147        ConsensusItem::Transaction(_) => "Transaction".to_string(),
148        ConsensusItem::Module(_) => "Module".to_string(),
149        ConsensusItem::Default { variant, .. } => format!("Unknown ({variant})"),
150    }
151}
152
153/// Format details about a consensus item
154fn format_item_details(item: &AcceptedItem) -> Markup {
155    match &item.item {
156        ConsensusItem::Transaction(tx) => {
157            html! {
158                div class="consensus-item-details" {
159                    div class="mb-2" {
160                        "Transaction ID: " code { (tx.tx_hash()) }
161                    }
162                    div class="mb-2" {
163                        "Nonce: " code { (hex::encode(tx.nonce)) }
164                    }
165
166                    // Inputs section
167                    details class="mb-2" {
168                        summary { "Inputs: " strong { (tx.inputs.len()) } }
169                        @if tx.inputs.is_empty() {
170                            div class="alert alert-secondary mt-2" { "No inputs" }
171                        } @else {
172                            div class="table-responsive mt-2" {
173                                table class="table table-sm" {
174                                    thead {
175                                        tr {
176                                            th { "#" }
177                                            th { "Module ID" }
178                                            th { "Type" }
179                                        }
180                                    }
181                                    tbody {
182                                        @for (idx, input) in tx.inputs.iter().enumerate() {
183                                            tr {
184                                                td { (idx) }
185                                                td { (input.module_instance_id()) }
186                                                td { (input.to_string()) }
187                                            }
188                                        }
189                                    }
190                                }
191                            }
192                        }
193                    }
194
195                    // Outputs section
196                    details class="mb-2" {
197                        summary { "Outputs: " strong { (tx.outputs.len()) } }
198                        @if tx.outputs.is_empty() {
199                            div class="alert alert-secondary mt-2" { "No outputs" }
200                        } @else {
201                            div class="table-responsive mt-2" {
202                                table class="table table-sm" {
203                                    thead {
204                                        tr {
205                                            th { "#" }
206                                            th { "Module ID" }
207                                            th { "Type" }
208                                        }
209                                    }
210                                    tbody {
211                                        @for (idx, output) in tx.outputs.iter().enumerate() {
212                                            tr {
213                                                td { (idx) }
214                                                td { (output.module_instance_id()) }
215                                                td { (output.to_string()) }
216                                            }
217                                        }
218                                    }
219                                }
220                            }
221                        }
222                    }
223
224                    // Signature info
225                    details class="mb-2" {
226                        summary { "Signature Info" }
227                        div class="mt-2" {
228                            @match &tx.signatures {
229                                TransactionSignature::NaiveMultisig(sigs) => {
230                                    div { "Type: NaiveMultisig" }
231                                    div { "Signatures: " (sigs.len()) }
232                                }
233                                TransactionSignature::Default { variant, bytes } => {
234                                    div { "Type: Unknown (variant " (variant) ")" }
235                                    div { "Size: " (bytes.len()) " bytes" }
236                                }
237                            }
238                        }
239                    }
240                }
241            }
242        }
243        ConsensusItem::Module(module_item) => {
244            html! {
245                div class="consensus-item-details" {
246                    div class="mb-2" {
247                        "Module Instance ID: " code { (module_item.module_instance_id()) }
248                    }
249
250                    @if let Some(kind) = module_item.module_kind() {
251                        div class="mb-2" {
252                            "Module Kind: " strong { (kind.to_string()) }
253                        }
254                    } @else {
255                        div class="alert alert-warning mb-2" {
256                            "Unknown Module Kind"
257                        }
258                    }
259
260                    div class="mb-2" {
261                        "Module Item: " code { (module_item.to_string()) }
262                    }
263                }
264            }
265        }
266        ConsensusItem::Default { variant, bytes } => {
267            html! {
268                div class="consensus-item-details" {
269                    div class="alert alert-warning mb-2" {
270                        "Unknown Consensus Item Type (variant " (variant) ")"
271                    }
272                    div class="mb-2" {
273                        "Size: " (bytes.len()) " bytes"
274                    }
275                    @if !bytes.is_empty() {
276                        details {
277                            summary { "Raw Data (Hex)" }
278                            div class="mt-2" {
279                                code class="user-select-all" style="word-break: break-all;" {
280                                    (hex::encode(bytes))
281                                }
282                            }
283                        }
284                    }
285                }
286            }
287        }
288    }
289}