Skip to main content

foundry_wallets/
multi_wallet.rs

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