fedimint_gateway_ui/
lib.rs1mod bitcoin;
2mod connect_fed;
3mod federation;
4mod general;
5mod lightning;
6mod mnemonic;
7mod payment_summary;
8
9use std::fmt::Display;
10use std::sync::Arc;
11
12use ::bitcoin::Txid;
13use async_trait::async_trait;
14use axum::extract::{Query, State};
15use axum::response::{Html, IntoResponse, Redirect};
16use axum::routing::{get, post};
17use axum::{Form, Router};
18use axum_extra::extract::CookieJar;
19use axum_extra::extract::cookie::{Cookie, SameSite};
20use fedimint_bitcoind::BlockchainInfo;
21use fedimint_core::bitcoin::Network;
22use fedimint_core::secp256k1::serde::Deserialize;
23use fedimint_gateway_common::{
24 ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, ConnectFedPayload,
25 FederationInfo, GatewayInfo, LeaveFedPayload, LightningMode, MnemonicResponse,
26 OpenChannelRequest, PaymentSummaryPayload, PaymentSummaryResponse, SetFeesPayload,
27};
28use fedimint_ui_common::assets::WithStaticRoutesExt;
29use fedimint_ui_common::auth::UserAuth;
30use fedimint_ui_common::{
31 LOGIN_ROUTE, LoginInput, ROOT_ROUTE, UiState, dashboard_layout, login_form_response,
32 login_layout,
33};
34use maud::html;
35
36use crate::connect_fed::connect_federation_handler;
37use crate::federation::{leave_federation_handler, set_fees_handler};
38use crate::lightning::{channels_fragment_handler, close_channel_handler, open_channel_handler};
39
40pub type DynGatewayApi<E> = Arc<dyn IAdminGateway<Error = E> + Send + Sync + 'static>;
41
42pub(crate) const OPEN_CHANNEL_ROUTE: &str = "/ui/channels/open";
43pub(crate) const CLOSE_CHANNEL_ROUTE: &str = "/ui/channels/close";
44pub(crate) const CHANNEL_FRAGMENT_ROUTE: &str = "/ui/channels/fragment";
45pub(crate) const LEAVE_FEDERATION_ROUTE: &str = "/ui/federations/{id}/leave";
46pub(crate) const CONNECT_FEDERATION_ROUTE: &str = "/ui/federations/join";
47pub(crate) const SET_FEES_ROUTE: &str = "/ui/federation/set-fees";
48
49#[derive(Default, Deserialize)]
50pub struct DashboardQuery {
51 pub success: Option<String>,
52 pub ui_error: Option<String>,
53}
54
55fn redirect_success(msg: String) -> impl IntoResponse {
56 let encoded: String = url::form_urlencoded::byte_serialize(msg.as_bytes()).collect();
57 Redirect::to(&format!("/?success={}", encoded))
58}
59
60fn redirect_error(msg: String) -> impl IntoResponse {
61 let encoded: String = url::form_urlencoded::byte_serialize(msg.as_bytes()).collect();
62 Redirect::to(&format!("/?ui_error={}", encoded))
63}
64
65#[async_trait]
66pub trait IAdminGateway {
67 type Error;
68
69 async fn handle_get_info(&self) -> Result<GatewayInfo, Self::Error>;
70
71 async fn handle_list_channels_msg(
72 &self,
73 ) -> Result<Vec<fedimint_gateway_common::ChannelInfo>, Self::Error>;
74
75 async fn handle_payment_summary_msg(
76 &self,
77 PaymentSummaryPayload {
78 start_millis,
79 end_millis,
80 }: PaymentSummaryPayload,
81 ) -> Result<PaymentSummaryResponse, Self::Error>;
82
83 async fn handle_leave_federation(
84 &self,
85 payload: LeaveFedPayload,
86 ) -> Result<FederationInfo, Self::Error>;
87
88 async fn handle_connect_federation(
89 &self,
90 payload: ConnectFedPayload,
91 ) -> Result<FederationInfo, Self::Error>;
92
93 async fn handle_set_fees_msg(&self, payload: SetFeesPayload) -> Result<(), Self::Error>;
94
95 async fn handle_mnemonic_msg(&self) -> Result<MnemonicResponse, Self::Error>;
96
97 async fn handle_open_channel_msg(
98 &self,
99 payload: OpenChannelRequest,
100 ) -> Result<Txid, Self::Error>;
101
102 async fn handle_close_channels_with_peer_msg(
103 &self,
104 payload: CloseChannelsWithPeerRequest,
105 ) -> Result<CloseChannelsWithPeerResponse, Self::Error>;
106
107 fn get_password_hash(&self) -> String;
108
109 fn gatewayd_version(&self) -> String;
110
111 async fn get_chain_source(&self) -> (Option<BlockchainInfo>, ChainSource, Network);
112
113 fn lightning_mode(&self) -> LightningMode;
114}
115
116async fn login_form<E>(State(_state): State<UiState<DynGatewayApi<E>>>) -> impl IntoResponse {
117 login_form_response("Fedimint Gateway Login")
118}
119
120async fn login_submit<E>(
122 State(state): State<UiState<DynGatewayApi<E>>>,
123 jar: CookieJar,
124 Form(input): Form<LoginInput>,
125) -> impl IntoResponse {
126 if let Ok(verify) = bcrypt::verify(input.password, &state.api.get_password_hash())
127 && verify
128 {
129 let mut cookie = Cookie::new(state.auth_cookie_name.clone(), state.auth_cookie_value);
130 cookie.set_path(ROOT_ROUTE);
131
132 cookie.set_http_only(true);
133 cookie.set_same_site(Some(SameSite::Lax));
134
135 let jar = jar.add(cookie);
136 return (jar, Redirect::to(ROOT_ROUTE)).into_response();
137 }
138
139 let content = html! {
140 div class="alert alert-danger" { "The password is invalid" }
141 div class="button-container" {
142 a href=(LOGIN_ROUTE) class="btn btn-primary setup-btn" { "Return to Login" }
143 }
144 };
145
146 Html(login_layout("Login Failed", content).into_string()).into_response()
147}
148
149async fn dashboard_view<E>(
150 State(state): State<UiState<DynGatewayApi<E>>>,
151 _auth: UserAuth,
152 Query(msg): Query<DashboardQuery>,
153) -> impl IntoResponse
154where
155 E: std::fmt::Display,
156{
157 let gatewayd_version = state.api.gatewayd_version();
158 let gateway_info = match state.api.handle_get_info().await {
159 Ok(info) => info,
160 Err(err) => {
161 let content = html! {
162 div class="alert alert-danger mt-4" {
163 strong { "Failed to fetch gateway info: " }
164 (err.to_string())
165 }
166 };
167 return Html(
168 dashboard_layout(content, "Fedimint Gateway UI", Some(&gatewayd_version))
169 .into_string(),
170 )
171 .into_response();
172 }
173 };
174
175 let content = html! {
176
177 (federation::scripts())
178
179 @if let Some(success) = msg.success {
180 div class="alert alert-success mt-2 d-flex justify-content-between align-items-center" {
181 span { (success) }
182 a href=(ROOT_ROUTE)
183 class="ms-3 text-decoration-none text-dark fw-bold"
184 style="font-size: 1.5rem; line-height: 1; cursor: pointer;"
185 { "×" }
186 }
187 }
188 @if let Some(error) = msg.ui_error {
189 div class="alert alert-danger mt-2 d-flex justify-content-between align-items-center" {
190 span { (error) }
191 a href=(ROOT_ROUTE)
192 class="ms-3 text-decoration-none text-dark fw-bold"
193 style="font-size: 1.5rem; line-height: 1; cursor: pointer;"
194 { "×" }
195 }
196 }
197
198 div class="row gy-4" {
199 div class="col-md-6" {
200 (general::render(&gateway_info))
201 }
202 div class="col-md-6" {
203 (payment_summary::render(&state.api).await)
204 }
205 }
206
207 div class="row gy-4 mt-2" {
208 div class="col-md-6" {
209 (bitcoin::render(&state.api).await)
210 }
211 div class="col-md-6" {
212 (mnemonic::render(&state.api).await)
213 }
214 }
215
216 div class="row gy-4 mt-2" {
217 div class="col-md-12" {
218 (lightning::render(&gateway_info, &state.api).await)
219 }
220 }
221
222 div class="row gy-4 mt-2" {
223 div class="col-md-12" {
224 (connect_fed::render())
225 }
226 }
227
228 @for fed in gateway_info.federations {
229 (federation::render(&fed))
230 }
231 };
232
233 Html(dashboard_layout(content, "Fedimint Gateway UI", Some(&gatewayd_version)).into_string())
234 .into_response()
235}
236
237pub fn router<E: Display + Send + Sync + 'static>(api: DynGatewayApi<E>) -> Router {
238 let app = Router::new()
239 .route(ROOT_ROUTE, get(dashboard_view))
240 .route(LOGIN_ROUTE, get(login_form).post(login_submit))
241 .route(OPEN_CHANNEL_ROUTE, post(open_channel_handler))
242 .route(CLOSE_CHANNEL_ROUTE, post(close_channel_handler))
243 .route(CHANNEL_FRAGMENT_ROUTE, get(channels_fragment_handler))
244 .route(LEAVE_FEDERATION_ROUTE, post(leave_federation_handler))
245 .route(CONNECT_FEDERATION_ROUTE, post(connect_federation_handler))
246 .route(SET_FEES_ROUTE, post(set_fees_handler))
247 .with_static_routes();
248
249 app.with_state(UiState::new(api))
250}