Skip to content

Commit

Permalink
issue foundry-rs#6958: Include HW wallets in cast wallet ls
Browse files Browse the repository at this point in the history
- added new options for list command, default list local keystore

  --dir (alias to default)
      List all the accounts in the keystore default directory
  -l, --ledger
      List accounts from Ledger hardware wallet
  -t, --trezor
      List accounts from Trezor hardware wallet
  --aws
      List configured AWS accounts
  --all
      List accounts from keystore default directory, hardware wallets and AWS

- for ledger display 2 addresses for each ledger live and legacy HD
- for trezor display 2 addresses for trezor live HD path
- for AWS display all keys configured

- unit tests
  • Loading branch information
grandizzy committed Feb 14, 2024
1 parent 79ed5a9 commit 5c3f9a8
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 31 deletions.
193 changes: 162 additions & 31 deletions crates/cast/bin/cmd/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"])]
Expand Down Expand Up @@ -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<Vec<_>, 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<Vec<_>, 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::<Result<Vec<_>, 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::<Result<Vec<_>, 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 } => {
Expand Down Expand Up @@ -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"),
}
}
}
45 changes: 45 additions & 0 deletions crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 5c3f9a8

Please sign in to comment.