fedimint_gateway_ui/
setup.rs1use axum::Form;
2use axum::extract::{Query, State};
3use axum::response::{Html, IntoResponse, Redirect};
4use bip39::Language;
5use fedimint_gateway_common::SetMnemonicPayload;
6use fedimint_ui_common::{ROOT_ROUTE, UiState, login_layout};
7use maud::{PreEscaped, html};
8use serde::Deserialize;
9
10use crate::{
11 CREATE_WALLET_ROUTE, DashboardQuery, DynGatewayApi, RECOVER_WALLET_ROUTE, redirect_error,
12};
13
14#[derive(Deserialize)]
15pub struct RecoverWalletForm {
16 pub word1: String,
17 pub word2: String,
18 pub word3: String,
19 pub word4: String,
20 pub word5: String,
21 pub word6: String,
22 pub word7: String,
23 pub word8: String,
24 pub word9: String,
25 pub word10: String,
26 pub word11: String,
27 pub word12: String,
28}
29
30pub async fn setup_view<E>(
34 State(_state): State<UiState<DynGatewayApi<E>>>,
35 Query(msg): Query<DashboardQuery>,
36) -> impl IntoResponse
37where
38 E: std::fmt::Display,
39{
40 let content = html! {
41 @if let Some(error) = msg.ui_error {
42 div class="alert alert-danger mb-3" { (error) }
43 }
44 @if let Some(success) = msg.success {
45 div class="alert alert-success mb-3" { (success) }
46 }
47
48 p class="text-muted mb-4" {
49 "Your gateway needs to be configured before use. "
50 "Choose an option below to set up your wallet."
51 }
52
53 div class="d-grid gap-3" {
54 form action=(CREATE_WALLET_ROUTE) method="post" {
56 button type="submit" class="btn btn-primary btn-lg w-100" {
57 div class="fw-bold" { "Create New Wallet" }
58 small class="text-white" {
59 "Generate a new 12-word recovery phrase"
60 }
61 }
62 }
63
64 a href=(RECOVER_WALLET_ROUTE) class="btn btn-outline-secondary btn-lg w-100" {
66 div class="fw-bold" { "Recover Wallet" }
67 small class="text-secondary" {
68 "Use an existing 12-word recovery phrase"
69 }
70 }
71 }
72 };
73
74 Html(login_layout("Setup Gateway", content).into_string())
75}
76
77pub async fn create_wallet_handler<E>(
79 State(state): State<UiState<DynGatewayApi<E>>>,
80) -> impl IntoResponse
81where
82 E: std::fmt::Display,
83{
84 match state
85 .api
86 .handle_set_mnemonic_msg(SetMnemonicPayload { words: None })
87 .await
88 {
89 Ok(()) => Redirect::to(ROOT_ROUTE).into_response(),
90 Err(err) => redirect_error(format!("Failed to create wallet: {err}")).into_response(),
91 }
92}
93
94pub async fn recover_wallet_form<E>(
96 State(_state): State<UiState<DynGatewayApi<E>>>,
97 Query(msg): Query<DashboardQuery>,
98) -> impl IntoResponse
99where
100 E: std::fmt::Display,
101{
102 let content = html! {
103 @if let Some(error) = msg.ui_error {
104 div class="alert alert-danger mb-3" { (error) }
105 }
106
107 p class="text-muted mb-3" {
108 "Enter your 12-word recovery phrase to restore your wallet."
109 }
110
111 div class="alert alert-warning mb-3" {
112 strong { "Note: " }
113 "After recovery, you will need to re-join the federations you were previously connected to in order to recover your ecash."
114 }
115
116 form action=(RECOVER_WALLET_ROUTE) method="post" {
117 div class="d-flex flex-column flex-wrap gap-2 mb-3" style="height: 19.5rem;" {
118 @for i in 1..=12 {
119 div style="width: calc(50% - 0.25rem);" {
120 div class="input-group" {
121 span class="input-group-text" style="min-width: 3rem; justify-content: center;" {
122 (i)
123 }
124 input
125 type="text"
126 class="form-control"
127 id=(format!("word{}", i))
128 name=(format!("word{}", i))
129 placeholder=(format!("Word {}", i))
130 required
131 autocomplete="off"
132 autocapitalize="none"
133 spellcheck="false";
134 }
135 }
136 }
137 }
138
139 div class="d-flex gap-2" {
140 a href=(ROOT_ROUTE) class="btn btn-outline-secondary" { "Cancel" }
141 button type="submit" class="btn btn-primary flex-grow-1" {
142 "Recover Wallet"
143 }
144 }
145 }
146
147 script {
149 (PreEscaped(format!(
150 "const BIP39_WORDS = {};",
151 serde_json::to_string(&Language::English.word_list().to_vec()).expect("Failed to serialize BIP39 word list")
152 )))
153 (PreEscaped(r#"
154 const wordSet = new Set(BIP39_WORDS.map(w => w.toLowerCase()));
155
156 document.querySelectorAll('input[id^="word"]').forEach(input => {
157 input.addEventListener('input', function() {
158 const value = this.value.trim().toLowerCase();
159 this.classList.remove('is-valid', 'is-invalid');
160 if (value.length > 0) {
161 if (wordSet.has(value)) {
162 this.classList.add('is-valid');
163 } else {
164 this.classList.add('is-invalid');
165 }
166 }
167 });
168 });
169 "#))
170 }
171 };
172
173 Html(login_layout("Recover Wallet", content).into_string())
174}
175
176pub async fn recover_wallet_handler<E>(
178 State(state): State<UiState<DynGatewayApi<E>>>,
179 Form(form): Form<RecoverWalletForm>,
180) -> impl IntoResponse
181where
182 E: std::fmt::Display,
183{
184 let words = [
186 &form.word1,
187 &form.word2,
188 &form.word3,
189 &form.word4,
190 &form.word5,
191 &form.word6,
192 &form.word7,
193 &form.word8,
194 &form.word9,
195 &form.word10,
196 &form.word11,
197 &form.word12,
198 ]
199 .iter()
200 .map(|w| w.trim())
201 .collect::<Vec<_>>()
202 .join(" ");
203
204 match state
205 .api
206 .handle_set_mnemonic_msg(SetMnemonicPayload { words: Some(words) })
207 .await
208 {
209 Ok(()) => Redirect::to(ROOT_ROUTE).into_response(),
210 Err(err) => redirect_error(format!("Failed to recover wallet: {err}")).into_response(),
211 }
212}