foundry_wallets/
wallet.rs

1use crate::{raw_wallet::RawWalletOpts, utils, wallet_signer::WalletSigner};
2use alloy_primitives::Address;
3use clap::Parser;
4use eyre::Result;
5use serde::Serialize;
6
7/// The wallet options can either be:
8/// 1. Raw (via private key / mnemonic file, see `RawWallet`)
9/// 2. Ledger
10/// 3. Trezor
11/// 4. Keystore (via file path)
12/// 5. AWS KMS
13/// 6. Google Cloud KMS
14#[derive(Clone, Debug, Default, Serialize, Parser)]
15#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
16pub struct WalletOpts {
17    /// The sender account.
18    #[arg(
19        long,
20        short,
21        value_name = "ADDRESS",
22        help_heading = "Wallet options - raw",
23        env = "ETH_FROM"
24    )]
25    pub from: Option<Address>,
26
27    #[command(flatten)]
28    pub raw: RawWalletOpts,
29
30    /// Use the keystore in the given folder or file.
31    #[arg(
32        long = "keystore",
33        help_heading = "Wallet options - keystore",
34        value_name = "PATH",
35        env = "ETH_KEYSTORE"
36    )]
37    pub keystore_path: Option<String>,
38
39    /// Use a keystore from the default keystores folder (~/.foundry/keystores) by its filename
40    #[arg(
41        long = "account",
42        help_heading = "Wallet options - keystore",
43        value_name = "ACCOUNT_NAME",
44        env = "ETH_KEYSTORE_ACCOUNT",
45        conflicts_with = "keystore_path"
46    )]
47    pub keystore_account_name: Option<String>,
48
49    /// The keystore password.
50    ///
51    /// Used with --keystore.
52    #[arg(
53        long = "password",
54        help_heading = "Wallet options - keystore",
55        requires = "keystore_path",
56        value_name = "PASSWORD"
57    )]
58    pub keystore_password: Option<String>,
59
60    /// The keystore password file path.
61    ///
62    /// Used with --keystore.
63    #[arg(
64        long = "password-file",
65        help_heading = "Wallet options - keystore",
66        requires = "keystore_path",
67        value_name = "PASSWORD_FILE",
68        env = "ETH_PASSWORD"
69    )]
70    pub keystore_password_file: Option<String>,
71
72    /// Use a Ledger hardware wallet.
73    #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
74    pub ledger: bool,
75
76    /// Use a Trezor hardware wallet.
77    #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
78    pub trezor: bool,
79
80    /// Use AWS Key Management Service.
81    ///
82    /// Ensure the AWS_KMS_KEY_ID environment variable is set.
83    #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "aws-kms"))]
84    pub aws: bool,
85
86    /// Use Google Cloud Key Management Service.
87    ///
88    /// Ensure the following environment variables are set: GCP_PROJECT_ID, GCP_LOCATION,
89    /// GCP_KEY_RING, GCP_KEY_NAME, GCP_KEY_VERSION.
90    ///
91    /// See: <https://cloud.google.com/kms/docs>
92    #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
93    pub gcp: bool,
94}
95
96impl WalletOpts {
97    pub async fn signer(&self) -> Result<WalletSigner> {
98        trace!("start finding signer");
99
100        let get_env = |key: &str| {
101            std::env::var(key)
102                .map_err(|_| eyre::eyre!("{key} environment variable is required for signer"))
103        };
104
105        let signer = if self.ledger {
106            utils::create_ledger_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
107                .await?
108        } else if self.trezor {
109            utils::create_trezor_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
110                .await?
111        } else if self.aws {
112            let key_id = get_env("AWS_KMS_KEY_ID")?;
113            WalletSigner::from_aws(key_id).await?
114        } else if self.gcp {
115            let project_id = get_env("GCP_PROJECT_ID")?;
116            let location = get_env("GCP_LOCATION")?;
117            let keyring = get_env("GCP_KEYRING")?;
118            let key_name = get_env("GCP_NAME")?;
119            let key_version = get_env("GCP_KEY_VERSION")?
120                .parse()
121                .map_err(|_| eyre::eyre!("GCP_KEY_VERSION could not be parsed into u64"))?;
122            WalletSigner::from_gcp(project_id, location, keyring, key_name, key_version).await?
123        } else if let Some(raw_wallet) = self.raw.signer()? {
124            raw_wallet
125        } else if let Some(path) = utils::maybe_get_keystore_path(
126            self.keystore_path.as_deref(),
127            self.keystore_account_name.as_deref(),
128        )? {
129            let (maybe_signer, maybe_pending) = utils::create_keystore_signer(
130                &path,
131                self.keystore_password.as_deref(),
132                self.keystore_password_file.as_deref(),
133            )?;
134            if let Some(pending) = maybe_pending {
135                pending.unlock()?
136            } else if let Some(signer) = maybe_signer {
137                signer
138            } else {
139                unreachable!()
140            }
141        } else {
142            eyre::bail!(
143                "\
144Error accessing local wallet. Did you pass a keystore, hardware wallet, private key or mnemonic?
145
146Run the command with --help flag for more information or use the corresponding CLI
147flag to set your key via:
148
149--keystore
150--interactive
151--private-key
152--mnemonic-path
153--aws
154--gcp
155--trezor
156--ledger
157
158Alternatively, when using the `cast send` or `cast mktx` commands with a local node
159or RPC that has unlocked accounts, the --unlocked or --ethsign flags can be used,
160respectively. The sender address can be specified by setting the `ETH_FROM` environment
161variable to the desired unlocked account address, or by providing the address directly
162using the --from flag."
163            )
164        };
165
166        Ok(signer)
167    }
168}
169
170impl From<RawWalletOpts> for WalletOpts {
171    fn from(options: RawWalletOpts) -> Self {
172        Self { raw: options, ..Default::default() }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use alloy_signer::Signer;
180    use std::{path::Path, str::FromStr};
181
182    #[tokio::test]
183    async fn find_keystore() {
184        let keystore =
185            Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore"));
186        let keystore_file = keystore
187            .join("UTC--2022-12-20T10-30-43.591916000Z--ec554aeafe75601aaab43bd4621a22284db566c2");
188        let password_file = keystore.join("password-ec554");
189        let wallet: WalletOpts = WalletOpts::parse_from([
190            "foundry-cli",
191            "--from",
192            "560d246fcddc9ea98a8b032c9a2f474efb493c28",
193            "--keystore",
194            keystore_file.to_str().unwrap(),
195            "--password-file",
196            password_file.to_str().unwrap(),
197        ]);
198        let signer = wallet.signer().await.unwrap();
199        assert_eq!(
200            signer.address(),
201            Address::from_str("ec554aeafe75601aaab43bd4621a22284db566c2").unwrap()
202        );
203    }
204
205    #[tokio::test]
206    async fn illformed_private_key_generates_user_friendly_error() {
207        let wallet = WalletOpts {
208            raw: RawWalletOpts {
209                interactive: false,
210                private_key: Some("123".to_string()),
211                mnemonic: None,
212                mnemonic_passphrase: None,
213                hd_path: None,
214                mnemonic_index: 0,
215            },
216            from: None,
217            keystore_path: None,
218            keystore_account_name: None,
219            keystore_password: None,
220            keystore_password_file: None,
221            ledger: false,
222            trezor: false,
223            aws: false,
224            gcp: false,
225        };
226        match wallet.signer().await {
227            Ok(_) => {
228                panic!("illformed private key shouldn't decode")
229            }
230            Err(x) => {
231                assert!(
232                    x.to_string().contains("Failed to decode private key"),
233                    "Error message is not user-friendly"
234                );
235            }
236        }
237    }
238}