fedimint_gateway_ui/
lib.rs

1mod bitcoin;
2mod connect_fed;
3mod federation;
4mod general;
5mod lightning;
6mod mnemonic;
7mod payment_summary;
8mod setup;
9
10use std::fmt::Display;
11use std::sync::Arc;
12
13use ::bitcoin::{Address, Txid};
14use async_trait::async_trait;
15use axum::extract::{Query, State};
16use axum::response::{Html, IntoResponse, Redirect};
17use axum::routing::{get, post};
18use axum::{Form, Router};
19use axum_extra::extract::CookieJar;
20use axum_extra::extract::cookie::{Cookie, SameSite};
21use fedimint_core::bitcoin::Network;
22use fedimint_core::secp256k1::serde::Deserialize;
23use fedimint_core::task::TaskGroup;
24use fedimint_gateway_common::{
25    ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, ConnectFedPayload,
26    CreateInvoiceForOperatorPayload, DepositAddressPayload, FederationInfo, GatewayBalances,
27    GatewayInfo, LeaveFedPayload, LightningMode, ListTransactionsPayload, ListTransactionsResponse,
28    MnemonicResponse, OpenChannelRequest, PayInvoiceForOperatorPayload, PaymentLogPayload,
29    PaymentLogResponse, PaymentSummaryPayload, PaymentSummaryResponse, ReceiveEcashPayload,
30    ReceiveEcashResponse, SendOnchainRequest, SetFeesPayload, SetMnemonicPayload,
31    SpendEcashPayload, SpendEcashResponse, WithdrawPayload, WithdrawPreviewPayload,
32    WithdrawPreviewResponse, WithdrawResponse,
33};
34use fedimint_ln_common::contracts::Preimage;
35use fedimint_logging::LOG_GATEWAY_UI;
36use fedimint_ui_common::assets::WithStaticRoutesExt;
37use fedimint_ui_common::auth::UserAuth;
38use fedimint_ui_common::{
39    LOGIN_ROUTE, LoginInput, ROOT_ROUTE, UiState, dashboard_layout, login_form_response,
40    login_layout,
41};
42use lightning_invoice::Bolt11Invoice;
43use maud::html;
44use tracing::debug;
45
46use crate::connect_fed::connect_federation_handler;
47use crate::federation::{
48    deposit_address_handler, leave_federation_handler, receive_ecash_handler, set_fees_handler,
49    spend_ecash_handler, withdraw_confirm_handler, withdraw_preview_handler,
50};
51use crate::lightning::{
52    channels_fragment_handler, close_channel_handler, create_bolt11_invoice_handler,
53    generate_receive_address_handler, open_channel_handler, pay_bolt11_invoice_handler,
54    payments_fragment_handler, send_onchain_handler, transactions_fragment_handler,
55    wallet_fragment_handler,
56};
57use crate::payment_summary::payment_log_fragment_handler;
58use crate::setup::{create_wallet_handler, recover_wallet_form, recover_wallet_handler};
59pub type DynGatewayApi<E> = Arc<dyn IAdminGateway<Error = E> + Send + Sync + 'static>;
60
61pub(crate) const OPEN_CHANNEL_ROUTE: &str = "/ui/channels/open";
62pub(crate) const CLOSE_CHANNEL_ROUTE: &str = "/ui/channels/close";
63pub(crate) const CHANNEL_FRAGMENT_ROUTE: &str = "/ui/channels/fragment";
64pub(crate) const LEAVE_FEDERATION_ROUTE: &str = "/ui/federations/{id}/leave";
65pub(crate) const CONNECT_FEDERATION_ROUTE: &str = "/ui/federations/join";
66pub(crate) const SET_FEES_ROUTE: &str = "/ui/federation/set-fees";
67pub(crate) const SEND_ONCHAIN_ROUTE: &str = "/ui/wallet/send";
68pub(crate) const WALLET_FRAGMENT_ROUTE: &str = "/ui/wallet/fragment";
69pub(crate) const LN_ONCHAIN_ADDRESS_ROUTE: &str = "/ui/wallet/receive";
70pub(crate) const DEPOSIT_ADDRESS_ROUTE: &str = "/ui/federations/deposit-address";
71pub(crate) const PAYMENTS_FRAGMENT_ROUTE: &str = "/ui/payments/fragment";
72pub(crate) const CREATE_BOLT11_INVOICE_ROUTE: &str = "/ui/payments/receive/bolt11";
73pub(crate) const PAY_BOLT11_INVOICE_ROUTE: &str = "/ui/payments/send/bolt11";
74pub(crate) const TRANSACTIONS_FRAGMENT_ROUTE: &str = "/ui/transactions/fragment";
75pub(crate) const RECEIVE_ECASH_ROUTE: &str = "/ui/federations/receive";
76pub(crate) const STOP_GATEWAY_ROUTE: &str = "/ui/stop";
77pub(crate) const WITHDRAW_PREVIEW_ROUTE: &str = "/ui/federations/withdraw-preview";
78pub(crate) const WITHDRAW_CONFIRM_ROUTE: &str = "/ui/federations/withdraw-confirm";
79pub(crate) const SPEND_ECASH_ROUTE: &str = "/ui/federations/spend";
80pub(crate) const PAYMENT_LOG_ROUTE: &str = "/ui/payment-log";
81pub(crate) const CREATE_WALLET_ROUTE: &str = "/ui/wallet/create";
82pub(crate) const RECOVER_WALLET_ROUTE: &str = "/ui/wallet/recover";
83
84#[derive(Default, Deserialize)]
85pub struct DashboardQuery {
86    pub success: Option<String>,
87    pub ui_error: Option<String>,
88}
89
90fn redirect_success(msg: String) -> impl IntoResponse {
91    let encoded: String = url::form_urlencoded::byte_serialize(msg.as_bytes()).collect();
92    Redirect::to(&format!("/?success={}", encoded))
93}
94
95fn redirect_error(msg: String) -> impl IntoResponse {
96    let encoded: String = url::form_urlencoded::byte_serialize(msg.as_bytes()).collect();
97    Redirect::to(&format!("/?ui_error={}", encoded))
98}
99
100pub fn is_allowed_setup_route(path: &str) -> bool {
101    path == ROOT_ROUTE
102        || path == LOGIN_ROUTE
103        || path.starts_with("/assets/")
104        || path == CREATE_WALLET_ROUTE
105        || path == RECOVER_WALLET_ROUTE
106}
107
108#[async_trait]
109pub trait IAdminGateway {
110    type Error;
111
112    async fn handle_get_info(&self) -> Result<GatewayInfo, Self::Error>;
113
114    async fn handle_list_channels_msg(
115        &self,
116    ) -> Result<Vec<fedimint_gateway_common::ChannelInfo>, Self::Error>;
117
118    async fn handle_payment_summary_msg(
119        &self,
120        PaymentSummaryPayload {
121            start_millis,
122            end_millis,
123        }: PaymentSummaryPayload,
124    ) -> Result<PaymentSummaryResponse, Self::Error>;
125
126    async fn handle_leave_federation(
127        &self,
128        payload: LeaveFedPayload,
129    ) -> Result<FederationInfo, Self::Error>;
130
131    async fn handle_connect_federation(
132        &self,
133        payload: ConnectFedPayload,
134    ) -> Result<FederationInfo, Self::Error>;
135
136    async fn handle_set_fees_msg(&self, payload: SetFeesPayload) -> Result<(), Self::Error>;
137
138    async fn handle_mnemonic_msg(&self) -> Result<MnemonicResponse, Self::Error>;
139
140    async fn handle_open_channel_msg(
141        &self,
142        payload: OpenChannelRequest,
143    ) -> Result<Txid, Self::Error>;
144
145    async fn handle_close_channels_with_peer_msg(
146        &self,
147        payload: CloseChannelsWithPeerRequest,
148    ) -> Result<CloseChannelsWithPeerResponse, Self::Error>;
149
150    async fn handle_get_balances_msg(&self) -> Result<GatewayBalances, Self::Error>;
151
152    async fn handle_send_onchain_msg(
153        &self,
154        payload: SendOnchainRequest,
155    ) -> Result<Txid, Self::Error>;
156
157    async fn handle_get_ln_onchain_address_msg(&self) -> Result<Address, Self::Error>;
158
159    async fn handle_deposit_address_msg(
160        &self,
161        payload: DepositAddressPayload,
162    ) -> Result<Address, Self::Error>;
163
164    async fn handle_receive_ecash_msg(
165        &self,
166        payload: ReceiveEcashPayload,
167    ) -> Result<ReceiveEcashResponse, Self::Error>;
168
169    async fn handle_create_invoice_for_operator_msg(
170        &self,
171        payload: CreateInvoiceForOperatorPayload,
172    ) -> Result<Bolt11Invoice, Self::Error>;
173
174    async fn handle_pay_invoice_for_operator_msg(
175        &self,
176        payload: PayInvoiceForOperatorPayload,
177    ) -> Result<Preimage, Self::Error>;
178
179    async fn handle_list_transactions_msg(
180        &self,
181        payload: ListTransactionsPayload,
182    ) -> Result<ListTransactionsResponse, Self::Error>;
183
184    async fn handle_spend_ecash_msg(
185        &self,
186        payload: SpendEcashPayload,
187    ) -> Result<SpendEcashResponse, Self::Error>;
188
189    async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> Result<(), Self::Error>;
190
191    fn get_task_group(&self) -> TaskGroup;
192
193    async fn handle_withdraw_msg(
194        &self,
195        payload: WithdrawPayload,
196    ) -> Result<WithdrawResponse, Self::Error>;
197
198    async fn handle_withdraw_preview_msg(
199        &self,
200        payload: WithdrawPreviewPayload,
201    ) -> Result<WithdrawPreviewResponse, Self::Error>;
202
203    async fn handle_payment_log_msg(
204        &self,
205        payload: PaymentLogPayload,
206    ) -> Result<PaymentLogResponse, Self::Error>;
207
208    fn get_password_hash(&self) -> String;
209
210    fn gatewayd_version(&self) -> String;
211
212    async fn get_chain_source(&self) -> (ChainSource, Network);
213
214    fn lightning_mode(&self) -> LightningMode;
215
216    async fn is_configured(&self) -> bool;
217
218    async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload)
219    -> Result<(), Self::Error>;
220}
221
222async fn login_form<E>(State(_state): State<UiState<DynGatewayApi<E>>>) -> impl IntoResponse {
223    login_form_response("Fedimint Gateway Login")
224}
225
226// Dashboard login submit handler
227async fn login_submit<E>(
228    State(state): State<UiState<DynGatewayApi<E>>>,
229    jar: CookieJar,
230    Form(input): Form<LoginInput>,
231) -> impl IntoResponse {
232    if let Ok(verify) = bcrypt::verify(input.password, &state.api.get_password_hash())
233        && verify
234    {
235        let mut cookie = Cookie::new(state.auth_cookie_name.clone(), state.auth_cookie_value);
236        cookie.set_path(ROOT_ROUTE);
237
238        cookie.set_http_only(true);
239        cookie.set_same_site(Some(SameSite::Lax));
240
241        let jar = jar.add(cookie);
242        return (jar, Redirect::to(ROOT_ROUTE)).into_response();
243    }
244
245    let content = html! {
246        div class="alert alert-danger" { "The password is invalid" }
247        div class="button-container" {
248            a href=(LOGIN_ROUTE) class="btn btn-primary setup-btn" { "Return to Login" }
249        }
250    };
251
252    Html(login_layout("Login Failed", content).into_string()).into_response()
253}
254
255async fn dashboard_view<E>(
256    State(state): State<UiState<DynGatewayApi<E>>>,
257    _auth: UserAuth,
258    Query(msg): Query<DashboardQuery>,
259) -> impl IntoResponse
260where
261    E: std::fmt::Display,
262{
263    // If gateway is not configured, show setup view instead of dashboard
264    if !state.api.is_configured().await {
265        return setup::setup_view(State(state), Query(msg))
266            .await
267            .into_response();
268    }
269
270    let gatewayd_version = state.api.gatewayd_version();
271    debug!(target: LOG_GATEWAY_UI, "Getting gateway info...");
272    let gateway_info = match state.api.handle_get_info().await {
273        Ok(info) => info,
274        Err(err) => {
275            let content = html! {
276                div class="alert alert-danger mt-4" {
277                    strong { "Failed to fetch gateway info: " }
278                    (err.to_string())
279                }
280            };
281            return Html(
282                dashboard_layout(content, "Fedimint Gateway UI", Some(&gatewayd_version))
283                    .into_string(),
284            )
285            .into_response();
286        }
287    };
288
289    let content = html! {
290
291       (federation::scripts())
292
293        @if let Some(success) = msg.success {
294            div class="alert alert-success mt-2 d-flex justify-content-between align-items-center" {
295                span { (success) }
296                a href=(ROOT_ROUTE)
297                class="ms-3 text-decoration-none text-dark fw-bold"
298                style="font-size: 1.5rem; line-height: 1; cursor: pointer;"
299                { "×" }
300            }
301        }
302        @if let Some(error) = msg.ui_error {
303            div class="alert alert-danger mt-2 d-flex justify-content-between align-items-center" {
304                span { (error) }
305                a href=(ROOT_ROUTE)
306                class="ms-3 text-decoration-none text-dark fw-bold"
307                style="font-size: 1.5rem; line-height: 1; cursor: pointer;"
308                { "×" }
309            }
310        }
311
312        div class="row mt-4" {
313            div class="col-md-12 text-end" {
314                form action=(STOP_GATEWAY_ROUTE) method="post" {
315                    button class="btn btn-outline-danger" type="submit"
316                        onclick="return confirm('Are you sure you want to safely stop the gateway? The gateway will wait for outstanding payments and then shutdown.');"
317                    {
318                        "Safely Stop Gateway"
319                    }
320                }
321            }
322        }
323
324        div class="row gy-4" {
325            div class="col-md-6" {
326                (general::render(&gateway_info))
327            }
328            div class="col-md-6" {
329                (payment_summary::render(&state.api, &gateway_info.federations).await)
330            }
331        }
332
333        div class="row gy-4 mt-2" {
334            div class="col-md-6" {
335                (bitcoin::render(&state.api).await)
336            }
337            div class="col-md-6" {
338                (mnemonic::render(&state.api).await)
339            }
340        }
341
342        div class="row gy-4 mt-2" {
343            div class="col-md-12" {
344                (lightning::render(&gateway_info, &state.api).await)
345            }
346        }
347
348        div class="row gy-4 mt-2" {
349            div class="col-md-12" {
350                (connect_fed::render())
351            }
352        }
353
354        @for fed in gateway_info.federations {
355            (federation::render(&fed))
356        }
357    };
358
359    Html(dashboard_layout(content, "Fedimint Gateway UI", Some(&gatewayd_version)).into_string())
360        .into_response()
361}
362
363async fn stop_gateway_handler<E>(
364    State(state): State<UiState<DynGatewayApi<E>>>,
365    _auth: UserAuth,
366) -> impl IntoResponse
367where
368    E: std::fmt::Display,
369{
370    match state
371        .api
372        .handle_shutdown_msg(state.api.get_task_group())
373        .await
374    {
375        Ok(_) => redirect_success("Gateway is safely shutting down...".to_string()).into_response(),
376        Err(err) => redirect_error(format!("Failed to stop gateway: {err}")).into_response(),
377    }
378}
379
380pub fn router<E: Display + Send + Sync + std::fmt::Debug + 'static>(
381    api: DynGatewayApi<E>,
382) -> Router {
383    let app = Router::new()
384        .route(ROOT_ROUTE, get(dashboard_view))
385        .route(LOGIN_ROUTE, get(login_form).post(login_submit))
386        .route(OPEN_CHANNEL_ROUTE, post(open_channel_handler))
387        .route(CLOSE_CHANNEL_ROUTE, post(close_channel_handler))
388        .route(CHANNEL_FRAGMENT_ROUTE, get(channels_fragment_handler))
389        .route(WALLET_FRAGMENT_ROUTE, get(wallet_fragment_handler))
390        .route(LEAVE_FEDERATION_ROUTE, post(leave_federation_handler))
391        .route(CONNECT_FEDERATION_ROUTE, post(connect_federation_handler))
392        .route(SET_FEES_ROUTE, post(set_fees_handler))
393        .route(SEND_ONCHAIN_ROUTE, post(send_onchain_handler))
394        .route(
395            LN_ONCHAIN_ADDRESS_ROUTE,
396            get(generate_receive_address_handler),
397        )
398        .route(DEPOSIT_ADDRESS_ROUTE, post(deposit_address_handler))
399        .route(SPEND_ECASH_ROUTE, post(spend_ecash_handler))
400        .route(RECEIVE_ECASH_ROUTE, post(receive_ecash_handler))
401        .route(PAYMENTS_FRAGMENT_ROUTE, get(payments_fragment_handler))
402        .route(
403            CREATE_BOLT11_INVOICE_ROUTE,
404            post(create_bolt11_invoice_handler),
405        )
406        .route(PAY_BOLT11_INVOICE_ROUTE, post(pay_bolt11_invoice_handler))
407        .route(
408            TRANSACTIONS_FRAGMENT_ROUTE,
409            get(transactions_fragment_handler),
410        )
411        .route(STOP_GATEWAY_ROUTE, post(stop_gateway_handler))
412        .route(WITHDRAW_PREVIEW_ROUTE, post(withdraw_preview_handler))
413        .route(WITHDRAW_CONFIRM_ROUTE, post(withdraw_confirm_handler))
414        .route(PAYMENT_LOG_ROUTE, get(payment_log_fragment_handler))
415        .route(CREATE_WALLET_ROUTE, post(create_wallet_handler))
416        .route(
417            RECOVER_WALLET_ROUTE,
418            get(recover_wallet_form).post(recover_wallet_handler),
419        )
420        .with_static_routes();
421
422    app.with_state(UiState::new(api))
423}