Skip to main content

foundry_wallets/wallet_browser/
signer.rs

1use 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    /// Send a transaction through the browser wallet.
62    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        // Not directly supported - use sign_dynamic_typed_data instead
129        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        // Parse the signature
157        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}