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::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    /// Send a transaction through the browser wallet.
63    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        // Not directly supported - use sign_dynamic_typed_data instead
130        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        // Parse the signature
158        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}