Skip to main content

foundry_wallets/wallet_browser/
handlers.rs

1use 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
23/// Serve index.html
24pub(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
36/// Serve styles.css
37pub(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
49/// Serve main.js with injected session token.
50pub(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
67/// Serve banner.png
68pub(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
75/// Serve logo.png
76pub(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
83/// Get the next pending transaction request.
84/// Route: GET /api/transaction/request
85pub(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
94/// Post a transaction response (signed or error).
95/// Route: POST /api/transaction/response
96pub(crate) async fn post_transaction_response<N: Network>(
97    State(state): State<Arc<BrowserWalletState<N>>>,
98    Json(body): Json<BrowserTransactionResponse>,
99) -> Json<BrowserApiResponse> {
100    // Ensure that the transaction request exists.
101    if !state.has_transaction_request(&body.id).await {
102        return Json(BrowserApiResponse::error("Unknown transaction id"));
103    }
104
105    // Ensure that exactly one of hash or error is provided.
106    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    // Validate transaction hash if provided.
117    if let Some(hash) = &body.hash {
118        // Check for all-zero hash
119        if hash.is_zero() {
120            return Json(BrowserApiResponse::error("Invalid (zero) transaction hash"));
121        }
122
123        // Sanity check: ensure the hash is exactly 32 bytes
124        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
136/// Get the next pending signing request.
137/// Route: GET /api/signing/request
138pub(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
147/// Post a signing response (signature or error).
148/// Route: POST /api/signing/response
149pub(crate) async fn post_signing_response<N: Network>(
150    State(state): State<Arc<BrowserWalletState<N>>>,
151    Json(body): Json<BrowserSignResponse>,
152) -> Json<BrowserApiResponse> {
153    // Ensure that the signing request exists.
154    if !state.has_signing_request(&body.id).await {
155        return Json(BrowserApiResponse::error("Unknown signing request id"));
156    }
157
158    // Ensure that exactly one of signature or error is provided.
159    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
176/// Get current connection information.
177/// Route: GET /api/connection
178pub(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
186/// Post connection update (connect or disconnect).
187/// Route: POST /api/connection
188pub(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}