diff --git a/crates/cast/bin/cmd/wallet/mod.rs b/crates/cast/bin/cmd/wallet/mod.rs index acf578dc02bbb..7c58a3b0c17ba 100644 --- a/crates/cast/bin/cmd/wallet/mod.rs +++ b/crates/cast/bin/cmd/wallet/mod.rs @@ -5,12 +5,17 @@ use alloy_signer::{ }; use clap::Parser; use ethers_core::types::transaction::eip712::TypedData; -use ethers_signers::Signer; +use ethers_signers::{AwsSigner, HDPath as LedgerHDPath, Ledger, Signer, Trezor, TrezorHDPath}; use eyre::{Context, Result}; use foundry_common::{fs, types::ToAlloy}; use foundry_config::Config; use foundry_wallets::{RawWallet, Wallet}; use rand::thread_rng; +use rusoto_core::{ + credential::ChainProvider as AwsChainProvider, region::Region as AwsRegion, + request::HttpClient as AwsHttpClient, Client as AwsClient, +}; +use rusoto_kms::KmsClient; use serde_json::json; use std::{path::Path, str::FromStr}; use yansi::Paint; @@ -131,9 +136,29 @@ pub enum WalletSubcommands { #[clap(flatten)] raw_wallet_options: RawWallet, }, - /// List all the accounts in the keystore default directory + /// List accounts from keystore default directory, hardware ledger and AWS KMS. #[clap(visible_alias = "ls")] - List, + List { + /// List all the accounts in the keystore default directory. + #[clap(long, help_heading = "List local accounts")] + dir: bool, + + /// List accounts from a Ledger hardware wallet. + #[clap(long, short, help_heading = "List Ledger hardware wallet accounts")] + ledger: bool, + + /// List accounts from a Trezor hardware wallet. + #[clap(long, short, help_heading = "List Trezor hardware wallet accounts")] + trezor: bool, + + /// List accounts from AWS KMS. + #[clap(long, help_heading = "List AWS KMS account")] + aws: bool, + + /// List all configured accounts. + #[clap(long, help_heading = "List all accounts")] + all: bool, + }, /// Derives private key from mnemonic #[clap(name = "derive-private-key", visible_aliases = &["--derive-private-key"])] @@ -320,40 +345,123 @@ flag to set your key via: ); println!("{}", Paint::green(success_message)); } - WalletSubcommands::List => { - let default_keystore_dir = Config::foundry_keystores_dir() - .ok_or_else(|| eyre::eyre!("Could not find the default keystore directory."))?; - // Create the keystore directory if it doesn't exist - fs::create_dir_all(&default_keystore_dir)?; - // List all files in keystore directory - let keystore_files: Result, eyre::Report> = - std::fs::read_dir(&default_keystore_dir) - .wrap_err("Failed to read the directory")? - .filter_map(|entry| match entry { - Ok(entry) => { - let path = entry.path(); - if path.is_file() && path.extension().is_none() { - Some(Ok(path)) - } else { - None + WalletSubcommands::List { dir, ledger, trezor, aws, all } => { + // List local accounts, also by default if no option passed + if dir || all || !(dir || ledger || trezor || aws || all) { + let default_keystore_dir = + Config::foundry_keystores_dir().ok_or_else(|| { + eyre::eyre!("Could not find the default keystore directory.") + })?; + // Create the keystore directory if it doesn't exist + fs::create_dir_all(&default_keystore_dir)?; + // List all files in keystore directory + let keystore_files: Result, eyre::Report> = + std::fs::read_dir(&default_keystore_dir) + .wrap_err("Failed to read the directory")? + .filter_map(|entry| match entry { + Ok(entry) => { + let path = entry.path(); + if path.is_file() && path.extension().is_none() { + Some(Ok(path)) + } else { + None + } + } + Err(e) => Some(Err(e.into())), + }) + .collect::, eyre::Report>>(); + // Print the names of the keystore files + match keystore_files { + Ok(files) => { + println!("Keystore default directory"); + // Print the names of the keystore files + for file in files { + if let Some(file_name) = file.file_name() { + if let Some(name) = file_name.to_str() { + println!("- {}", name); + } } } - Err(e) => Some(Err(e.into())), - }) - .collect::, eyre::Report>>(); - // Print the names of the keystore files - match keystore_files { - Ok(files) => { - // Print the names of the keystore files - for file in files { - if let Some(file_name) = file.file_name() { - if let Some(name) = file_name.to_str() { - println!("{}", name); + } + Err(e) => { + println!("{}", e) + } + } + } + + // List ledger accounts + if ledger || all { + match Ledger::new(LedgerHDPath::LedgerLive(0), 0).await { + Ok(ledger) => { + let mut live_addrs = String::new(); + let mut legacy_addrs = String::new(); + // get first 2 addresses from live and legacy derivation paths + for i in 0..2 { + if let Ok(address) = + ledger.get_address_with_path(&LedgerHDPath::LedgerLive(i)).await + { + live_addrs.push_str(&format!(" -{}\n", address.to_alloy())); + } + + if let Ok(address) = + ledger.get_address_with_path(&LedgerHDPath::Legacy(i)).await + { + legacy_addrs.push_str(&format!(" -{}\n", address.to_alloy())); } } + println!( + "Ledger\n LedgerLive HD path\n{} ...\n Legacy HD path\n{} ...", + live_addrs, legacy_addrs + ); + } + Err(e) => { + println!("{}", e.to_string()) } } - Err(e) => return Err(e), + } + + // List Trezor accounts + if trezor || all { + match Trezor::new(TrezorHDPath::TrezorLive(0), 0, None).await { + Ok(trezor) => { + let mut addrs = String::new(); + // get first 2 addresses from live derivation path + for i in 0..2 { + if let Ok(address) = + trezor.get_address_with_path(&TrezorHDPath::TrezorLive(i)).await + { + addrs.push_str(&format!(" -{}\n", address.to_alloy())); + } + } + println!("Trezor\n{} ...", addrs); + } + Err(e) => { + println!("{}", e.to_string()) + } + } + } + + // List AWS account + if aws || all { + let key_ids = std::env::var("AWS_KMS_KEY_IDS") + .unwrap_or(std::env::var("AWS_KMS_KEY_ID").unwrap_or(String::new())); + + if key_ids.is_empty() { + println!("AWS KMS keys not configured"); + } else { + let client = AwsClient::new_with( + AwsChainProvider::default(), + AwsHttpClient::new().unwrap(), + ); + let kms = KmsClient::new_with_client(client, AwsRegion::default()); + let mut addrs = String::new(); + for key in key_ids.split(',') { + if let Ok(aws_signer) = AwsSigner::new(kms.clone(), key, 0).await { + addrs.push_str(&format!(" -{}\n", aws_signer.address().to_alloy())); + } + } + println!("AWS\n{}", addrs); + } } } WalletSubcommands::DerivePrivateKey { mnemonic, mnemonic_index } => { @@ -443,4 +551,27 @@ mod tests { _ => panic!("expected WalletSubcommands::Sign"), } } + + #[test] + fn can_parse_list_commands() { + let args = WalletSubcommands::parse_from([ + "foundry-cli", + "ls", + "--dir", + "--ledger", + "--trezor", + "--aws", + "--all", + ]); + match args { + WalletSubcommands::List { dir, ledger, trezor, aws, all } => { + assert!(dir); + assert!(ledger); + assert!(trezor); + assert!(aws); + assert!(all); + } + _ => panic!("expected WalletSubcommands::List"), + } + } } diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index e4009aa4af9f6..2399a7901f47a 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -131,6 +131,51 @@ casttest!(wallet_sign_typed_data_file, |_prj, cmd| { assert_eq!(output.trim(), "0x06c18bdc8163219fddc9afaf5a0550e381326474bb757c86dc32317040cf384e07a2c72ce66c1a0626b6750ca9b6c035bf6f03e7ed67ae2d1134171e9085c0b51b"); }); +// tests that `cast wallet list` outputs the local accounts +casttest!(wallet_list_local_list, |_prj, cmd| { + cmd.args(["wallet", "list"]); + let list_output = cmd.stdout_lossy(); + assert!(list_output.contains("Keystore default directory")); +}); + +// tests that `cast wallet list --dir` outputs the local accounts +casttest!(wallet_list_local_dir, |_prj, cmd| { + cmd.args(["wallet", "list", "--dir"]); + let list_output = cmd.stdout_lossy(); + assert!(list_output.contains("Keystore default directory")); +}); + +// tests that `cast wallet list --ledger` attempt to connect and list accounts from ledger wallet +casttest!(wallet_list_ledger, |_prj, cmd| { + cmd.args(["wallet", "list", "--ledger"]); + let list_output = cmd.stdout_lossy(); + assert!(list_output.contains("Ledger device not found")); +}); + +// tests that `cast wallet list --trezor` attempt to connect and list accounts from trezor wallet +casttest!(wallet_list_trezor, |_prj, cmd| { + cmd.args(["wallet", "list", "--trezor"]); + let list_output = cmd.stdout_lossy(); + assert!(list_output.contains("Trezor device not found")); +}); + +// tests that `cast wallet list --aws` attempt to use AWS KMS keys +casttest!(wallet_list_aws, |_prj, cmd| { + cmd.args(["wallet", "list", "--aws"]); + let list_output = cmd.stdout_lossy(); + assert!(list_output.contains("AWS KMS keys not configured")); +}); + +// tests that `cast wallet list --all` attempt to retrieve all keys +casttest!(wallet_list_all, |_prj, cmd| { + cmd.args(["wallet", "list", "--all"]); + let list_output = cmd.stdout_lossy(); + assert!(list_output.contains("Keystore default directory")); + assert!(list_output.contains("Ledger device not found")); + assert!(list_output.contains("Trezor device not found")); + assert!(list_output.contains("AWS KMS keys not configured")); +}); + // tests that `cast estimate` is working correctly. casttest!(estimate_function_gas, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint();