Skip to content

Commit

Permalink
feat(cast wallet list) issue #6958: Include HW wallets in cast wall…
Browse files Browse the repository at this point in the history
…et ls (#7123)

* issue #6958: Include HW wallets in cast wallet ls

* Changes after review:
- use annotations for builder defaults
- handle Local signer in available_senders, return Ledger addresses for legacy derivation, add doc
- fix condition to list files in keystore dir
- simplify creation of keystore default directory

* Changes after review: use list_signers macro

* Changes after review:
- remove help_headings
- remove match and use ? as dir already exists
- remove async from list_local_senders fn
- move Ok(senders) at the bottom of available_senders fn
- list_senders doesn't need match as available_senders cannot fail
- make max_senders arg for ls command , default 3

* Nit

* Remove list_senders fn, move logic in macro

* Nit macro
  • Loading branch information
grandizzy authored Feb 22, 2024
1 parent 9fe9a3f commit 57815e0
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 38 deletions.
73 changes: 73 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 108 additions & 0 deletions crates/cast/bin/cmd/wallet/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use clap::Parser;
use eyre::Result;

use foundry_common::{fs, types::ToAlloy};
use foundry_config::Config;
use foundry_wallets::multi_wallet::MultiWalletOptsBuilder;

/// CLI arguments for `cast wallet list`.
#[derive(Clone, Debug, Parser)]
pub struct ListArgs {
/// List all the accounts in the keystore directory.
/// Default keystore directory is used if no path provided.
#[clap(long, default_missing_value = "", num_args(0..=1))]
dir: Option<String>,

/// List accounts from a Ledger hardware wallet.
#[clap(long, short, group = "hw-wallets")]
ledger: bool,

/// List accounts from a Trezor hardware wallet.
#[clap(long, short, group = "hw-wallets")]
trezor: bool,

/// List accounts from AWS KMS.
#[clap(long)]
aws: bool,

/// List all configured accounts.
#[clap(long, group = "hw-wallets")]
all: bool,

/// Max number of addresses to display from hardware wallets.
#[clap(long, short, default_value = "3", requires = "hw-wallets")]
max_senders: Option<usize>,
}

impl ListArgs {
pub async fn run(self) -> Result<()> {
// list local accounts as files in keystore dir, no need to unlock / provide password
if self.dir.is_some() || self.all || (!self.ledger && !self.trezor && !self.aws) {
let _ = self.list_local_senders();
}

// Create options for multi wallet - ledger, trezor and AWS
let list_opts = MultiWalletOptsBuilder::default()
.ledger(self.ledger || self.all)
.mnemonic_indexes(Some(vec![0]))
.trezor(self.trezor || self.all)
.aws(self.aws || self.all)
.interactives(0)
.build()
.expect("build multi wallet");

// macro to print senders for a list of signers
macro_rules! list_senders {
($signers:expr, $label:literal) => {
match $signers.await {
Ok(signers) => {
for signer in signers.unwrap_or_default().iter() {
signer
.available_senders(self.max_senders.unwrap())
.await?
.iter()
.for_each(|sender| println!("{} ({})", sender.to_alloy(), $label));
}
}
Err(e) => {
if !self.all {
println!("{}", e)
}
}
}
};
}

list_senders!(list_opts.ledgers(), "Ledger");
list_senders!(list_opts.trezors(), "Trezor");
list_senders!(list_opts.aws_signers(), "AWS");

Ok(())
}

fn list_local_senders(&self) -> Result<()> {
let keystore_path = self.dir.clone().unwrap_or_default();
let keystore_dir = if keystore_path.is_empty() {
// Create the keystore default directory if it doesn't exist
let default_dir = Config::foundry_keystores_dir().unwrap();
fs::create_dir_all(&default_dir)?;
default_dir
} else {
dunce::canonicalize(keystore_path)?
};

// list files within keystore dir
std::fs::read_dir(keystore_dir)?.flatten().for_each(|entry| {
let path = entry.path();
if path.is_file() && path.extension().is_none() {
if let Some(file_name) = path.file_name() {
if let Some(name) = file_name.to_str() {
println!("{} (Local)", name);
}
}
}
});

Ok(())
}
}
42 changes: 6 additions & 36 deletions crates/cast/bin/cmd/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ use yansi::Paint;
pub mod vanity;
use vanity::VanityArgs;

pub mod list;
use list::ListArgs;

/// CLI arguments for `cast wallet`.
#[derive(Debug, Parser)]
pub enum WalletSubcommands {
Expand Down Expand Up @@ -137,7 +140,7 @@ pub enum WalletSubcommands {
},
/// List all the accounts in the keystore default directory
#[clap(visible_alias = "ls")]
List,
List(ListArgs),

/// Derives private key from mnemonic
#[clap(name = "derive-private-key", visible_aliases = &["--derive-private-key"])]
Expand Down Expand Up @@ -331,41 +334,8 @@ 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
}
}
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) => return Err(e),
}
WalletSubcommands::List(cmd) => {
cmd.run().await?;
}
WalletSubcommands::DerivePrivateKey { mnemonic, mnemonic_index } => {
let phrase = Mnemonic::<English>::new_from_phrase(mnemonic.as_str())?.to_phrase();
Expand Down
23 changes: 22 additions & 1 deletion crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use foundry_common::rpc::{next_http_rpc_endpoint, next_ws_rpc_endpoint};
use foundry_test_utils::{casttest, util::OutputExt};
use std::{io::Write, path::Path};
use std::{fs, io::Write, path::Path};

// tests `--help` is printed to std out
casttest!(print_help, |_prj, cmd| {
Expand Down Expand Up @@ -131,6 +131,27 @@ 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_accounts, |prj, cmd| {
let keystore_path = prj.root().join("keystore");
fs::create_dir_all(keystore_path).unwrap();
cmd.set_current_dir(prj.root());

// empty results
cmd.cast_fuse().args(["wallet", "list", "--dir", "keystore"]);
let list_output = cmd.stdout_lossy();
assert!(list_output.is_empty());

// create 10 wallets
cmd.cast_fuse().args(["wallet", "new", "keystore", "-n", "10", "--unsafe-password", "test"]);
cmd.stdout_lossy();

// test list new wallet
cmd.cast_fuse().args(["wallet", "list", "--dir", "keystore"]);
let list_output = cmd.stdout_lossy();
assert_eq!(list_output.matches('\n').count(), 10);
});

// tests that `cast estimate` is working correctly.
casttest!(estimate_function_gas, |_prj, cmd| {
let eth_rpc_url = next_http_rpc_endpoint();
Expand Down
1 change: 1 addition & 0 deletions crates/wallets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ foundry-common.workspace = true

async-trait = "0.1"
clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] }
derive_builder = "0.20.0"
eyre.workspace = true
hex = { workspace = true, features = ["serde"] }
itertools.workspace = true
Expand Down
Loading

0 comments on commit 57815e0

Please sign in to comment.