foundry_wallets/
signer.rs

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