Skip to main content

foundry_wallets/
opts.rs

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