foundry_wallets/
utils.rs

1use crate::{PendingSigner, WalletSigner, error::PrivateKeyError};
2use alloy_primitives::{B256, hex::FromHex};
3use alloy_signer_ledger::HDPath as LedgerHDPath;
4use alloy_signer_local::PrivateKeySigner;
5use alloy_signer_trezor::HDPath as TrezorHDPath;
6use eyre::{Context, Result};
7use foundry_config::Config;
8use std::{
9    fs,
10    path::{Path, PathBuf},
11};
12
13fn ensure_pk_not_env(pk: &str) -> Result<()> {
14    if !pk.starts_with("0x") && std::env::var(pk).is_ok() {
15        return Err(PrivateKeyError::ExistsAsEnvVar(pk.to_string()).into());
16    }
17    Ok(())
18}
19
20/// Validates and sanitizes user inputs, returning configured [WalletSigner].
21pub fn create_private_key_signer(private_key_str: &str) -> Result<WalletSigner> {
22    let Ok(private_key) = B256::from_hex(private_key_str) else {
23        ensure_pk_not_env(private_key_str)?;
24        eyre::bail!("Failed to decode private key")
25    };
26    match PrivateKeySigner::from_bytes(&private_key) {
27        Ok(pk) => Ok(WalletSigner::Local(pk)),
28        Err(err) => {
29            ensure_pk_not_env(private_key_str)?;
30            eyre::bail!("Failed to create wallet from private key: {err}")
31        }
32    }
33}
34
35/// Creates [WalletSigner] instance from given mnemonic parameters.
36///
37/// Mnemonic can be either a file path or a mnemonic phrase.
38pub fn create_mnemonic_signer(
39    mnemonic: &str,
40    passphrase: Option<&str>,
41    hd_path: Option<&str>,
42    index: u32,
43) -> Result<WalletSigner> {
44    let mnemonic = if Path::new(mnemonic).is_file() {
45        fs::read_to_string(mnemonic)?
46    } else {
47        mnemonic.to_owned()
48    };
49    let mnemonic = mnemonic.split_whitespace().collect::<Vec<_>>().join(" ");
50
51    Ok(WalletSigner::from_mnemonic(&mnemonic, passphrase, hd_path, index)?)
52}
53
54/// Creates [WalletSigner] instance from given Ledger parameters.
55pub async fn create_ledger_signer(
56    hd_path: Option<&str>,
57    mnemonic_index: u32,
58) -> Result<WalletSigner> {
59    let derivation = if let Some(hd_path) = hd_path {
60        LedgerHDPath::Other(hd_path.to_owned())
61    } else {
62        LedgerHDPath::LedgerLive(mnemonic_index as usize)
63    };
64
65    WalletSigner::from_ledger_path(derivation).await.wrap_err_with(|| {
66        "\
67Could not connect to Ledger device.
68Make sure it's connected and unlocked, with no other desktop wallet apps open."
69    })
70}
71
72/// Creates [WalletSigner] instance from given Trezor parameters.
73pub async fn create_trezor_signer(
74    hd_path: Option<&str>,
75    mnemonic_index: u32,
76) -> Result<WalletSigner> {
77    let derivation = if let Some(hd_path) = hd_path {
78        TrezorHDPath::Other(hd_path.to_owned())
79    } else {
80        TrezorHDPath::TrezorLive(mnemonic_index as usize)
81    };
82
83    WalletSigner::from_trezor_path(derivation).await.wrap_err_with(|| {
84        "\
85Could not connect to Trezor device.
86Make sure it's connected and unlocked, with no other conflicting desktop wallet apps open."
87    })
88}
89
90pub fn maybe_get_keystore_path(
91    maybe_path: Option<&str>,
92    maybe_name: Option<&str>,
93) -> Result<Option<PathBuf>> {
94    let default_keystore_dir = Config::foundry_keystores_dir()
95        .ok_or_else(|| eyre::eyre!("Could not find the default keystore directory."))?;
96    Ok(maybe_path
97        .map(PathBuf::from)
98        .or_else(|| maybe_name.map(|name| default_keystore_dir.join(name))))
99}
100
101/// Creates keystore signer from given parameters.
102///
103/// If correct password or password file is provided, the keystore is decrypted and a [WalletSigner]
104/// is returned.
105///
106/// Otherwise, a [PendingSigner] is returned, which can be used to unlock the keystore later,
107/// prompting user for password.
108pub fn create_keystore_signer(
109    path: &PathBuf,
110    maybe_password: Option<&str>,
111    maybe_password_file: Option<&str>,
112) -> Result<(Option<WalletSigner>, Option<PendingSigner>)> {
113    if !path.exists() {
114        eyre::bail!("Keystore file `{path:?}` does not exist")
115    }
116
117    if path.is_dir() {
118        eyre::bail!(
119            "Keystore path `{path:?}` is a directory. Please specify the keystore file directly."
120        )
121    }
122
123    let password = match (maybe_password, maybe_password_file) {
124        (Some(password), _) => Ok(Some(password.to_string())),
125        (_, Some(password_file)) => {
126            let password_file = Path::new(password_file);
127            if !password_file.is_file() {
128                Err(eyre::eyre!("Keystore password file `{password_file:?}` does not exist"))
129            } else {
130                Ok(Some(
131                    fs::read_to_string(password_file)
132                        .wrap_err_with(|| {
133                            format!("Failed to read keystore password file at {password_file:?}")
134                        })?
135                        .trim_end()
136                        .to_string(),
137                ))
138            }
139        }
140        (None, None) => Ok(None),
141    }?;
142
143    if let Some(password) = password {
144        let wallet = PrivateKeySigner::decrypt_keystore(path, password)
145            .wrap_err_with(|| format!("Failed to decrypt keystore {path:?}"))?;
146        Ok((Some(WalletSigner::Local(wallet)), None))
147    } else {
148        Ok((None, Some(PendingSigner::Keystore(path.clone()))))
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn parse_private_key_signer() {
158        let pk = B256::random();
159        let pk_str = pk.to_string();
160        assert!(create_private_key_signer(&pk_str).is_ok());
161        // skip 0x
162        assert!(create_private_key_signer(&pk_str[2..]).is_ok());
163    }
164}