Skip to main content

foundry_wallets/
opts.rs

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