foundry_wallets/wallet_browser/
handlers.rs1use std::sync::Arc;
2
3use axum::{
4 Json,
5 extract::State,
6 http::{
7 HeaderMap, HeaderValue,
8 header::{CACHE_CONTROL, CONTENT_TYPE, EXPIRES, PRAGMA},
9 },
10 response::Html,
11};
12
13use crate::wallet_browser::{
14 app::contents,
15 state::BrowserWalletState,
16 types::{
17 BrowserApiResponse, BrowserSignRequest, BrowserSignResponse, BrowserTransactionRequest,
18 BrowserTransactionResponse, Connection,
19 },
20};
21
22pub(crate) async fn serve_index() -> impl axum::response::IntoResponse {
24 let mut headers = HeaderMap::new();
25 headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8"));
26 headers.insert(
27 CACHE_CONTROL,
28 HeaderValue::from_static("no-store, no-cache, must-revalidate, max-age=0"),
29 );
30 headers.insert(PRAGMA, HeaderValue::from_static("no-cache"));
31 headers.insert(EXPIRES, HeaderValue::from_static("0"));
32 (headers, Html(contents::INDEX_HTML))
33}
34
35pub(crate) async fn serve_css() -> impl axum::response::IntoResponse {
37 let mut headers = HeaderMap::new();
38 headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/css; charset=utf-8"));
39 headers.insert(
40 CACHE_CONTROL,
41 HeaderValue::from_static("no-store, no-cache, must-revalidate, max-age=0"),
42 );
43 headers.insert(PRAGMA, HeaderValue::from_static("no-cache"));
44 headers.insert(EXPIRES, HeaderValue::from_static("0"));
45 (headers, contents::STYLES_CSS)
46}
47
48pub(crate) async fn serve_js(
50 State(state): State<Arc<BrowserWalletState>>,
51) -> impl axum::response::IntoResponse {
52 let token = state.session_token();
53 let js = format!("window.__SESSION_TOKEN__ = \"{}\";\n{}", token, contents::MAIN_JS);
54
55 let mut headers = HeaderMap::new();
56 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/javascript; charset=utf-8"));
57 headers.insert(
58 CACHE_CONTROL,
59 HeaderValue::from_static("no-store, no-cache, must-revalidate, max-age=0"),
60 );
61 headers.insert(PRAGMA, HeaderValue::from_static("no-cache"));
62 headers.insert(EXPIRES, HeaderValue::from_static("0"));
63 (headers, js)
64}
65
66pub(crate) async fn serve_banner_png() -> impl axum::response::IntoResponse {
68 let mut headers = HeaderMap::new();
69 headers.insert(CONTENT_TYPE, HeaderValue::from_static("image/png"));
70 headers.insert(CACHE_CONTROL, HeaderValue::from_static("public, max-age=31536000, immutable"));
71 (headers, contents::BANNER_PNG)
72}
73
74pub(crate) async fn serve_logo_png() -> impl axum::response::IntoResponse {
76 let mut headers = HeaderMap::new();
77 headers.insert(CONTENT_TYPE, HeaderValue::from_static("image/png"));
78 headers.insert(CACHE_CONTROL, HeaderValue::from_static("public, max-age=31536000, immutable"));
79 (headers, contents::LOGO_PNG)
80}
81
82pub(crate) async fn get_next_transaction_request(
85 State(state): State<Arc<BrowserWalletState>>,
86) -> Json<BrowserApiResponse<BrowserTransactionRequest>> {
87 match state.read_next_transaction_request().await {
88 Some(tx) => Json(BrowserApiResponse::with_data(tx)),
89 None => Json(BrowserApiResponse::error("No pending transaction request")),
90 }
91}
92
93pub(crate) async fn post_transaction_response(
96 State(state): State<Arc<BrowserWalletState>>,
97 Json(body): Json<BrowserTransactionResponse>,
98) -> Json<BrowserApiResponse> {
99 if !state.has_transaction_request(&body.id).await {
101 return Json(BrowserApiResponse::error("Unknown transaction id"));
102 }
103
104 match (&body.hash, &body.error) {
106 (None, None) => {
107 return Json(BrowserApiResponse::error("Either hash or error must be provided"));
108 }
109 (Some(_), Some(_)) => {
110 return Json(BrowserApiResponse::error("Only one of hash or error can be provided"));
111 }
112 _ => {}
113 }
114
115 if let Some(hash) = &body.hash {
117 if hash.is_zero() {
119 return Json(BrowserApiResponse::error("Invalid (zero) transaction hash"));
120 }
121
122 if hash.as_slice().len() != 32 {
124 return Json(BrowserApiResponse::error(
125 "Malformed transaction hash (expected 32 bytes)",
126 ));
127 }
128 }
129
130 state.add_transaction_response(body).await;
131
132 Json(BrowserApiResponse::ok())
133}
134
135pub(crate) async fn get_next_signing_request(
138 State(state): State<Arc<BrowserWalletState>>,
139) -> Json<BrowserApiResponse<BrowserSignRequest>> {
140 match state.read_next_signing_request().await {
141 Some(req) => Json(BrowserApiResponse::with_data(req)),
142 None => Json(BrowserApiResponse::error("No pending signing request")),
143 }
144}
145
146pub(crate) async fn post_signing_response(
149 State(state): State<Arc<BrowserWalletState>>,
150 Json(body): Json<BrowserSignResponse>,
151) -> Json<BrowserApiResponse> {
152 if !state.has_signing_request(&body.id).await {
154 return Json(BrowserApiResponse::error("Unknown signing request id"));
155 }
156
157 match (&body.signature, &body.error) {
159 (None, None) => {
160 return Json(BrowserApiResponse::error("Either signature or error must be provided"));
161 }
162 (Some(_), Some(_)) => {
163 return Json(BrowserApiResponse::error(
164 "Only one of signature or error can be provided",
165 ));
166 }
167 _ => {}
168 }
169
170 state.add_signing_response(body).await;
171
172 Json(BrowserApiResponse::ok())
173}
174
175pub(crate) async fn get_connection_info(
178 State(state): State<Arc<BrowserWalletState>>,
179) -> Json<BrowserApiResponse<Option<Connection>>> {
180 let connection = state.get_connection().await;
181
182 Json(BrowserApiResponse::with_data(connection))
183}
184
185pub(crate) async fn post_connection_update(
188 State(state): State<Arc<BrowserWalletState>>,
189 Json(body): Json<Option<Connection>>,
190) -> Json<BrowserApiResponse> {
191 state.set_connection(body).await;
192
193 Json(BrowserApiResponse::ok())
194}