Skip to content
19 changes: 18 additions & 1 deletion crates/cast/src/cmd/wallet/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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)
Expand Down
74 changes: 74 additions & 0 deletions crates/cast/src/cmd/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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<String>,

/// 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
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
Some(Self::foundry_dir()?.join("wallets.json"))
}

/// Returns the path to foundry's etherscan cache dir for `chain_id`:
/// `~/.foundry/cache/etherscan/<chain>`
pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
Expand Down
1 change: 1 addition & 0 deletions crates/wallets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions crates/wallets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions crates/wallets/src/multi_wallet.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
registry::{WalletKind, WalletRegistry},
utils,
wallet_signer::{PendingSigner, WalletSigner},
};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -378,6 +382,40 @@ impl MultiWalletOpts {
Ok(None)
}

/// Returns signers created from registry aliases present in `--accounts`.
pub async fn registry_signers(&self) -> Result<Option<Vec<WalletSigner>>> {
if let Some(names) = &self.keystore_account_names {
let reg = WalletRegistry::load().unwrap_or_default();
let mut signers: Vec<WalletSigner> = 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<Option<Vec<WalletSigner>>> {
if self.trezor {
let mut args = self.clone();
Expand Down
76 changes: 76 additions & 0 deletions crates/wallets/src/registry.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
#[serde(default)]
pub mnemonic_index: Option<u32>,
#[serde(default)]
pub cached_public_key: Option<String>,
#[serde(default)]
pub cached_address: Option<Address>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct WalletRegistry {
#[serde(default)]
pub wallets: BTreeMap<String, WalletRegistryEntry>,
}

impl WalletRegistry {
fn file_path() -> Result<PathBuf> {
Config::foundry_wallets_file().ok_or_else(|| eyre::eyre!("Could not find foundry dir"))
}

pub fn load() -> Result<Self> {
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<Item = (&String, &WalletRegistryEntry)> {
self.wallets.iter()
}
}
Loading
Loading