foundry_wallets/wallet_browser/
signer.rs1use std::{
2 sync::Arc,
3 time::{Duration, Instant},
4};
5
6use alloy_consensus::SignableTransaction;
7use alloy_dyn_abi::TypedData;
8use alloy_network::TxSigner;
9use alloy_primitives::{Address, B256, ChainId, hex};
10use alloy_rpc_types::TransactionRequest;
11use alloy_signer::{Result, Signature, Signer, SignerSync};
12use alloy_sol_types::{Eip712Domain, SolStruct};
13use async_trait::async_trait;
14use tokio::sync::Mutex;
15use uuid::Uuid;
16
17use crate::wallet_browser::{
18 server::BrowserWalletServer,
19 types::{BrowserSignRequest, BrowserTransactionRequest, Connection, SignRequest, SignType},
20};
21
22#[derive(Clone, Debug)]
23pub struct BrowserSigner {
24 server: Arc<Mutex<BrowserWalletServer>>,
25 address: Address,
26 chain_id: ChainId,
27}
28
29impl BrowserSigner {
30 pub async fn new(
31 port: u16,
32 open_browser: bool,
33 timeout: Duration,
34 development: bool,
35 ) -> Result<Self> {
36 let mut server = BrowserWalletServer::new(port, open_browser, timeout, development);
37
38 server.start().await.map_err(alloy_signer::Error::other)?;
39
40 let _ = sh_warn!("Browser wallet is still in early development. Use with caution!");
41 let _ = sh_println!("Opening browser for wallet connection...");
42 let _ = sh_println!("Waiting for wallet connection...");
43
44 let start = Instant::now();
45
46 loop {
47 if let Some(Connection { address, chain_id }) = server.get_connection().await {
48 let _ = sh_println!("Wallet connected: {}", address);
49 let _ = sh_println!("Chain ID: {}", chain_id);
50
51 return Ok(Self { server: Arc::new(Mutex::new(server)), address, chain_id });
52 }
53
54 if start.elapsed() > timeout {
55 return Err(alloy_signer::Error::other("Wallet connection timeout"));
56 }
57
58 tokio::time::sleep(Duration::from_secs(1)).await;
59 }
60 }
61
62 pub async fn send_transaction_via_browser(
64 &self,
65 tx_request: TransactionRequest,
66 ) -> Result<B256> {
67 if let Some(from) = tx_request.from
68 && from != self.address
69 {
70 return Err(alloy_signer::Error::other(
71 "Transaction `from` address does not match connected wallet address",
72 ));
73 }
74
75 if let Some(chain_id) = tx_request.chain_id
76 && chain_id != self.chain_id
77 {
78 return Err(alloy_signer::Error::other(
79 "Transaction `chainId` does not match connected wallet chain ID",
80 ));
81 }
82
83 let request = BrowserTransactionRequest { id: Uuid::new_v4(), request: tx_request };
84
85 let server = self.server.lock().await;
86 let tx_hash =
87 server.request_transaction(request).await.map_err(alloy_signer::Error::other)?;
88
89 tokio::time::sleep(Duration::from_millis(500)).await;
90
91 Ok(tx_hash)
92 }
93}
94
95impl SignerSync for BrowserSigner {
96 fn sign_hash_sync(&self, _hash: &B256) -> Result<Signature> {
97 Err(alloy_signer::Error::other(
98 "Browser wallets cannot sign raw hashes. Use sign_message or send_transaction instead.",
99 ))
100 }
101
102 fn sign_message_sync(&self, _message: &[u8]) -> Result<Signature> {
103 Err(alloy_signer::Error::other(
104 "Browser signer requires async operations. Use sign_message instead.",
105 ))
106 }
107
108 fn chain_id_sync(&self) -> Option<ChainId> {
109 Some(self.chain_id)
110 }
111}
112
113#[async_trait]
114impl Signer for BrowserSigner {
115 async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
116 Err(alloy_signer::Error::other(
117 "Browser wallets sign and send transactions in one step. Use eth_sendTransaction instead.",
118 ))
119 }
120
121 async fn sign_typed_data<T: SolStruct + Send + Sync>(
122 &self,
123 _payload: &T,
124 _domain: &Eip712Domain,
125 ) -> Result<Signature>
126 where
127 Self: Sized,
128 {
129 Err(alloy_signer::Error::other(
131 "Browser wallets cannot sign typed data directly. Use sign_dynamic_typed_data instead.",
132 ))
133 }
134
135 async fn sign_message(&self, message: &[u8]) -> Result<Signature> {
136 let request = BrowserSignRequest {
137 id: Uuid::new_v4(),
138 sign_type: SignType::PersonalSign,
139 request: SignRequest { message: hex::encode_prefixed(message), address: self.address },
140 };
141
142 let server = self.server.lock().await;
143 let signature =
144 server.request_signing(request).await.map_err(alloy_signer::Error::other)?;
145
146 Signature::try_from(signature.as_ref())
147 .map_err(|e| alloy_signer::Error::other(format!("Invalid signature: {e}")))
148 }
149
150 async fn sign_dynamic_typed_data(&self, payload: &TypedData) -> Result<Signature> {
151 let server = self.server.lock().await;
152 let signature = server
153 .request_typed_data_signing(self.address, payload.clone())
154 .await
155 .map_err(alloy_signer::Error::other)?;
156
157 Signature::try_from(signature.as_ref())
159 .map_err(|e| alloy_signer::Error::other(format!("Invalid signature: {e}")))
160 }
161
162 fn address(&self) -> Address {
163 self.address
164 }
165
166 fn chain_id(&self) -> Option<ChainId> {
167 Some(self.chain_id)
168 }
169
170 fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
171 if let Some(id) = chain_id {
172 self.chain_id = id;
173 }
174 }
175}
176
177#[async_trait]
178impl TxSigner<Signature> for BrowserSigner {
179 fn address(&self) -> Address {
180 self.address
181 }
182
183 async fn sign_transaction(
184 &self,
185 _tx: &mut dyn SignableTransaction<Signature>,
186 ) -> Result<Signature> {
187 Err(alloy_signer::Error::other("Use send_transaction_via_browser for browser wallets"))
188 }
189}
190
191impl Drop for BrowserSigner {
192 fn drop(&mut self) {
193 let server = self.server.clone();
194
195 tokio::spawn(async move {
196 let mut server = server.lock().await;
197 let _ = server.stop().await;
198 });
199 }
200}