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#[derive(Debug, Default)]
16pub struct MultiWallet {
17 pending_signers: Vec<PendingSigner>,
20 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
53macro_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#[derive(Builder, Clone, Debug, Default, Serialize, Parser)]
90#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
91pub struct MultiWalletOpts {
92 #[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 #[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 #[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 #[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 #[arg(long, alias = "mnemonic-paths", help_heading = "Wallet options - raw")]
138 #[builder(default = "None")]
139 pub mnemonics: Option<Vec<String>>,
140
141 #[arg(long, help_heading = "Wallet options - raw", value_name = "PASSPHRASE")]
143 #[builder(default = "None")]
144 pub mnemonic_passphrases: Option<Vec<String>>,
145
146 #[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 #[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 #[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 #[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 #[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 #[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 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
220 pub ledger: bool,
221
222 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
224 pub trezor: bool,
225
226 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "aws-kms"))]
228 pub aws: bool,
229
230 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
232 pub gcp: bool,
233}
234
235impl MultiWalletOpts {
236 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 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 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 #[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}