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