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