foundry_wallets/wallet_browser/
server.rs1use std::{
2 net::SocketAddr,
3 sync::Arc,
4 time::{Duration, Instant},
5};
6
7use alloy_dyn_abi::TypedData;
8use alloy_network::Network;
9use alloy_primitives::{Address, Bytes, TxHash};
10use tokio::{
11 net::TcpListener,
12 sync::{Mutex, oneshot},
13};
14use uuid::Uuid;
15
16use crate::wallet_browser::{
17 error::BrowserWalletError,
18 router::build_router,
19 state::BrowserWalletState,
20 types::{
21 BrowserSignRequest, BrowserSignTypedDataRequest, BrowserTransactionRequest, Connection,
22 SignRequest, SignType,
23 },
24};
25
26#[derive(Debug, Clone)]
28pub struct BrowserWalletServer<N: Network> {
29 port: u16,
30 state: Arc<BrowserWalletState<N>>,
31 shutdown_tx: Option<Arc<Mutex<Option<oneshot::Sender<()>>>>>,
32 open_browser: bool,
33 timeout: Duration,
34}
35
36impl<N: Network> BrowserWalletServer<N> {
37 pub fn new(port: u16, open_browser: bool, timeout: Duration, development: bool) -> Self {
39 Self {
40 port,
41 state: Arc::new(BrowserWalletState::new(Uuid::new_v4().to_string(), development)),
42 shutdown_tx: None,
43 open_browser,
44 timeout,
45 }
46 }
47
48 pub async fn start(&mut self) -> Result<(), BrowserWalletError> {
50 let router = build_router(self.state.clone(), self.port).await;
51
52 let addr = SocketAddr::from(([127, 0, 0, 1], self.port));
53 let listener = TcpListener::bind(addr)
54 .await
55 .map_err(|e| BrowserWalletError::ServerError(e.to_string()))?;
56 self.port = listener.local_addr().unwrap().port();
57
58 let (shutdown_tx, shutdown_rx) = oneshot::channel();
59 self.shutdown_tx = Some(Arc::new(Mutex::new(Some(shutdown_tx))));
60
61 tokio::spawn(async move {
62 let server = axum::serve(listener, router);
63 let _ = server
64 .with_graceful_shutdown(async {
65 let _ = shutdown_rx.await;
66 })
67 .await;
68 });
69
70 if self.open_browser {
71 webbrowser::open(&format!("http://127.0.0.1:{}", self.port)).map_err(|e| {
72 BrowserWalletError::ServerError(format!("Failed to open browser: {e}"))
73 })?;
74 }
75
76 Ok(())
77 }
78
79 pub async fn stop(&mut self) -> Result<(), BrowserWalletError> {
81 if let Some(shutdown_arc) = self.shutdown_tx.take()
82 && let Some(tx) = shutdown_arc.lock().await.take()
83 {
84 let _ = tx.send(());
85 }
86 Ok(())
87 }
88
89 pub fn port(&self) -> u16 {
91 self.port
92 }
93
94 pub fn open_browser(&self) -> bool {
96 self.open_browser
97 }
98
99 pub fn timeout(&self) -> Duration {
101 self.timeout
102 }
103
104 pub fn session_token(&self) -> &str {
106 self.state.session_token()
107 }
108
109 pub async fn is_connected(&self) -> bool {
111 self.state.is_connected().await
112 }
113
114 pub async fn get_connection(&self) -> Option<Connection> {
116 self.state.get_connection().await
117 }
118
119 pub async fn request_transaction(
121 &self,
122 request: BrowserTransactionRequest<N>,
123 ) -> Result<TxHash, BrowserWalletError> {
124 if !self.is_connected().await {
125 return Err(BrowserWalletError::NotConnected);
126 }
127
128 let tx_id = request.id;
129
130 self.state.add_transaction_request(request).await;
131
132 let start = Instant::now();
133
134 loop {
135 if let Some(response) = self.state.get_transaction_response(&tx_id).await {
136 if let Some(hash) = response.hash {
137 return Ok(hash);
138 } else if let Some(error) = response.error {
139 return Err(BrowserWalletError::Rejected {
140 operation: "Transaction",
141 reason: error,
142 });
143 }
144 return Err(BrowserWalletError::ServerError(
145 "Transaction response missing both hash and error".to_string(),
146 ));
147 }
148
149 if start.elapsed() > self.timeout {
150 self.state.remove_transaction_request(&tx_id).await;
151 return Err(BrowserWalletError::Timeout { operation: "Transaction" });
152 }
153
154 tokio::time::sleep(Duration::from_millis(100)).await;
155 }
156 }
157
158 pub async fn request_signing(
160 &self,
161 request: BrowserSignRequest,
162 ) -> Result<Bytes, BrowserWalletError> {
163 if !self.is_connected().await {
164 return Err(BrowserWalletError::NotConnected);
165 }
166
167 let tx_id = request.id;
168
169 self.state.add_signing_request(request).await;
170
171 let start = Instant::now();
172
173 loop {
174 if let Some(response) = self.state.get_signing_response(&tx_id).await {
175 if let Some(signature) = response.signature {
176 return Ok(signature);
177 } else if let Some(error) = response.error {
178 return Err(BrowserWalletError::Rejected {
179 operation: "Signing",
180 reason: error,
181 });
182 }
183 return Err(BrowserWalletError::ServerError(
184 "Signing response missing both signature and error".to_string(),
185 ));
186 }
187
188 if start.elapsed() > self.timeout {
189 self.state.remove_signing_request(&tx_id).await;
190 return Err(BrowserWalletError::Timeout { operation: "Signing" });
191 }
192
193 tokio::time::sleep(Duration::from_millis(100)).await;
194 }
195 }
196
197 pub async fn request_typed_data_signing(
199 &self,
200 address: Address,
201 typed_data: TypedData,
202 ) -> Result<Bytes, BrowserWalletError> {
203 let request = BrowserSignTypedDataRequest { id: Uuid::new_v4(), address, typed_data };
204
205 let sign_request = BrowserSignRequest {
206 id: request.id,
207 sign_type: SignType::SignTypedDataV4,
208 request: SignRequest {
209 message: serde_json::to_string(&request.typed_data).map_err(|e| {
210 BrowserWalletError::ServerError(format!("Failed to serialize typed data: {e}"))
211 })?,
212 address: request.address,
213 },
214 };
215
216 self.request_signing(sign_request).await
217 }
218}