diff --git a/crates/cast/src/cmd/wallet/list.rs b/crates/cast/src/cmd/wallet/list.rs index d8c297adf53cc..6f877c570bfc6 100644 --- a/crates/cast/src/cmd/wallet/list.rs +++ b/crates/cast/src/cmd/wallet/list.rs @@ -4,7 +4,10 @@ use std::env; use foundry_common::{fs, sh_err, sh_println}; use foundry_config::Config; -use foundry_wallets::multi_wallet::MultiWalletOptsBuilder; +use foundry_wallets::{ + multi_wallet::MultiWalletOptsBuilder, + registry::{WalletKind, WalletRegistry}, +}; /// CLI arguments for `cast wallet list`. #[derive(Clone, Debug, Parser)] @@ -57,6 +60,20 @@ impl ListArgs { let _ = self.list_local_senders(); } + // list registered aliases + let registry = WalletRegistry::load().unwrap_or_default(); + for (name, entry) in registry.list() { + let label = match entry.kind { + WalletKind::Ledger => "Ledger", + WalletKind::Trezor => "Trezor", + }; + if let Some(addr) = entry.cached_address { + let _ = sh_println!("{name} ({label}, {addr})"); + } else { + let _ = sh_println!("{name} ({label})"); + } + } + // Create options for multi wallet - ledger, trezor and AWS let list_opts = MultiWalletOptsBuilder::default() .ledger(self.ledger || self.all) diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index 7c9a99d08b0ce..8036e283295b8 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -23,6 +23,7 @@ pub mod vanity; use vanity::VanityArgs; pub mod list; +use foundry_wallets::registry::{WalletKind, WalletRegistry, WalletRegistryEntry}; use list::ListArgs; /// CLI arguments for `cast wallet`. @@ -200,6 +201,34 @@ pub enum WalletSubcommands { #[command(visible_alias = "ls")] List(ListArgs), + /// Register a hardware wallet alias for use with --account + #[command(name = "register", visible_alias = "reg")] + Register { + /// Alias name to register + #[arg(long, required = true)] + name: String, + + /// Register a Ledger hardware wallet + #[arg(long, conflicts_with = "trezor")] + ledger: bool, + + /// Register a Trezor hardware wallet + #[arg(long, conflicts_with = "ledger")] + trezor: bool, + + /// Derivation path to use (optional) + #[arg(long, alias = "hd-path")] + derivation_path: Option, + + /// Mnemonic index to use when no derivation path is provided + #[arg(long, default_value = "0")] + mnemonic_index: u32, + + /// Attempt to connect and cache public key/address + #[arg(long)] + cache: bool, + }, + /// Remove a wallet from the keystore. /// /// This command requires the wallet alias and will prompt for a password to ensure that only @@ -670,6 +699,51 @@ flag to set your key via: Self::List(cmd) => { cmd.run().await?; } + Self::Register { name, ledger, trezor, derivation_path, mnemonic_index, cache } => { + let kind = match (ledger, trezor) { + (true, false) => WalletKind::Ledger, + (false, true) => WalletKind::Trezor, + _ => eyre::bail!("Please specify exactly one of --ledger or --trezor"), + }; + + let mut reg = WalletRegistry::load().unwrap_or_default(); + + let mut entry = WalletRegistryEntry { + name: name.clone(), + kind, + hd_path: derivation_path, + mnemonic_index: Some(mnemonic_index), + cached_public_key: None, + cached_address: None, + }; + + if cache { + // Try to connect and fetch public data + match entry.kind { + WalletKind::Ledger => { + let signer = foundry_wallets::utils::create_ledger_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await?; + entry.cached_address = Some(signer.address()); + } + WalletKind::Trezor => { + let signer = foundry_wallets::utils::create_trezor_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await?; + entry.cached_address = Some(signer.address()); + } + } + } + + reg.set(entry); + reg.save()?; + + sh_println!("Registered alias `{}`", name)?; + } Self::Remove { name, dir, unsafe_password } => { let dir = if let Some(path) = dir { Path::new(&path).to_path_buf() diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 83d56995918eb..e5700e5fc95a4 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1995,6 +1995,11 @@ impl Config { Some(Self::foundry_dir()?.join("keystores")) } + /// Returns the path to foundry's wallets registry file: `~/.foundry/wallets.json`. + pub fn foundry_wallets_file() -> Option { + Some(Self::foundry_dir()?.join("wallets.json")) + } + /// Returns the path to foundry's etherscan cache dir for `chain_id`: /// `~/.foundry/cache/etherscan/` pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into) -> Option { diff --git a/crates/wallets/Cargo.toml b/crates/wallets/Cargo.toml index 8b83021df32ea..02d989df223bd 100644 --- a/crates/wallets/Cargo.toml +++ b/crates/wallets/Cargo.toml @@ -38,6 +38,7 @@ derive_builder = "0.20" eyre.workspace = true rpassword = "7" serde.workspace = true +serde_json.workspace = true thiserror.workspace = true tracing.workspace = true eth-keystore = "0.5.0" diff --git a/crates/wallets/src/lib.rs b/crates/wallets/src/lib.rs index 622e8a3b8d5a2..79413c371f2be 100644 --- a/crates/wallets/src/lib.rs +++ b/crates/wallets/src/lib.rs @@ -11,6 +11,7 @@ extern crate tracing; pub mod error; pub mod multi_wallet; pub mod raw_wallet; +pub mod registry; pub mod utils; pub mod wallet; pub mod wallet_signer; diff --git a/crates/wallets/src/multi_wallet.rs b/crates/wallets/src/multi_wallet.rs index 1ff62b80992f5..e70fc972ce41e 100644 --- a/crates/wallets/src/multi_wallet.rs +++ b/crates/wallets/src/multi_wallet.rs @@ -1,4 +1,5 @@ use crate::{ + registry::{WalletKind, WalletRegistry}, utils, wallet_signer::{PendingSigner, WalletSigner}, }; @@ -241,6 +242,9 @@ impl MultiWalletOpts { if let Some(gcp_signer) = self.gcp_signers().await? { signers.extend(gcp_signer); } + if let Some(reg_signers) = self.registry_signers().await? { + signers.extend(reg_signers); + } if let Some((pending_keystores, unlocked)) = self.keystores()? { pending.extend(pending_keystores); signers.extend(unlocked); @@ -378,6 +382,40 @@ impl MultiWalletOpts { Ok(None) } + /// Returns signers created from registry aliases present in `--accounts`. + pub async fn registry_signers(&self) -> Result>> { + if let Some(names) = &self.keystore_account_names { + let reg = WalletRegistry::load().unwrap_or_default(); + let mut signers: Vec = Vec::new(); + for name in names { + if let Some(entry) = reg.get(name) { + match entry.kind { + WalletKind::Ledger => { + let signer = utils::create_ledger_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await?; + signers.push(signer); + } + WalletKind::Trezor => { + let signer = utils::create_trezor_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await?; + signers.push(signer); + } + } + } + } + if !signers.is_empty() { + return Ok(Some(signers)); + } + } + Ok(None) + } + pub async fn trezors(&self) -> Result>> { if self.trezor { let mut args = self.clone(); diff --git a/crates/wallets/src/registry.rs b/crates/wallets/src/registry.rs new file mode 100644 index 0000000000000..58cd7eb201ed3 --- /dev/null +++ b/crates/wallets/src/registry.rs @@ -0,0 +1,76 @@ +use alloy_primitives::Address; +use eyre::{Context, Result}; +use foundry_config::Config; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fs, path::PathBuf}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum WalletKind { + Ledger, + Trezor, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WalletRegistryEntry { + pub name: String, + pub kind: WalletKind, + #[serde(default)] + pub hd_path: Option, + #[serde(default)] + pub mnemonic_index: Option, + #[serde(default)] + pub cached_public_key: Option, + #[serde(default)] + pub cached_address: Option
, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct WalletRegistry { + #[serde(default)] + pub wallets: BTreeMap, +} + +impl WalletRegistry { + fn file_path() -> Result { + Config::foundry_wallets_file().ok_or_else(|| eyre::eyre!("Could not find foundry dir")) + } + + pub fn load() -> Result { + let path = Self::file_path()?; + if !path.exists() { + return Ok(Default::default()); + } + let data = fs::read_to_string(&path) + .wrap_err_with(|| format!("Failed to read wallets registry at {}", path.display()))?; + let reg: Self = serde_json::from_str(&data) + .wrap_err_with(|| format!("Failed to parse wallets registry at {}", path.display()))?; + Ok(reg) + } + + pub fn save(&self) -> Result<()> { + let path = Self::file_path()?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let json = serde_json::to_string_pretty(self)?; + fs::write(&path, json) + .wrap_err_with(|| format!("Failed to write wallets registry at {}", path.display())) + } + + pub fn get(&self, name: &str) -> Option<&WalletRegistryEntry> { + self.wallets.get(name) + } + + pub fn set(&mut self, entry: WalletRegistryEntry) { + self.wallets.insert(entry.name.clone(), entry); + } + + pub fn remove(&mut self, name: &str) { + self.wallets.remove(name); + } + + pub fn list(&self) -> impl Iterator { + self.wallets.iter() + } +} diff --git a/crates/wallets/src/wallet.rs b/crates/wallets/src/wallet.rs index 3feb8f99202c2..9a623a76a1e5f 100644 --- a/crates/wallets/src/wallet.rs +++ b/crates/wallets/src/wallet.rs @@ -1,4 +1,9 @@ -use crate::{raw_wallet::RawWalletOpts, utils, wallet_signer::WalletSigner}; +use crate::{ + raw_wallet::RawWalletOpts, + registry::{WalletKind, WalletRegistry}, + utils, + wallet_signer::WalletSigner, +}; use alloy_primitives::Address; use clap::Parser; use eyre::Result; @@ -126,18 +131,98 @@ impl WalletOpts { self.keystore_path.as_deref(), self.keystore_account_name.as_deref(), )? { - let (maybe_signer, maybe_pending) = utils::create_keystore_signer( - &path, - self.keystore_password.as_deref(), - self.keystore_password_file.as_deref(), - )?; - if let Some(pending) = maybe_pending { - pending.unlock()? - } else if let Some(signer) = maybe_signer { - signer + if path.exists() { + let (maybe_signer, maybe_pending) = utils::create_keystore_signer( + &path, + self.keystore_password.as_deref(), + self.keystore_password_file.as_deref(), + )?; + if let Some(pending) = maybe_pending { + pending.unlock()? + } else if let Some(signer) = maybe_signer { + signer + } else { + unreachable!() + } + } else if let Some(account_name) = self.keystore_account_name.as_deref() { + // Fallback: try wallet registry alias for hardware wallets + let reg = WalletRegistry::load().unwrap_or_default(); + if let Some(entry) = reg.get(account_name) { + match entry.kind { + WalletKind::Ledger => { + utils::create_ledger_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await? + } + WalletKind::Trezor => { + utils::create_trezor_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await? + } + } + } else { + eyre::bail!( + "\ +Error accessing local wallet. Did you pass a keystore, hardware wallet, private key or mnemonic? + +Run the command with --help flag for more information or use the corresponding CLI +flag to set your key via: + +--keystore +--interactive +--private-key +--mnemonic-path +--aws +--gcp +--trezor +--ledger" + ) + } } else { unreachable!() } + } else if let Some(account_name) = self.keystore_account_name.as_deref() { + // If no keystore file resolved, try wallet registry alias for hardware wallets + let reg = WalletRegistry::load().unwrap_or_default(); + if let Some(entry) = reg.get(account_name) { + match entry.kind { + WalletKind::Ledger => { + return utils::create_ledger_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await; + } + WalletKind::Trezor => { + return utils::create_trezor_signer( + entry.hd_path.as_deref(), + entry.mnemonic_index.unwrap_or(0), + ) + .await; + } + } + } else { + eyre::bail!( + "\ +Error accessing local wallet. Did you pass a keystore, hardware wallet, private key or mnemonic? + +Run the command with --help flag for more information or use the corresponding CLI +flag to set your key via: + +--keystore +--interactive +--private-key +--mnemonic-path +--aws +--gcp +--trezor +--ledger" + ) + } } else { eyre::bail!( "\