fedimint_server_ui/dashboard/modules/walletv2.rs
1use maud::{Markup, 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(20).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 html! {
23 div class="row gy-4 mt-2" {
24 div class="col-12" {
25 div class="card h-100" {
26 div class="card-header dashboard-header" { "Wallet V2" }
27 div class="card-body" {
28 div class="mb-4" {
29 table class="table" {
30 tr {
31 th { "Network" }
32 td { (network) }
33 }
34 @if let Some(wallet) = federation_wallet {
35 tr {
36 th { "Value in Custody" }
37 td { (format!("{:.8} BTC", wallet.value.to_btc())) }
38 }
39 tr {
40 th { "Transaction Chain Tip" }
41 td {
42 a href={ "https://mempool.space/tx/" (wallet.outpoint.txid) } class="btn btn-sm btn-outline-primary" target="_blank" {
43 "mempool.space"
44 }
45 }
46 }
47 }
48 tr {
49 th { "Consensus Block Count" }
50 td { (consensus_block_count) }
51 }
52 tr {
53 th { "Consensus Fee Rate" }
54 td {
55 @if let Some(fee_rate) = consensus_fee_rate {
56 (fee_rate) " sat/vbyte"
57 } @else {
58 "No consensus fee rate available"
59 }
60 }
61 }
62 tr {
63 th { "Send Fee" }
64 td {
65 @if let Some(fee) = send_fee {
66 (fee.to_sat()) " sats"
67 } @else {
68 "No send fee available"
69 }
70 }
71 }
72 tr {
73 th { "Receive Fee" }
74 td {
75 @if let Some(fee) = receive_fee {
76 (fee.to_sat()) " sats"
77 } @else {
78 "No receive fee available"
79 }
80 }
81 }
82 }
83 }
84
85
86 @if !pending_tx_chain.is_empty() {
87 div class="mb-4" {
88 h5 { "Pending Transaction Chain" }
89 @if consensus_block_count > pending_tx_chain.last().unwrap().created + 18 {
90 div class="alert alert-danger" role="alert" {
91 "Warning: Transaction has been pending for more than 18 blocks!"
92 }
93 }
94
95 table class="table" {
96 thead {
97 tr {
98 th { "Index" }
99 th { "Value in Custody" }
100 th { "Fee" }
101 th { "vBytes" }
102 th { "Feerate" }
103 th { "Age" }
104 th { "Transaction" }
105 }
106 }
107 tbody {
108 @for tx in pending_tx_chain{
109 tr {
110 td { (tx.index) }
111 td {
112 @if tx.output >= tx.input {
113 span class="text-success" { "+" (tx.output - tx.input) }
114 } @else {
115 span class="text-danger" { "-" (tx.input - tx.output) }
116 }
117 }
118 td { (tx.fee.to_sat()) }
119 td { (tx.vbytes) }
120 td { (tx.feerate()) }
121 td { (consensus_block_count.saturating_sub(tx.created)) }
122 td {
123 a href={ "https://mempool.space/tx/" (tx.txid) } class="btn btn-sm btn-outline-primary" target="_blank" {
124 "mempool.space"
125 }
126 }
127 }
128 }
129 }
130 }
131
132 div class="alert alert-info" role="alert" {
133 "Total feerate of pending chain: " strong { (total_pending_fee / total_pending_vbytes) " sat/vbyte" }
134 }
135 }
136 }
137
138
139 @if !tx_chain.is_empty() {
140 div class="mb-4" {
141 h5 { "Total Transaction Chain" }
142 table class="table" {
143 thead {
144 tr {
145 th { "Index" }
146 th { "Value in Custody" }
147 th { "Fee" }
148 th { "vBytes" }
149 th { "Feerate" }
150 th { "Age" }
151 th { "Transaction" }
152 }
153 }
154 tbody {
155 @for tx in tx_chain {
156 tr {
157 td { (tx.index) }
158 td {
159 @if tx.output >= tx.input {
160 span class="text-success" { "+" (tx.output - tx.input) }
161 } @else {
162 span class="text-danger" { "-" (tx.input - tx.output) }
163 }
164 }
165 td { (tx.fee.to_sat()) }
166 td { (tx.vbytes) }
167 td { (tx.feerate()) }
168 td { (consensus_block_count.saturating_sub(tx.created)) }
169 td {
170 a href={ "https://mempool.space/tx/" (tx.txid) } class="btn btn-sm btn-outline-primary" target="_blank" {
171 "mempool.space"
172 }
173 }
174 }
175 }
176 }
177 }
178 }
179 }
180
181 @if let Some((recovery_public_keys, recovery_private_key)) = &recovery_keys {
182 // Federation Shutdown accordion
183 div class="accordion mt-4" id="shutdownAccordion" {
184 div class="accordion-item" {
185 h2 class="accordion-header" {
186 button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#shutdownCollapse" aria-expanded="false" aria-controls="shutdownCollapse" {
187 "Federation Shutdown"
188 }
189 }
190 div id="shutdownCollapse" class="accordion-collapse collapse" data-bs-parent="#shutdownAccordion" {
191 div class="accordion-body" {
192 div class="alert alert-info mb-3" {
193 "To recover your remaining funds after decommissioning the federation, please go to the "
194 a href="https://recovery.fedimint.org" target="_blank" { "recovery tool" }
195 " and follow the instructions."
196 }
197
198 div class="alert alert-warning mb-3" {
199 "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."
200 }
201
202 div class="mb-3" {
203 table class="table table-sm" {
204 thead {
205 tr {
206 th { "Guardian" }
207 th { "Public Key (hex)" }
208 }
209 }
210 tbody {
211 @for (peer, pk) in recovery_public_keys {
212 tr {
213 td { (peer) }
214 td class="text-break" style="word-break: break-all; font-family: monospace;" { (pk) }
215 }
216 }
217 }
218 }
219 }
220
221 div class="mb-3" {
222 p class="mb-2" { strong { "Your Private Key (WIF)" } }
223 div class="alert alert-danger text-break" style="word-break: break-all;" {
224 (recovery_private_key)
225 }
226 }
227 }
228 }
229 }
230 }
231 }
232 }
233 }
234 }
235 }
236 }
237}