foundry_wallets/
multi_wallet.rs

1use crate::{
2    utils,
3    wallet_signer::{PendingSigner, WalletSigner},
4};
5use alloy_primitives::map::AddressHashMap;
6use alloy_signer::Signer;
7use clap::Parser;
8use derive_builder::Builder;
9use eyre::Result;
10use foundry_config::Config;
11use serde::Serialize;
12use std::path::PathBuf;
13
14/// Container for multiple wallets.
15#[derive(Debug, Default)]
16pub struct MultiWallet {
17    /// Vector of wallets that require an action to be unlocked.
18    /// Those are lazily unlocked on the first access of the signers.
19    pending_signers: Vec<PendingSigner>,
20    /// Contains unlocked signers.
21    signers: AddressHashMap<WalletSigner>,
22}
23
24impl MultiWallet {
25    pub fn new(pending_signers: Vec<PendingSigner>, signers: Vec<WalletSigner>) -> Self {
26        let signers = signers.into_iter().map(|signer| (signer.address(), signer)).collect();
27        Self { pending_signers, signers }
28    }
29
30    fn maybe_unlock_pending(&mut self) -> Result<()> {
31        for pending in self.pending_signers.drain(..) {
32            let signer = pending.unlock()?;
33            self.signers.insert(signer.address(), signer);
34        }
35        Ok(())
36    }
37
38    pub fn signers(&mut self) -> Result<&AddressHashMap<WalletSigner>> {
39        self.maybe_unlock_pending()?;
40        Ok(&self.signers)
41    }
42
43    pub fn into_signers(mut self) -> Result<AddressHashMap<WalletSigner>> {
44        self.maybe_unlock_pending()?;
45        Ok(self.signers)
46    }
47
48    pub fn add_signer(&mut self, signer: WalletSigner) {
49        self.signers.insert(signer.address(), signer);
50    }
51}
52
53/// A macro that initializes multiple wallets
54///
55/// Should be used with a [`MultiWallet`] instance
56macro_rules! create_hw_wallets {
57    ($self:ident, $create_signer:expr, $signers:ident) => {
58        let mut $signers = vec![];
59
60        if let Some(hd_paths) = &$self.hd_paths {
61            for path in hd_paths {
62                let hw = $create_signer(Some(path), 0).await?;
63                $signers.push(hw);
64            }
65        }
66
67        if let Some(mnemonic_indexes) = &$self.mnemonic_indexes {
68            for index in mnemonic_indexes {
69                let hw = $create_signer(None, *index).await?;
70                $signers.push(hw);
71            }
72        }
73
74        if $signers.is_empty() {
75            let hw = $create_signer(None, 0).await?;
76            $signers.push(hw);
77        }
78    };
79}
80
81/// The wallet options can either be:
82/// 1. Ledger
83/// 2. Trezor
84/// 3. Mnemonics (via file path)
85/// 4. Keystores (via file path)
86/// 5. Private Keys (cleartext in CLI)
87/// 6. Private Keys (interactively via secure prompt)
88/// 7. AWS KMS
89#[derive(Builder, Clone, Debug, Default, Serialize, Parser)]
90#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
91pub struct MultiWalletOpts {
92    /// Open an interactive prompt to enter your private key.
93    ///
94    /// Takes a value for the number of keys to enter.
95    #[arg(
96        long,
97        short,
98        help_heading = "Wallet options - raw",
99        default_value = "0",
100        value_name = "NUM"
101    )]
102    pub interactives: u32,
103
104    /// Use the provided private keys.
105    #[arg(long, help_heading = "Wallet options - raw", value_name = "RAW_PRIVATE_KEYS")]
106    #[builder(default = "None")]
107    pub private_keys: Option<Vec<String>>,
108
109    /// Use the provided private key.
110    #[arg(
111        long,
112        help_heading = "Wallet options - raw",
113        conflicts_with = "private_keys",
114        value_name = "RAW_PRIVATE_KEY"
115    )]
116    #[builder(default = "None")]
117    pub private_key: Option<String>,
118
119    /// Use the mnemonic phrases of mnemonic files at the specified paths.
120    #[arg(long, alias = "mnemonic-paths", help_heading = "Wallet options - raw")]
121    #[builder(default = "None")]
122    pub mnemonics: Option<Vec<String>>,
123
124    /// Use a BIP39 passphrases for the mnemonic.
125    #[arg(long, help_heading = "Wallet options - raw", value_name = "PASSPHRASE")]
126    #[builder(default = "None")]
127    pub mnemonic_passphrases: Option<Vec<String>>,
128
129    /// The wallet derivation path.
130    ///
131    /// Works with both --mnemonic-path and hardware wallets.
132    #[arg(
133        long = "mnemonic-derivation-paths",
134        alias = "hd-paths",
135        help_heading = "Wallet options - raw",
136        value_name = "PATH"
137    )]
138    #[builder(default = "None")]
139    pub hd_paths: Option<Vec<String>>,
140
141    /// Use the private key from the given mnemonic index.
142    ///
143    /// Can be used with --mnemonics, --ledger, --aws and --trezor.
144    #[arg(
145        long,
146        conflicts_with = "hd_paths",
147        help_heading = "Wallet options - raw",
148        default_value = "0",
149        value_name = "INDEXES"
150    )]
151    pub mnemonic_indexes: Option<Vec<u32>>,
152
153    /// Use the keystore by its filename in the given folder.
154    #[arg(
155        long = "keystore",
156        visible_alias = "keystores",
157        help_heading = "Wallet options - keystore",
158        value_name = "PATHS",
159        env = "ETH_KEYSTORE"
160    )]
161    #[builder(default = "None")]
162    pub keystore_paths: Option<Vec<String>>,
163
164    /// Use a keystore from the default keystores folder (~/.foundry/keystores) by its filename.
165    #[arg(
166        long = "account",
167        visible_alias = "accounts",
168        help_heading = "Wallet options - keystore",
169        value_name = "ACCOUNT_NAMES",
170        env = "ETH_KEYSTORE_ACCOUNT",
171        conflicts_with = "keystore_paths"
172    )]
173    #[builder(default = "None")]
174    pub keystore_account_names: Option<Vec<String>>,
175
176    /// The keystore password.
177    ///
178    /// Used with --keystore.
179    #[arg(
180        long = "password",
181        help_heading = "Wallet options - keystore",
182        requires = "keystore_paths",
183        value_name = "PASSWORDS"
184    )]
185    #[builder(default = "None")]
186    pub keystore_passwords: Option<Vec<String>>,
187
188    /// The keystore password file path.
189    ///
190    /// Used with --keystore.
191    #[arg(
192        long = "password-file",
193        help_heading = "Wallet options - keystore",
194        requires = "keystore_paths",
195        value_name = "PATHS",
196        env = "ETH_PASSWORD"
197    )]
198    #[builder(default = "None")]
199    pub keystore_password_files: Option<Vec<String>>,
200
201    /// Use a Ledger hardware wallet.
202    #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
203    pub ledger: bool,
204
205    /// Use a Trezor hardware wallet.
206    #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
207    pub trezor: bool,
208
209    /// Use AWS Key Management Service.
210    #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "aws-kms"))]
211    pub aws: bool,
212
213    /// Use Google Cloud Key Management Service.
214    #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
215    pub gcp: bool,
216}
217
218impl MultiWalletOpts {
219    /// Returns [MultiWallet] container configured with provided options.
220    pub async fn get_multi_wallet(&self) -> Result<MultiWallet> {
221        let mut pending = Vec::new();
222        let mut signers: Vec<WalletSigner> = Vec::new();
223
224        if let Some(ledgers) = self.ledgers().await? {
225            signers.extend(ledgers);
226        }
227        if let Some(trezors) = self.trezors().await? {
228            signers.extend(trezors);
229        }
230        if let Some(aws_signers) = self.aws_signers().await? {
231            signers.extend(aws_signers);
232        }
233        if let Some(gcp_signer) = self.gcp_signers().await? {
234            signers.extend(gcp_signer);
235        }
236        if let Some((pending_keystores, unlocked)) = self.keystores()? {
237            pending.extend(pending_keystores);
238            signers.extend(unlocked);
239        }
240        if let Some(pks) = self.private_keys()? {
241            signers.extend(pks);
242        }
243        if let Some(mnemonics) = self.mnemonics()? {
244            signers.extend(mnemonics);
245        }
246        if self.interactives > 0 {
247            pending.extend(std::iter::repeat_n(
248                PendingSigner::Interactive,
249                self.interactives as usize,
250            ));
251        }
252
253        Ok(MultiWallet::new(pending, signers))
254    }
255
256    pub fn private_keys(&self) -> Result<Option<Vec<WalletSigner>>> {
257        let mut pks = vec![];
258        if let Some(private_key) = &self.private_key {
259            pks.push(private_key);
260        }
261        if let Some(private_keys) = &self.private_keys {
262            for pk in private_keys {
263                pks.push(pk);
264            }
265        }
266        if !pks.is_empty() {
267            let wallets = pks
268                .into_iter()
269                .map(|pk| utils::create_private_key_signer(pk))
270                .collect::<Result<Vec<_>>>()?;
271            Ok(Some(wallets))
272        } else {
273            Ok(None)
274        }
275    }
276
277    fn keystore_paths(&self) -> Result<Option<Vec<PathBuf>>> {
278        if let Some(keystore_paths) = &self.keystore_paths {
279            return Ok(Some(keystore_paths.iter().map(PathBuf::from).collect()));
280        }
281        if let Some(keystore_account_names) = &self.keystore_account_names {
282            let default_keystore_dir = Config::foundry_keystores_dir()
283                .ok_or_else(|| eyre::eyre!("Could not find the default keystore directory."))?;
284            return Ok(Some(
285                keystore_account_names
286                    .iter()
287                    .map(|keystore_name| default_keystore_dir.join(keystore_name))
288                    .collect(),
289            ));
290        }
291        Ok(None)
292    }
293
294    /// Returns all wallets read from the provided keystores arguments
295    ///
296    /// Returns `Ok(None)` if no keystore provided.
297    pub fn keystores(&self) -> Result<Option<(Vec<PendingSigner>, Vec<WalletSigner>)>> {
298        if let Some(keystore_paths) = self.keystore_paths()? {
299            let mut pending = Vec::new();
300            let mut signers = Vec::new();
301
302            let mut passwords_iter =
303                self.keystore_passwords.clone().unwrap_or_default().into_iter();
304
305            let mut password_files_iter =
306                self.keystore_password_files.clone().unwrap_or_default().into_iter();
307
308            for path in &keystore_paths {
309                let (maybe_signer, maybe_pending) = utils::create_keystore_signer(
310                    path,
311                    passwords_iter.next().as_deref(),
312                    password_files_iter.next().as_deref(),
313                )?;
314                if let Some(pending_signer) = maybe_pending {
315                    pending.push(pending_signer);
316                } else if let Some(signer) = maybe_signer {
317                    signers.push(signer);
318                }
319            }
320            return Ok(Some((pending, signers)));
321        }
322        Ok(None)
323    }
324
325    pub fn mnemonics(&self) -> Result<Option<Vec<WalletSigner>>> {
326        if let Some(ref mnemonics) = self.mnemonics {
327            let mut wallets = vec![];
328
329            let mut hd_paths_iter = self.hd_paths.clone().unwrap_or_default().into_iter();
330
331            let mut passphrases_iter =
332                self.mnemonic_passphrases.clone().unwrap_or_default().into_iter();
333
334            let mut indexes_iter = self.mnemonic_indexes.clone().unwrap_or_default().into_iter();
335
336            for mnemonic in mnemonics {
337                let wallet = utils::create_mnemonic_signer(
338                    mnemonic,
339                    passphrases_iter.next().as_deref(),
340                    hd_paths_iter.next().as_deref(),
341                    indexes_iter.next().unwrap_or(0),
342                )?;
343                wallets.push(wallet);
344            }
345            return Ok(Some(wallets));
346        }
347        Ok(None)
348    }
349
350    pub async fn ledgers(&self) -> Result<Option<Vec<WalletSigner>>> {
351        if self.ledger {
352            let mut args = self.clone();
353
354            if let Some(paths) = &args.hd_paths {
355                if paths.len() > 1 {
356                    eyre::bail!("Ledger only supports one signer.");
357                }
358                args.mnemonic_indexes = None;
359            }
360
361            create_hw_wallets!(args, utils::create_ledger_signer, wallets);
362            return Ok(Some(wallets));
363        }
364        Ok(None)
365    }
366
367    pub async fn trezors(&self) -> Result<Option<Vec<WalletSigner>>> {
368        if self.trezor {
369            create_hw_wallets!(self, utils::create_trezor_signer, wallets);
370            return Ok(Some(wallets));
371        }
372        Ok(None)
373    }
374
375    pub async fn aws_signers(&self) -> Result<Option<Vec<WalletSigner>>> {
376        #[cfg(feature = "aws-kms")]
377        if self.aws {
378            let mut wallets = vec![];
379            let aws_keys = std::env::var("AWS_KMS_KEY_IDS")
380                .or(std::env::var("AWS_KMS_KEY_ID"))?
381                .split(',')
382                .map(|k| k.to_string())
383                .collect::<Vec<_>>();
384
385            for key in aws_keys {
386                let aws_signer = WalletSigner::from_aws(key).await?;
387                wallets.push(aws_signer)
388            }
389
390            return Ok(Some(wallets));
391        }
392
393        Ok(None)
394    }
395
396    /// Returns a list of GCP signers if the GCP flag is set.
397    ///
398    /// The GCP signers are created from the following environment variables:
399    /// - GCP_PROJECT_ID: The GCP project ID. e.g. `my-project-123456`.
400    /// - GCP_LOCATION: The GCP location. e.g. `us-central1`.
401    /// - GCP_KEY_RING: The GCP key ring name. e.g. `my-key-ring`.
402    /// - GCP_KEY_NAME: The GCP key name. e.g. `my-key`.
403    /// - GCP_KEY_VERSION: The GCP key version. e.g. `1`.
404    ///
405    /// For more information on GCP KMS, see the [official documentation](https://cloud.google.com/kms/docs).
406    pub async fn gcp_signers(&self) -> Result<Option<Vec<WalletSigner>>> {
407        #[cfg(feature = "gcp-kms")]
408        if self.gcp {
409            let mut wallets = vec![];
410
411            let project_id = std::env::var("GCP_PROJECT_ID")?;
412            let location = std::env::var("GCP_LOCATION")?;
413            let key_ring = std::env::var("GCP_KEY_RING")?;
414            let key_names = std::env::var("GCP_KEY_NAME")?;
415            let key_version = std::env::var("GCP_KEY_VERSION")?;
416
417            let gcp_signer = WalletSigner::from_gcp(
418                project_id,
419                location,
420                key_ring,
421                key_names,
422                key_version.parse()?,
423            )
424            .await?;
425            wallets.push(gcp_signer);
426
427            return Ok(Some(wallets));
428        }
429
430        Ok(None)
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use alloy_primitives::address;
438    use std::path::Path;
439
440    #[test]
441    fn parse_keystore_args() {
442        let args: MultiWalletOpts =
443            MultiWalletOpts::parse_from(["foundry-cli", "--keystores", "my/keystore/path"]);
444        assert_eq!(args.keystore_paths, Some(vec!["my/keystore/path".to_string()]));
445
446        unsafe {
447            std::env::set_var("ETH_KEYSTORE", "MY_KEYSTORE");
448        }
449        let args: MultiWalletOpts = MultiWalletOpts::parse_from(["foundry-cli"]);
450        assert_eq!(args.keystore_paths, Some(vec!["MY_KEYSTORE".to_string()]));
451
452        unsafe {
453            std::env::remove_var("ETH_KEYSTORE");
454        }
455    }
456
457    #[test]
458    fn parse_keystore_password_file() {
459        let keystore =
460            Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore"));
461        let keystore_file = keystore
462            .join("UTC--2022-12-20T10-30-43.591916000Z--ec554aeafe75601aaab43bd4621a22284db566c2");
463
464        let keystore_password_file = keystore.join("password-ec554").into_os_string();
465
466        let args: MultiWalletOpts = MultiWalletOpts::parse_from([
467            "foundry-cli",
468            "--keystores",
469            keystore_file.to_str().unwrap(),
470            "--password-file",
471            keystore_password_file.to_str().unwrap(),
472        ]);
473        assert_eq!(
474            args.keystore_password_files,
475            Some(vec![keystore_password_file.to_str().unwrap().to_string()])
476        );
477
478        let (_, unlocked) = args.keystores().unwrap().unwrap();
479        assert_eq!(unlocked.len(), 1);
480        assert_eq!(unlocked[0].address(), address!("0xec554aeafe75601aaab43bd4621a22284db566c2"));
481    }
482
483    // https://github.com/foundry-rs/foundry/issues/5179
484    #[test]
485    fn should_not_require_the_mnemonics_flag_with_mnemonic_indexes() {
486        let wallet_options = vec![
487            ("ledger", "--mnemonic-indexes", 1),
488            ("trezor", "--mnemonic-indexes", 2),
489            ("aws", "--mnemonic-indexes", 10),
490        ];
491
492        for test_case in wallet_options {
493            let args: MultiWalletOpts = MultiWalletOpts::parse_from([
494                "foundry-cli",
495                &format!("--{}", test_case.0),
496                test_case.1,
497                &test_case.2.to_string(),
498            ]);
499
500            match test_case.0 {
501                "ledger" => assert!(args.ledger),
502                "trezor" => assert!(args.trezor),
503                "aws" => assert!(args.aws),
504                _ => panic!("Should have matched one of the previous wallet options"),
505            }
506
507            assert_eq!(
508                args.mnemonic_indexes.expect("--mnemonic-indexes should have been set")[0],
509                test_case.2
510            )
511        }
512    }
513}