fedimint_gateway_ui/
federation.rs1use std::fmt::Display;
2use std::str::FromStr;
3use std::time::{Duration, SystemTime};
4
5use axum::Form;
6use axum::extract::{Path, State};
7use axum::response::IntoResponse;
8use fedimint_core::Amount;
9use fedimint_core::config::FederationId;
10use fedimint_gateway_common::{FederationInfo, LeaveFedPayload, SetFeesPayload};
11use fedimint_ui_common::UiState;
12use fedimint_ui_common::auth::UserAuth;
13use maud::{Markup, html};
14
15use crate::{DynGatewayApi, SET_FEES_ROUTE, redirect_error, redirect_success};
16
17pub fn scripts() -> Markup {
18 html!(
19 script {
20 "function toggleFeesEdit(id) { \
21 const view = document.getElementById('fees-view-' + id); \
22 const edit = document.getElementById('fees-edit-' + id); \
23 if (view.style.display === 'none') { \
24 view.style.display = 'block'; \
25 edit.style.display = 'none'; \
26 } else { \
27 view.style.display = 'none'; \
28 edit.style.display = 'block'; \
29 } \
30 }"
31 }
32 )
33}
34
35pub fn render(fed: &FederationInfo) -> Markup {
36 html!(
37 @let bal = fed.balance_msat;
38 @let balance_class = if bal == Amount::ZERO {
39 "alert alert-danger"
40 } else {
41 "alert alert-success"
42 };
43 @let last_backup_str = fed.last_backup_time
44 .map(time_ago)
45 .unwrap_or("Never".to_string());
46
47
48 div class="row gy-4 mt-2" {
49 div class="col-12" {
50 div class="card h-100" {
51 div class="card-header dashboard-header d-flex justify-content-between align-items-center" {
52 div {
53 (fed.federation_name.clone().unwrap_or("Unnamed Federation".to_string()))
54 }
55
56 form method="post" action={(format!("/ui/federations/{}/leave", fed.federation_id))} {
57 button type="submit"
58 class="btn btn-outline-danger btn-sm"
59 title="Leave Federation" { "📤" }
60 }
61 }
62 div class="card-body" {
63 div id="balance" class=(balance_class) {
64 "Balance: " strong { (fed.balance_msat) }
65 }
66 div class="alert alert-secondary py-1 px-2 small" {
67 "Last Backup: " strong { (last_backup_str) }
68 }
69
70 div id={(format!("fees-view-{}", fed.federation_id))} {
72 table class="table table-sm mb-2" {
73 tbody {
74 tr {
75 th { "Lightning Base Fee" }
76 td { (fed.config.lightning_fee.base) }
77 }
78 tr {
79 th { "Lightning PPM" }
80 td { (fed.config.lightning_fee.parts_per_million) }
81 }
82 tr {
83 th { "Transaction Base Fee" }
84 td { (fed.config.transaction_fee.base) }
85 }
86 tr {
87 th { "Transaction PPM" }
88 td { (fed.config.transaction_fee.parts_per_million) }
89 }
90 }
91 }
92
93 button
94 class="btn btn-sm btn-outline-primary"
95 type="button"
96 onclick={(format!("toggleFeesEdit('{}')", fed.federation_id))}
97 {
98 "Edit Fees"
99 }
100 }
101
102 div id={(format!("fees-edit-{}", fed.federation_id))} style="display: none;" {
104 form
105 method="post"
106 action={(SET_FEES_ROUTE)}
107 {
108 input type="hidden" name="federation_id" value=(fed.federation_id.to_string());
109 table class="table table-sm mb-2" {
110 tbody {
111 tr {
112 th { "Lightning Base Fee" }
113 td {
114 input type="number"
115 class="form-control form-control-sm"
116 name="lightning_base"
117 value=(fed.config.lightning_fee.base.msats);
118 }
119 }
120 tr {
121 th { "Lightning PPM" }
122 td {
123 input type="number"
124 class="form-control form-control-sm"
125 name="lightning_parts_per_million"
126 value=(fed.config.lightning_fee.parts_per_million);
127 }
128 }
129 tr {
130 th { "Transaction Base Fee" }
131 td {
132 input type="number"
133 class="form-control form-control-sm"
134 name="transaction_base"
135 value=(fed.config.transaction_fee.base.msats);
136 }
137 }
138 tr {
139 th { "Transaction PPM" }
140 td {
141 input type="number"
142 class="form-control form-control-sm"
143 name="transaction_parts_per_million"
144 value=(fed.config.transaction_fee.parts_per_million);
145 }
146 }
147 }
148 }
149
150 button type="submit" class="btn btn-sm btn-primary me-2" { "Save Fees" }
151 button
152 type="button"
153 class="btn btn-sm btn-secondary"
154 onclick={(format!("toggleFeesEdit('{}')", fed.federation_id))}
155 {
156 "Cancel"
157 }
158 }
159 }
160
161
162 }
163 }
164 }
165 }
166 )
167}
168
169fn time_ago(t: SystemTime) -> String {
170 let now = fedimint_core::time::now();
171 let diff = match now.duration_since(t) {
172 Ok(d) => d,
173 Err(_) => Duration::from_secs(0),
174 };
175
176 let secs = diff.as_secs();
177
178 match secs {
179 0..=59 => format!("{} seconds ago", secs),
180 60..=3599 => format!("{} minutes ago", secs / 60),
181 _ => format!("{} hours ago", secs / 3600),
182 }
183}
184
185pub async fn leave_federation_handler<E: Display>(
186 State(state): State<UiState<DynGatewayApi<E>>>,
187 Path(id): Path<String>,
188 _auth: UserAuth,
189) -> impl IntoResponse {
190 let federation_id = FederationId::from_str(&id);
191 if let Ok(federation_id) = federation_id {
192 match state
193 .api
194 .handle_leave_federation(LeaveFedPayload { federation_id })
195 .await
196 {
197 Ok(info) => {
198 redirect_success(format!(
200 "Successfully left {}",
201 info.federation_name
202 .unwrap_or("Unnamed Federation".to_string())
203 ))
204 .into_response()
205 }
206 Err(err) => {
207 redirect_error(format!("Failed to leave federation: {err}")).into_response()
208 }
209 }
210 } else {
211 redirect_error("Failed to leave federation: Invalid federation id".to_string())
212 .into_response()
213 }
214}
215
216pub async fn set_fees_handler<E: Display>(
217 State(state): State<UiState<DynGatewayApi<E>>>,
218 _auth: UserAuth,
219 Form(payload): Form<SetFeesPayload>,
220) -> impl IntoResponse {
221 tracing::info!("Received fees payload: {:?}", payload);
222
223 match state.api.handle_set_fees_msg(payload).await {
224 Ok(_) => redirect_success("Successfully set fees".to_string()).into_response(),
225 Err(err) => redirect_error(format!("Failed to update fees: {err}")).into_response(),
226 }
227}