fedimint_gateway_ui/
federation.rs

1use 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                        // READ-ONLY VERSION
71                        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                        // EDIT FORM (HIDDEN INITIALLY)
103                        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 back to dashboard after success
199                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}