foundry_wallets/
signer.rs

1use crate::{error::WalletSignerError, wallet_browser::signer::BrowserSigner};
2use alloy_consensus::{Sealed, SignableTransaction};
3use alloy_dyn_abi::TypedData;
4use alloy_network::{NetworkWallet, TransactionBuilder, TxSigner};
5use alloy_primitives::{Address, B256, ChainId, Signature, hex};
6use alloy_signer::Signer;
7use alloy_signer_ledger::{HDPath as LedgerHDPath, LedgerSigner};
8use alloy_signer_local::{MnemonicBuilder, PrivateKeySigner, coins_bip39::English};
9use alloy_signer_trezor::{HDPath as TrezorHDPath, TrezorSigner};
10use alloy_sol_types::{Eip712Domain, SolStruct};
11use async_trait::async_trait;
12use foundry_primitives::{
13    FoundryNetwork, FoundryTransactionRequest, FoundryTxEnvelope, FoundryTypedTx,
14};
15use std::{collections::HashSet, path::PathBuf, time::Duration};
16use tempo_primitives::TempoSignature;
17use tracing::warn;
18
19#[cfg(feature = "aws-kms")]
20use alloy_signer_aws::{AwsSigner, aws_config::BehaviorVersion, aws_sdk_kms::Client as AwsClient};
21
22#[cfg(feature = "gcp-kms")]
23use alloy_signer_gcp::{
24    GcpKeyRingRef, GcpSigner, GcpSignerError, KeySpecifier,
25    gcloud_sdk::{
26        GoogleApi,
27        google::cloud::kms::v1::key_management_service_client::KeyManagementServiceClient,
28    },
29};
30
31#[cfg(feature = "turnkey")]
32use alloy_signer_turnkey::TurnkeySigner;
33
34pub type Result<T> = std::result::Result<T, WalletSignerError>;
35
36/// Wrapper enum around different signers.
37#[derive(Debug)]
38pub enum WalletSigner {
39    /// Wrapper around local wallet. e.g. private key, mnemonic
40    Local(PrivateKeySigner),
41    /// Wrapper around Ledger signer.
42    Ledger(LedgerSigner),
43    /// Wrapper around Trezor signer.
44    Trezor(TrezorSigner),
45    /// Wrapper around browser wallet.
46    Browser(BrowserSigner),
47    /// Wrapper around AWS KMS signer.
48    #[cfg(feature = "aws-kms")]
49    Aws(AwsSigner),
50    /// Wrapper around Google Cloud KMS signer.
51    #[cfg(feature = "gcp-kms")]
52    Gcp(GcpSigner),
53    /// Wrapper around Turnkey signer.
54    #[cfg(feature = "turnkey")]
55    Turnkey(TurnkeySigner),
56}
57
58impl WalletSigner {
59    pub async fn from_ledger_path(path: LedgerHDPath) -> Result<Self> {
60        let ledger = LedgerSigner::new(path, None).await?;
61        Ok(Self::Ledger(ledger))
62    }
63
64    pub async fn from_trezor_path(path: TrezorHDPath) -> Result<Self> {
65        let trezor = TrezorSigner::new(path, None).await?;
66        Ok(Self::Trezor(trezor))
67    }
68
69    pub async fn from_browser(
70        port: u16,
71        open_browser: bool,
72        browser_development: bool,
73    ) -> Result<Self> {
74        let browser_signer =
75            BrowserSigner::new(port, open_browser, Duration::from_secs(300), browser_development)
76                .await
77                .map_err(|e| WalletSignerError::Browser(e.into()))?;
78        Ok(Self::Browser(browser_signer))
79    }
80
81    pub async fn from_aws(key_id: String) -> Result<Self> {
82        #[cfg(feature = "aws-kms")]
83        {
84            let config =
85                alloy_signer_aws::aws_config::load_defaults(BehaviorVersion::latest()).await;
86            let client = AwsClient::new(&config);
87
88            Ok(Self::Aws(
89                AwsSigner::new(client, key_id, None)
90                    .await
91                    .map_err(|e| WalletSignerError::Aws(Box::new(e)))?,
92            ))
93        }
94
95        #[cfg(not(feature = "aws-kms"))]
96        {
97            let _ = key_id;
98            Err(WalletSignerError::aws_unsupported())
99        }
100    }
101
102    pub async fn from_gcp(
103        project_id: String,
104        location: String,
105        keyring: String,
106        key_name: String,
107        key_version: u64,
108    ) -> Result<Self> {
109        #[cfg(feature = "gcp-kms")]
110        {
111            let keyring = GcpKeyRingRef::new(&project_id, &location, &keyring);
112            let client = match GoogleApi::from_function(
113                KeyManagementServiceClient::new,
114                "https://cloudkms.googleapis.com",
115                None,
116            )
117            .await
118            {
119                Ok(c) => c,
120                Err(e) => {
121                    return Err(WalletSignerError::Gcp(Box::new(GcpSignerError::GoogleKmsError(
122                        e,
123                    ))));
124                }
125            };
126
127            let specifier = KeySpecifier::new(keyring, &key_name, key_version);
128
129            Ok(Self::Gcp(
130                GcpSigner::new(client, specifier, None)
131                    .await
132                    .map_err(|e| WalletSignerError::Gcp(Box::new(e)))?,
133            ))
134        }
135
136        #[cfg(not(feature = "gcp-kms"))]
137        {
138            let _ = project_id;
139            let _ = location;
140            let _ = keyring;
141            let _ = key_name;
142            let _ = key_version;
143            Err(WalletSignerError::gcp_unsupported())
144        }
145    }
146
147    pub fn from_turnkey(
148        api_private_key: String,
149        organization_id: String,
150        address: Address,
151    ) -> Result<Self> {
152        #[cfg(feature = "turnkey")]
153        {
154            Ok(Self::Turnkey(TurnkeySigner::from_api_key(
155                &api_private_key,
156                organization_id,
157                address,
158                None,
159            )?))
160        }
161
162        #[cfg(not(feature = "turnkey"))]
163        {
164            let _ = api_private_key;
165            let _ = organization_id;
166            let _ = address;
167            Err(WalletSignerError::UnsupportedSigner("Turnkey"))
168        }
169    }
170
171    pub fn from_private_key(private_key: &B256) -> Result<Self> {
172        Ok(Self::Local(PrivateKeySigner::from_bytes(private_key)?))
173    }
174
175    /// Returns a list of addresses available to use with current signer
176    ///
177    /// - for Ledger and Trezor signers the number of addresses to retrieve is specified as argument
178    /// - the result for Ledger signers includes addresses available for both LedgerLive and Legacy
179    ///   derivation paths
180    /// - for Local and AWS signers the result contains a single address
181    /// - errors when retrieving addresses are logged but do not prevent returning available
182    ///   addresses
183    pub async fn available_senders(&self, max: usize) -> Result<Vec<Address>> {
184        let mut senders = HashSet::new();
185
186        match self {
187            Self::Local(local) => {
188                senders.insert(local.address());
189            }
190            Self::Ledger(ledger) => {
191                // Try LedgerLive derivation path
192                for i in 0..max {
193                    match ledger.get_address_with_path(&LedgerHDPath::LedgerLive(i)).await {
194                        Ok(address) => {
195                            senders.insert(address);
196                        }
197                        Err(e) => {
198                            warn!("Failed to get Ledger address at index {i} (LedgerLive): {e}");
199                        }
200                    }
201                }
202                // Try Legacy derivation path
203                for i in 0..max {
204                    match ledger.get_address_with_path(&LedgerHDPath::Legacy(i)).await {
205                        Ok(address) => {
206                            senders.insert(address);
207                        }
208                        Err(e) => {
209                            warn!("Failed to get Ledger address at index {i} (Legacy): {e}");
210                        }
211                    }
212                }
213            }
214            Self::Trezor(trezor) => {
215                for i in 0..max {
216                    match trezor.get_address_with_path(&TrezorHDPath::TrezorLive(i)).await {
217                        Ok(address) => {
218                            senders.insert(address);
219                        }
220                        Err(e) => {
221                            warn!("Failed to get Trezor address at index {i} (TrezorLive): {e}",);
222                        }
223                    }
224                }
225            }
226            Self::Browser(browser) => {
227                senders.insert(alloy_signer::Signer::address(browser));
228            }
229            #[cfg(feature = "aws-kms")]
230            Self::Aws(aws) => {
231                senders.insert(alloy_signer::Signer::address(aws));
232            }
233            #[cfg(feature = "gcp-kms")]
234            Self::Gcp(gcp) => {
235                senders.insert(alloy_signer::Signer::address(gcp));
236            }
237            #[cfg(feature = "turnkey")]
238            Self::Turnkey(turnkey) => {
239                senders.insert(alloy_signer::Signer::address(turnkey));
240            }
241        }
242        Ok(senders.into_iter().collect())
243    }
244
245    pub fn from_mnemonic(
246        mnemonic: &str,
247        passphrase: Option<&str>,
248        derivation_path: Option<&str>,
249        index: u32,
250    ) -> Result<Self> {
251        let mut builder = MnemonicBuilder::<English>::default().phrase(mnemonic);
252
253        if let Some(passphrase) = passphrase {
254            builder = builder.password(passphrase)
255        }
256
257        builder = if let Some(hd_path) = derivation_path {
258            builder.derivation_path(hd_path)?
259        } else {
260            builder.index(index)?
261        };
262
263        Ok(Self::Local(builder.build()?))
264    }
265}
266
267macro_rules! delegate {
268    ($s:ident, $inner:ident => $e:expr) => {
269        match $s {
270            Self::Local($inner) => $e,
271            Self::Ledger($inner) => $e,
272            Self::Trezor($inner) => $e,
273            Self::Browser($inner) => $e,
274            #[cfg(feature = "aws-kms")]
275            Self::Aws($inner) => $e,
276            #[cfg(feature = "gcp-kms")]
277            Self::Gcp($inner) => $e,
278            #[cfg(feature = "turnkey")]
279            Self::Turnkey($inner) => $e,
280        }
281    };
282}
283
284#[async_trait]
285impl Signer for WalletSigner {
286    /// Signs the given hash.
287    async fn sign_hash(&self, hash: &B256) -> alloy_signer::Result<Signature> {
288        delegate!(self, inner => inner.sign_hash(hash)).await
289    }
290
291    async fn sign_message(&self, message: &[u8]) -> alloy_signer::Result<Signature> {
292        delegate!(self, inner => inner.sign_message(message)).await
293    }
294
295    fn address(&self) -> Address {
296        delegate!(self, inner => alloy_signer::Signer::address(inner))
297    }
298
299    fn chain_id(&self) -> Option<ChainId> {
300        delegate!(self, inner => inner.chain_id())
301    }
302
303    fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
304        delegate!(self, inner => inner.set_chain_id(chain_id))
305    }
306
307    async fn sign_typed_data<T: SolStruct + Send + Sync>(
308        &self,
309        payload: &T,
310        domain: &Eip712Domain,
311    ) -> alloy_signer::Result<Signature>
312    where
313        Self: Sized,
314    {
315        delegate!(self, inner => inner.sign_typed_data(payload, domain)).await
316    }
317
318    async fn sign_dynamic_typed_data(
319        &self,
320        payload: &TypedData,
321    ) -> alloy_signer::Result<Signature> {
322        delegate!(self, inner => inner.sign_dynamic_typed_data(payload)).await
323    }
324}
325
326#[async_trait]
327impl TxSigner<Signature> for WalletSigner {
328    fn address(&self) -> Address {
329        Signer::address(self)
330    }
331
332    async fn sign_transaction(
333        &self,
334        tx: &mut dyn SignableTransaction<Signature>,
335    ) -> alloy_signer::Result<Signature> {
336        delegate!(self, inner => TxSigner::sign_transaction(inner, tx)).await
337    }
338}
339
340impl NetworkWallet<FoundryNetwork> for WalletSigner {
341    fn default_signer_address(&self) -> Address {
342        alloy_signer::Signer::address(self)
343    }
344
345    fn has_signer_for(&self, address: &Address) -> bool {
346        self.default_signer_address() == *address
347    }
348
349    fn signer_addresses(&self) -> impl Iterator<Item = Address> {
350        std::iter::once(self.default_signer_address())
351    }
352
353    async fn sign_transaction_from(
354        &self,
355        sender: Address,
356        tx: FoundryTypedTx,
357    ) -> alloy_signer::Result<FoundryTxEnvelope> {
358        if sender != self.default_signer_address() {
359            return Err(alloy_signer::Error::other("Signer address mismatch"));
360        }
361
362        match tx {
363            FoundryTypedTx::Legacy(mut inner) => {
364                let sig = TxSigner::sign_transaction(self, &mut inner).await?;
365                Ok(FoundryTxEnvelope::Legacy(inner.into_signed(sig)))
366            }
367            FoundryTypedTx::Eip2930(mut inner) => {
368                let sig = TxSigner::sign_transaction(self, &mut inner).await?;
369                Ok(FoundryTxEnvelope::Eip2930(inner.into_signed(sig)))
370            }
371            FoundryTypedTx::Eip1559(mut inner) => {
372                let sig = TxSigner::sign_transaction(self, &mut inner).await?;
373                Ok(FoundryTxEnvelope::Eip1559(inner.into_signed(sig)))
374            }
375            FoundryTypedTx::Eip4844(mut inner) => {
376                let sig = TxSigner::sign_transaction(self, &mut inner).await?;
377                Ok(FoundryTxEnvelope::Eip4844(inner.into_signed(sig)))
378            }
379            FoundryTypedTx::Eip7702(mut inner) => {
380                let sig = TxSigner::sign_transaction(self, &mut inner).await?;
381                Ok(FoundryTxEnvelope::Eip7702(inner.into_signed(sig)))
382            }
383            FoundryTypedTx::Deposit(inner) => {
384                // Deposit transactions don't require signing
385                Ok(FoundryTxEnvelope::Deposit(Sealed::new(inner)))
386            }
387            FoundryTypedTx::Tempo(mut inner) => {
388                let sig = TxSigner::sign_transaction(self, &mut inner).await?;
389                let tempo_sig: TempoSignature = sig.into();
390                Ok(FoundryTxEnvelope::Tempo(inner.into_signed(tempo_sig)))
391            }
392        }
393    }
394
395    #[doc(hidden)]
396    async fn sign_request(
397        &self,
398        request: FoundryTransactionRequest,
399    ) -> alloy_signer::Result<FoundryTxEnvelope> {
400        let sender = request.from().unwrap_or_else(|| self.default_signer_address());
401        let tx = request.build_typed_tx().map_err(|_| {
402            alloy_signer::Error::other("Failed to build typed transaction from request")
403        })?;
404        self.sign_transaction_from(sender, tx).await
405    }
406}
407
408/// Signers that require user action to be obtained.
409#[derive(Debug, Clone)]
410pub enum PendingSigner {
411    Keystore(PathBuf),
412    Interactive,
413}
414
415impl PendingSigner {
416    pub fn unlock(self) -> Result<WalletSigner> {
417        match self {
418            Self::Keystore(path) => {
419                let password = rpassword::prompt_password("Enter keystore password:")?;
420                match PrivateKeySigner::decrypt_keystore(path, password) {
421                    Ok(signer) => Ok(WalletSigner::Local(signer)),
422                    Err(e) => match e {
423                        // Catch the `MacMismatch` error, which indicates an incorrect password and
424                        // return a more user-friendly `IncorrectKeystorePassword`.
425                        alloy_signer_local::LocalSignerError::EthKeystoreError(
426                            eth_keystore::KeystoreError::MacMismatch,
427                        ) => Err(WalletSignerError::IncorrectKeystorePassword),
428                        _ => Err(WalletSignerError::Local(e)),
429                    },
430                }
431            }
432            Self::Interactive => {
433                let private_key = rpassword::prompt_password("Enter private key:")?;
434                Ok(WalletSigner::from_private_key(&hex::FromHex::from_hex(private_key)?)?)
435            }
436        }
437    }
438}