foundry_wallets/wallet_browser/
handlers.rs

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