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    /// Use a browser wallet.
107    #[arg(long, help_heading = "Wallet options - browser")]
108    pub browser: bool,
109
110    /// Port for the browser wallet server.
111    #[arg(
112        long,
113        help_heading = "Wallet options - browser",
114        value_name = "PORT",
115        default_value = "9545",
116        requires = "browser"
117    )]
118    pub browser_port: u16,
119
120    /// Whether to open the browser for wallet connection.
121    #[arg(
122        long,
123        help_heading = "Wallet options - browser",
124        default_value_t = false,
125        requires = "browser"
126    )]
127    pub browser_disable_open: bool,
128
129    /// Enable development mode for the browser wallet.
130    /// This relaxes certain security features for local development.
131    ///
132    /// **WARNING**: This should only be used in a development environment.
133    #[arg(long, help_heading = "Wallet options - browser", hide = true)]
134    pub browser_development: bool,
135}
136
137impl WalletOpts {
138    pub async fn signer(&self) -> Result<WalletSigner> {
139        trace!("start finding signer");
140
141        let get_env = |key: &str| {
142            std::env::var(key)
143                .map_err(|_| eyre::eyre!("{key} environment variable is required for signer"))
144        };
145
146        let signer = if self.ledger {
147            utils::create_ledger_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
148                .await?
149        } else if self.trezor {
150            utils::create_trezor_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
151                .await?
152        } else if self.aws {
153            let key_id = get_env("AWS_KMS_KEY_ID")?;
154            WalletSigner::from_aws(key_id).await?
155        } else if self.gcp {
156            let project_id = get_env("GCP_PROJECT_ID")?;
157            let location = get_env("GCP_LOCATION")?;
158            let keyring = get_env("GCP_KEY_RING")?;
159            let key_name = get_env("GCP_KEY_NAME")?;
160            let key_version = get_env("GCP_KEY_VERSION")?
161                .parse()
162                .map_err(|_| eyre::eyre!("GCP_KEY_VERSION could not be parsed into u64"))?;
163            WalletSigner::from_gcp(project_id, location, keyring, key_name, key_version).await?
164        } else if self.turnkey {
165            let api_private_key = get_env("TURNKEY_API_PRIVATE_KEY")?;
166            let organization_id = get_env("TURNKEY_ORGANIZATION_ID")?;
167            let address_str = get_env("TURNKEY_ADDRESS")?;
168            let address = address_str.parse().map_err(|_| {
169                eyre::eyre!("TURNKEY_ADDRESS could not be parsed as an Ethereum address")
170            })?;
171            WalletSigner::from_turnkey(api_private_key, organization_id, address)?
172        } else if self.browser {
173            WalletSigner::from_browser(
174                self.browser_port,
175                !self.browser_disable_open,
176                self.browser_development,
177            )
178            .await?
179        } else if let Some(raw_wallet) = self.raw.signer()? {
180            raw_wallet
181        } else if let Some(path) = utils::maybe_get_keystore_path(
182            self.keystore_path.as_deref(),
183            self.keystore_account_name.as_deref(),
184        )? {
185            let (maybe_signer, maybe_pending) = utils::create_keystore_signer(
186                &path,
187                self.keystore_password.as_deref(),
188                self.keystore_password_file.as_deref(),
189            )?;
190            if let Some(pending) = maybe_pending {
191                pending.unlock()?
192            } else if let Some(signer) = maybe_signer {
193                signer
194            } else {
195                unreachable!()
196            }
197        } else {
198            eyre::bail!(
199                "\
200Error accessing local wallet. Did you pass a keystore, hardware wallet, private key or mnemonic?
201
202Run the command with --help flag for more information or use the corresponding CLI
203flag to set your key via:
204
205--keystore
206--interactive
207--private-key
208--mnemonic-path
209--aws
210--gcp
211--turnkey
212--trezor
213--ledger
214--browser
215
216Alternatively, when using the `cast send` or `cast mktx` commands with a local node
217or RPC that has unlocked accounts, the --unlocked or --ethsign flags can be used,
218respectively. The sender address can be specified by setting the `ETH_FROM` environment
219variable to the desired unlocked account address, or by providing the address directly
220using the --from flag."
221            )
222        };
223
224        Ok(signer)
225    }
226}
227
228impl From<RawWalletOpts> for WalletOpts {
229    fn from(options: RawWalletOpts) -> Self {
230        Self { raw: options, ..Default::default() }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use alloy_signer::Signer;
238    use std::{path::Path, str::FromStr};
239
240    #[tokio::test]
241    async fn find_keystore() {
242        let keystore =
243            Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore"));
244        let keystore_file = keystore
245            .join("UTC--2022-12-20T10-30-43.591916000Z--ec554aeafe75601aaab43bd4621a22284db566c2");
246        let password_file = keystore.join("password-ec554");
247        let wallet: WalletOpts = WalletOpts::parse_from([
248            "foundry-cli",
249            "--from",
250            "560d246fcddc9ea98a8b032c9a2f474efb493c28",
251            "--keystore",
252            keystore_file.to_str().unwrap(),
253            "--password-file",
254            password_file.to_str().unwrap(),
255        ]);
256        let signer = wallet.signer().await.unwrap();
257        assert_eq!(
258            signer.address(),
259            Address::from_str("ec554aeafe75601aaab43bd4621a22284db566c2").unwrap()
260        );
261    }
262
263    #[tokio::test]
264    async fn illformed_private_key_generates_user_friendly_error() {
265        let wallet = WalletOpts {
266            raw: RawWalletOpts {
267                interactive: false,
268                private_key: Some("123".to_string()),
269                mnemonic: None,
270                mnemonic_passphrase: None,
271                hd_path: None,
272                mnemonic_index: 0,
273            },
274            from: None,
275            keystore_path: None,
276            keystore_account_name: None,
277            keystore_password: None,
278            keystore_password_file: None,
279            ledger: false,
280            trezor: false,
281            aws: false,
282            gcp: false,
283            turnkey: false,
284            browser: false,
285            browser_port: 9545,
286            browser_development: false,
287            browser_disable_open: false,
288        };
289        match wallet.signer().await {
290            Ok(_) => {
291                panic!("illformed private key shouldn't decode")
292            }
293            Err(x) => {
294                assert!(
295                    x.to_string().contains("Failed to decode private key"),
296                    "Error message is not user-friendly"
297                );
298            }
299        }
300    }
301}