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(
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 #[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 #[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 #[arg(long, alias = "mnemonic-paths", help_heading = "Wallet options - raw")]
122 #[builder(default = "None")]
123 pub mnemonics: Option<Vec<String>>,
124
125 #[arg(long, help_heading = "Wallet options - raw", value_name = "PASSPHRASE")]
127 #[builder(default = "None")]
128 pub mnemonic_passphrases: Option<Vec<String>>,
129
130 #[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 #[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 #[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 #[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 #[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 #[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 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
204 pub ledger: bool,
205
206 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
208 pub trezor: bool,
209
210 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "aws-kms"))]
215 pub aws: bool,
216
217 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
224 pub gcp: bool,
225
226 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "turnkey"))]
233 pub turnkey: bool,
234}
235
236impl MultiWalletOpts {
237 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 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 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 #[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}