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