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}