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