Skip to main content

fedimint_server_ui/dashboard/
consensus_explorer.rs

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