Skip to main content

fedimint_server_ui/dashboard/modules/
walletv2.rs

1use maud::{Markup, PreEscaped, html};
2
3// Function to render the Wallet v2 module UI section
4pub async fn render(wallet: &fedimint_walletv2_server::Wallet) -> Markup {
5    let network = wallet.network_ui();
6    let federation_wallet = wallet.federation_wallet_ui().await;
7    let consensus_block_count = wallet.consensus_block_count_ui().await;
8    let consensus_fee_rate = wallet.consensus_feerate_ui().await;
9    let send_fee = wallet.send_fee_ui().await;
10    let receive_fee = wallet.receive_fee_ui().await;
11    let pending_tx_chain = wallet.pending_tx_chain_ui().await;
12    let tx_chain = wallet.tx_chain_ui().await;
13    let recovery_keys = wallet.recovery_keys_ui().await;
14
15    let total_pending_vbytes = pending_tx_chain.iter().map(|info| info.vbytes).sum::<u64>();
16
17    let total_pending_fee = pending_tx_chain
18        .iter()
19        .map(|info| info.fee.to_sat())
20        .sum::<u64>();
21
22    let custody_chart_data = if let Some(last) = tx_chain.last() {
23        let mut heights: Vec<u64> = tx_chain.iter().map(|tx| tx.created).collect();
24        let mut values: Vec<f64> = tx_chain.iter().map(|tx| tx.output.to_btc()).collect();
25        heights.push(consensus_block_count);
26        values.push(last.output.to_btc());
27        Some((heights, values))
28    } else {
29        None
30    };
31
32    html! {
33        div class="row gy-4 mt-2" {
34            div class="col-12" {
35                div class="card h-100" {
36                    div class="card-header dashboard-header" { "Wallet V2" }
37                    div class="card-body" {
38                        div class="mb-4" {
39                            table class="table" {
40                                tr {
41                                    th { "Network" }
42                                    td { (network) }
43                                }
44                                @if let Some(wallet) = federation_wallet {
45                                    tr {
46                                        th { "Value in Custody" }
47                                        td { (format!("{:.8} BTC", wallet.value.to_btc())) }
48                                    }
49                                    tr {
50                                        th { "Transaction Chain Tip" }
51                                        td {
52                                            a href={ "https://mempool.space/tx/" (wallet.outpoint.txid) } class="btn btn-sm btn-outline-primary" target="_blank" {
53                                                "mempool.space"
54                                            }
55                                        }
56                                    }
57                                }
58                                tr {
59                                    th { "Consensus Block Count" }
60                                    td { (consensus_block_count) }
61                                }
62                                tr {
63                                    th { "Consensus Fee Rate" }
64                                    td {
65                                        @if let Some(fee_rate) = consensus_fee_rate {
66                                            (fee_rate) " sat/vbyte"
67                                        } @else {
68                                            "No consensus fee rate available"
69                                        }
70                                    }
71                                }
72                                tr {
73                                    th { "Send Fee" }
74                                    td {
75                                        @if let Some(fee) = send_fee {
76                                            (fee.to_sat()) " sats"
77                                        } @else {
78                                            "No send fee available"
79                                        }
80                                    }
81                                }
82                                tr {
83                                    th { "Receive Fee" }
84                                    td {
85                                        @if let Some(fee) = receive_fee {
86                                            (fee.to_sat()) " sats"
87                                        } @else {
88                                            "No receive fee available"
89                                        }
90                                    }
91                                }
92                            }
93                        }
94
95
96                        @if !pending_tx_chain.is_empty() {
97                            div class="mb-4" {
98                                h5 { "Pending Transaction Chain" }
99                                @if consensus_block_count > pending_tx_chain.last().unwrap().created + 18 {
100                                    div class="alert alert-danger" role="alert" {
101                                        "Warning: Transaction has been pending for more than 18 blocks!"
102                                    }
103                                }
104
105                                table class="table" {
106                                    thead {
107                                        tr {
108                                            th { "Index" }
109                                            th { "Value in Custody" }
110                                            th { "Fee" }
111                                            th { "vBytes" }
112                                            th { "Feerate" }
113                                            th { "Age" }
114                                            th { "Transaction" }
115                                        }
116                                    }
117                                    tbody {
118                                        @for tx in pending_tx_chain{
119                                            tr {
120                                                td { (tx.index) }
121                                                td {
122                                                    @if tx.output >= tx.input {
123                                                        span class="text-success" { "+" (tx.output - tx.input) }
124                                                    } @else {
125                                                        span class="text-danger" { "-" (tx.input - tx.output) }
126                                                    }
127                                                }
128                                                td { (tx.fee.to_sat()) }
129                                                td { (tx.vbytes) }
130                                                td { (tx.feerate()) }
131                                                td { (consensus_block_count.saturating_sub(tx.created)) }
132                                                td {
133                                                    a href={ "https://mempool.space/tx/" (tx.txid) } class="btn btn-sm btn-outline-primary" target="_blank" {
134                                                        "mempool.space"
135                                                    }
136                                                }
137                                            }
138                                        }
139                                    }
140                                }
141
142                                div class="alert alert-info" role="alert" {
143                                    "Total feerate of pending chain: " strong { (total_pending_fee / total_pending_vbytes) " sat/vbyte" }
144                                }
145                            }
146                        }
147
148
149                        @if let Some((heights, values)) = &custody_chart_data {
150                            div class="mb-4" {
151                                h5 { "Value in Custody" }
152                                canvas id="walletv2-custody-chart" {}
153                                script src="/assets/chart.umd.min.js" {}
154                                (PreEscaped(format!(
155                                    r#"<script>
156                                    document.addEventListener('DOMContentLoaded', function() {{
157                                        var heights = {heights:?};
158                                        var values = {values:?};
159                                        var data = heights.map(function(h, i) {{ return {{x: h, y: values[i]}}; }});
160                                        new Chart(document.getElementById('walletv2-custody-chart'), {{
161                                            type: 'line',
162                                            data: {{
163                                                datasets: [{{
164                                                    label: 'Value in Custody (BTC)',
165                                                    data: data,
166                                                    borderWidth: 2,
167                                                    fill: true,
168                                                    stepped: true,
169                                                    pointRadius: 0
170                                                }}]
171                                            }},
172                                            options: {{
173                                                responsive: true,
174                                                plugins: {{
175                                                    legend: {{ display: false }},
176                                                    tooltip: {{ enabled: false }}
177                                                }},
178                                                scales: {{
179                                                    x: {{
180                                                        type: 'linear',
181                                                        min: heights[0],
182                                                        max: heights[heights.length - 1],
183                                                        title: {{
184                                                            display: true,
185                                                            text: 'Block Height'
186                                                        }}
187                                                    }},
188                                                    y: {{
189                                                        beginAtZero: true,
190                                                        title: {{
191                                                            display: true,
192                                                            text: 'BTC'
193                                                        }}
194                                                    }}
195                                                }}
196                                            }}
197                                        }});
198                                    }});
199                                    </script>"#,
200                                )))
201                            }
202                        }
203
204                        @if let Some((recovery_public_keys, recovery_private_key)) = &recovery_keys {
205                            // Federation Shutdown accordion
206                            div class="accordion mt-4" id="shutdownAccordion" {
207                                div class="accordion-item" {
208                                    h2 class="accordion-header" {
209                                        button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#shutdownCollapse" aria-expanded="false" aria-controls="shutdownCollapse" {
210                                            "Federation Shutdown"
211                                        }
212                                    }
213                                    div id="shutdownCollapse" class="accordion-collapse collapse" data-bs-parent="#shutdownAccordion" {
214                                        div class="accordion-body" {
215                                            div class="alert alert-warning mb-3" {
216                                                "To recover your remaining funds after decommissioning the federation, please go to the "
217                                                a href="https://recovery.fedimint.org" target="_blank" { "recovery tool" }
218                                                " and follow the instructions. The recovery keys change with every transaction. All guardians must be fully synced before extracting keys, otherwise the keys will not match the current federation UTXO."
219                                            }
220
221                                            div class="mb-3" {
222                                                table class="table table-sm" {
223                                                    thead {
224                                                        tr {
225                                                            th { "Guardian" }
226                                                            th { "Public Key (hex)" }
227                                                        }
228                                                    }
229                                                    tbody {
230                                                        @for (peer, pk) in recovery_public_keys {
231                                                            tr {
232                                                                td { (peer) }
233                                                                td class="text-break" style="word-break: break-all; font-family: monospace;" { (pk) }
234                                                            }
235                                                        }
236                                                    }
237                                                }
238                                            }
239
240                                            div class="mb-3" {
241                                                p class="mb-2" { strong { "Your Private Key (WIF)" } }
242                                                div class="alert alert-danger text-break" style="word-break: break-all;" {
243                                                    (recovery_private_key)
244                                                }
245                                            }
246                                        }
247                                    }
248                                }
249                            }
250                        }
251                    }
252                }
253            }
254        }
255    }
256}