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