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