foundry_wallets/wallet_multi/
mod.rs

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