foundry_wallets/wallet_multi/
mod.rs1use 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#[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)]
91#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
92pub struct MultiWalletOpts {
93 #[arg(long, help_heading = "Wallet options - raw", default_value = "0", value_name = "NUM")]
97 pub interactives: u32,
98
99 #[arg(long, short, help_heading = "Wallet options - raw", conflicts_with = "interactives")]
101 pub interactive: bool,
102
103 #[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 #[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 #[arg(long, alias = "mnemonic-paths", help_heading = "Wallet options - raw")]
120 #[builder(default = "None")]
121 pub mnemonics: Option<Vec<String>>,
122
123 #[arg(long, help_heading = "Wallet options - raw", value_name = "PASSPHRASE")]
125 #[builder(default = "None")]
126 pub mnemonic_passphrases: Option<Vec<String>>,
127
128 #[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 #[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 #[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 #[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 #[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 #[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 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
202 pub ledger: bool,
203
204 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
206 pub trezor: bool,
207
208 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "aws-kms"))]
213 pub aws: bool,
214
215 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
222 pub gcp: bool,
223
224 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "turnkey"))]
231 pub turnkey: bool,
232}
233
234impl MultiWalletOpts {
235 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 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 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 #[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}