foundry_wallets/
multi_wallet.rs

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