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