Skip to main content

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