1use clap::Parser;
2use eyre::Result;
3use foundry_cli::json::print_json_success;
4use foundry_common::{fs, fs::canonicalize_path, sh_println, shell};
5use foundry_config::Config;
6use foundry_wallets::wallet_multi::MultiWalletOptsBuilder;
7use serde::Serialize;
8use std::{borrow::Cow, env};
9
10#[derive(Clone, Debug, Parser)]
12pub struct ListArgs {
13 #[arg(long, default_missing_value = "", num_args(0..=1))]
16 dir: Option<String>,
17
18 #[arg(long, short, group = "hw-wallets")]
20 ledger: bool,
21
22 #[arg(long, short, group = "hw-wallets")]
24 trezor: bool,
25
26 #[arg(long, hide = !cfg!(feature = "aws-kms"))]
31 aws: bool,
32
33 #[arg(long, hide = !cfg!(feature = "gcp-kms"))]
40 gcp: bool,
41
42 #[arg(long, hide = !cfg!(feature = "turnkey"))]
44 turnkey: bool,
45
46 #[arg(long, group = "hw-wallets")]
48 all: bool,
49
50 #[arg(long, short, default_value = "3", requires = "hw-wallets")]
52 max_senders: usize,
53}
54
55impl ListArgs {
56 pub async fn run(self) -> Result<()> {
57 let format_json = shell::is_json();
58 let mut accounts: Vec<WalletAccount> = Vec::new();
59
60 if self.dir.is_some()
62 || self.all
63 || (!self.ledger && !self.trezor && !self.aws && !self.gcp && !self.turnkey)
64 {
65 match self.list_local_senders() {
66 Ok(local) => accounts.extend(local),
67 Err(e) if !self.all => return Err(e),
68 _ => {}
69 }
70 }
71
72 let list_opts = MultiWalletOptsBuilder::default()
74 .ledger(self.ledger || self.all)
75 .mnemonic_indexes(Some(vec![0]))
76 .trezor(self.trezor || self.all)
77 .aws(self.aws || self.all)
78 .gcp(self.gcp || (self.all && gcp_env_vars_set()))
79 .turnkey(self.turnkey || self.all)
80 .interactives(0)
81 .interactive(false)
82 .browser(Default::default())
83 .build()
84 .expect("build multi wallet");
85
86 macro_rules! list_senders {
88 ($signers:expr, $label:literal) => {
89 match $signers.await {
90 Ok(signers) => {
91 for signer in signers.unwrap_or_default().iter() {
92 for sender in signer.available_senders(self.max_senders).await? {
93 if format_json {
94 accounts.push(WalletAccount {
95 address: sender.to_string(),
96 source: $label,
97 });
98 } else {
99 let _ = sh_println!("{} ({})", sender, $label);
100 }
101 }
102 }
103 }
104 Err(e) if !self.all => return Err(e),
105 _ => {}
106 }
107 };
108 }
109
110 list_senders!(list_opts.ledgers(), "Ledger");
111 list_senders!(list_opts.trezors(), "Trezor");
112 list_senders!(list_opts.aws_signers(), "AWS");
113 list_senders!(list_opts.gcp_signers(), "GCP");
114 list_senders!(async { list_opts.turnkey_signers() }, "Turnkey");
115
116 if format_json {
117 print_json_success(accounts)?;
118 }
119
120 Ok(())
121 }
122
123 fn list_local_senders(&self) -> Result<Vec<WalletAccount>> {
124 let keystore_path = self.dir.as_deref().unwrap_or_default();
125 let keystore_dir = if keystore_path.is_empty() {
126 let default_dir = Config::foundry_keystores_dir().unwrap();
128 fs::create_dir_all(&default_dir)?;
129 default_dir
130 } else {
131 canonicalize_path(keystore_path)?
132 };
133
134 let mut accounts = Vec::new();
135
136 let mut paths = std::fs::read_dir(keystore_dir)?
138 .map(|entry| entry.map(|entry| entry.path()))
139 .collect::<Result<Vec<_>, _>>()?;
140 paths.sort();
141 for path in paths {
142 if path.is_file()
143 && let Some(file_name) = path.file_name()
144 && let Some(name) = file_name.to_str()
145 && !matches!(super::is_touch_id_sidecar(&path), Ok(true))
147 {
148 let account = local_account_name(name);
149 if shell::is_json() {
150 accounts.push(WalletAccount { address: account.into_owned(), source: "Local" });
151 } else {
152 sh_println!("{} (Local)", account)?;
153 }
154 }
155 }
156
157 Ok(accounts)
158 }
159}
160
161fn local_account_name(name: &str) -> Cow<'_, str> {
163 if let Some((timestamp, address)) =
164 name.strip_prefix("UTC--").and_then(|suffix| suffix.rsplit_once("--"))
165 && !timestamp.is_empty()
166 && address.len() == 40
167 && address.bytes().all(|byte| byte.is_ascii_hexdigit())
168 {
169 Cow::Owned(format!("0x{address}"))
170 } else {
171 Cow::Borrowed(name)
172 }
173}
174
175#[derive(Serialize)]
176struct WalletAccount {
177 address: String,
178 source: &'static str,
179}
180
181fn gcp_env_vars_set() -> bool {
182 let required_vars =
183 ["GCP_PROJECT_ID", "GCP_LOCATION", "GCP_KEY_RING", "GCP_KEY_NAME", "GCP_KEY_VERSION"];
184
185 required_vars.iter().all(|&var| env::var(var).is_ok())
186}