foundry_wallets/
wallet_signer.rs

1use crate::error::WalletSignerError;
2use alloy_consensus::SignableTransaction;
3use alloy_dyn_abi::TypedData;
4use alloy_network::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 std::{collections::HashSet, path::PathBuf};
13use tracing::warn;
14
15#[cfg(feature = "aws-kms")]
16use alloy_signer_aws::{AwsSigner, aws_config::BehaviorVersion, aws_sdk_kms::Client as AwsClient};
17
18#[cfg(feature = "gcp-kms")]
19use alloy_signer_gcp::{
20    GcpKeyRingRef, GcpSigner, GcpSignerError, KeySpecifier,
21    gcloud_sdk::{
22        GoogleApi,
23        google::cloud::kms::v1::key_management_service_client::KeyManagementServiceClient,
24    },
25};
26
27pub type Result<T> = std::result::Result<T, WalletSignerError>;
28
29/// Wrapper enum around different signers.
30#[derive(Debug)]
31pub enum WalletSigner {
32    /// Wrapper around local wallet. e.g. private key, mnemonic
33    Local(PrivateKeySigner),
34    /// Wrapper around Ledger signer.
35    Ledger(LedgerSigner),
36    /// Wrapper around Trezor signer.
37    Trezor(TrezorSigner),
38    /// Wrapper around AWS KMS signer.
39    #[cfg(feature = "aws-kms")]
40    Aws(AwsSigner),
41    /// Wrapper around Google Cloud KMS signer.
42    #[cfg(feature = "gcp-kms")]
43    Gcp(GcpSigner),
44}
45
46impl WalletSigner {
47    pub async fn from_ledger_path(path: LedgerHDPath) -> Result<Self> {
48        let ledger = LedgerSigner::new(path, None).await?;
49        Ok(Self::Ledger(ledger))
50    }
51
52    pub async fn from_trezor_path(path: TrezorHDPath) -> Result<Self> {
53        let trezor = TrezorSigner::new(path, None).await?;
54        Ok(Self::Trezor(trezor))
55    }
56
57    pub async fn from_aws(key_id: String) -> Result<Self> {
58        #[cfg(feature = "aws-kms")]
59        {
60            let config =
61                alloy_signer_aws::aws_config::load_defaults(BehaviorVersion::latest()).await;
62            let client = AwsClient::new(&config);
63
64            Ok(Self::Aws(
65                AwsSigner::new(client, key_id, None)
66                    .await
67                    .map_err(|e| WalletSignerError::Aws(Box::new(e)))?,
68            ))
69        }
70
71        #[cfg(not(feature = "aws-kms"))]
72        {
73            let _ = key_id;
74            Err(WalletSignerError::aws_unsupported())
75        }
76    }
77
78    pub async fn from_gcp(
79        project_id: String,
80        location: String,
81        keyring: String,
82        key_name: String,
83        key_version: u64,
84    ) -> Result<Self> {
85        #[cfg(feature = "gcp-kms")]
86        {
87            let keyring = GcpKeyRingRef::new(&project_id, &location, &keyring);
88            let client = match GoogleApi::from_function(
89                KeyManagementServiceClient::new,
90                "https://cloudkms.googleapis.com",
91                None,
92            )
93            .await
94            {
95                Ok(c) => c,
96                Err(e) => {
97                    return Err(WalletSignerError::Gcp(Box::new(GcpSignerError::GoogleKmsError(
98                        e,
99                    ))));
100                }
101            };
102
103            let specifier = KeySpecifier::new(keyring, &key_name, key_version);
104
105            Ok(Self::Gcp(
106                GcpSigner::new(client, specifier, None)
107                    .await
108                    .map_err(|e| WalletSignerError::Gcp(Box::new(e)))?,
109            ))
110        }
111
112        #[cfg(not(feature = "gcp-kms"))]
113        {
114            let _ = project_id;
115            let _ = location;
116            let _ = keyring;
117            let _ = key_name;
118            let _ = key_version;
119            Err(WalletSignerError::gcp_unsupported())
120        }
121    }
122
123    pub fn from_private_key(private_key: &B256) -> Result<Self> {
124        Ok(Self::Local(PrivateKeySigner::from_bytes(private_key)?))
125    }
126
127    /// Returns a list of addresses available to use with current signer
128    ///
129    /// - for Ledger and Trezor signers the number of addresses to retrieve is specified as argument
130    /// - the result for Ledger signers includes addresses available for both LedgerLive and Legacy
131    ///   derivation paths
132    /// - for Local and AWS signers the result contains a single address
133    /// - errors when retrieving addresses are logged but do not prevent returning available
134    ///   addresses
135    pub async fn available_senders(&self, max: usize) -> Result<Vec<Address>> {
136        let mut senders = HashSet::new();
137
138        match self {
139            Self::Local(local) => {
140                senders.insert(local.address());
141            }
142            Self::Ledger(ledger) => {
143                // Try LedgerLive derivation path
144                for i in 0..max {
145                    match ledger.get_address_with_path(&LedgerHDPath::LedgerLive(i)).await {
146                        Ok(address) => {
147                            senders.insert(address);
148                        }
149                        Err(e) => {
150                            warn!("Failed to get Ledger address at index {i} (LedgerLive): {e}");
151                        }
152                    }
153                }
154                // Try Legacy derivation path
155                for i in 0..max {
156                    match ledger.get_address_with_path(&LedgerHDPath::Legacy(i)).await {
157                        Ok(address) => {
158                            senders.insert(address);
159                        }
160                        Err(e) => {
161                            warn!("Failed to get Ledger address at index {i} (Legacy): {e}");
162                        }
163                    }
164                }
165            }
166            Self::Trezor(trezor) => {
167                for i in 0..max {
168                    match trezor.get_address_with_path(&TrezorHDPath::TrezorLive(i)).await {
169                        Ok(address) => {
170                            senders.insert(address);
171                        }
172                        Err(e) => {
173                            warn!("Failed to get Trezor address at index {i} (TrezorLive): {e}",);
174                        }
175                    }
176                }
177            }
178            #[cfg(feature = "aws-kms")]
179            Self::Aws(aws) => {
180                senders.insert(alloy_signer::Signer::address(aws));
181            }
182            #[cfg(feature = "gcp-kms")]
183            Self::Gcp(gcp) => {
184                senders.insert(alloy_signer::Signer::address(gcp));
185            }
186        }
187        Ok(senders.into_iter().collect())
188    }
189
190    pub fn from_mnemonic(
191        mnemonic: &str,
192        passphrase: Option<&str>,
193        derivation_path: Option<&str>,
194        index: u32,
195    ) -> Result<Self> {
196        let mut builder = MnemonicBuilder::<English>::default().phrase(mnemonic);
197
198        if let Some(passphrase) = passphrase {
199            builder = builder.password(passphrase)
200        }
201
202        builder = if let Some(hd_path) = derivation_path {
203            builder.derivation_path(hd_path)?
204        } else {
205            builder.index(index)?
206        };
207
208        Ok(Self::Local(builder.build()?))
209    }
210}
211
212macro_rules! delegate {
213    ($s:ident, $inner:ident => $e:expr) => {
214        match $s {
215            Self::Local($inner) => $e,
216            Self::Ledger($inner) => $e,
217            Self::Trezor($inner) => $e,
218            #[cfg(feature = "aws-kms")]
219            Self::Aws($inner) => $e,
220            #[cfg(feature = "gcp-kms")]
221            Self::Gcp($inner) => $e,
222        }
223    };
224}
225
226#[async_trait]
227impl Signer for WalletSigner {
228    /// Signs the given hash.
229    async fn sign_hash(&self, hash: &B256) -> alloy_signer::Result<Signature> {
230        delegate!(self, inner => inner.sign_hash(hash)).await
231    }
232
233    async fn sign_message(&self, message: &[u8]) -> alloy_signer::Result<Signature> {
234        delegate!(self, inner => inner.sign_message(message)).await
235    }
236
237    fn address(&self) -> Address {
238        delegate!(self, inner => alloy_signer::Signer::address(inner))
239    }
240
241    fn chain_id(&self) -> Option<ChainId> {
242        delegate!(self, inner => inner.chain_id())
243    }
244
245    fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
246        delegate!(self, inner => inner.set_chain_id(chain_id))
247    }
248
249    async fn sign_typed_data<T: SolStruct + Send + Sync>(
250        &self,
251        payload: &T,
252        domain: &Eip712Domain,
253    ) -> alloy_signer::Result<Signature>
254    where
255        Self: Sized,
256    {
257        delegate!(self, inner => inner.sign_typed_data(payload, domain)).await
258    }
259
260    async fn sign_dynamic_typed_data(
261        &self,
262        payload: &TypedData,
263    ) -> alloy_signer::Result<Signature> {
264        delegate!(self, inner => inner.sign_dynamic_typed_data(payload)).await
265    }
266}
267
268#[async_trait]
269impl TxSigner<Signature> for WalletSigner {
270    fn address(&self) -> Address {
271        delegate!(self, inner => alloy_signer::Signer::address(inner))
272    }
273
274    async fn sign_transaction(
275        &self,
276        tx: &mut dyn SignableTransaction<Signature>,
277    ) -> alloy_signer::Result<Signature> {
278        delegate!(self, inner => inner.sign_transaction(tx)).await
279    }
280}
281
282/// Signers that require user action to be obtained.
283#[derive(Debug, Clone)]
284pub enum PendingSigner {
285    Keystore(PathBuf),
286    Interactive,
287}
288
289impl PendingSigner {
290    pub fn unlock(self) -> Result<WalletSigner> {
291        match self {
292            Self::Keystore(path) => {
293                let password = rpassword::prompt_password("Enter keystore password:")?;
294                match PrivateKeySigner::decrypt_keystore(path, password) {
295                    Ok(signer) => Ok(WalletSigner::Local(signer)),
296                    Err(e) => match e {
297                        // Catch the `MacMismatch` error, which indicates an incorrect password and
298                        // return a more user-friendly `IncorrectKeystorePassword`.
299                        alloy_signer_local::LocalSignerError::EthKeystoreError(
300                            eth_keystore::KeystoreError::MacMismatch,
301                        ) => Err(WalletSignerError::IncorrectKeystorePassword),
302                        _ => Err(WalletSignerError::Local(e)),
303                    },
304                }
305            }
306            Self::Interactive => {
307                let private_key = rpassword::prompt_password("Enter private key:")?;
308                Ok(WalletSigner::from_private_key(&hex::FromHex::from_hex(private_key)?)?)
309            }
310        }
311    }
312}