fedimint_gateway_ui/
lib.rs

1mod 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
120// Dashboard login submit handler
121async 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}