Skip to main content

cast/cmd/wallet/
list.rs

1use clap::Parser;
2use eyre::Result;
3use foundry_cli::json::print_json_success;
4use foundry_common::{fs, sh_err, sh_println, shell};
5use foundry_config::Config;
6use foundry_wallets::wallet_multi::MultiWalletOptsBuilder;
7use serde::Serialize;
8use std::{borrow::Cow, env};
9
10/// CLI arguments for `cast wallet list`.
11#[derive(Clone, Debug, Parser)]
12pub struct ListArgs {
13    /// List all the accounts in the keystore directory.
14    /// Default keystore directory is used if no path provided.
15    #[arg(long, default_missing_value = "", num_args(0..=1))]
16    dir: Option<String>,
17
18    /// List accounts from a Ledger hardware wallet.
19    #[arg(long, short, group = "hw-wallets")]
20    ledger: bool,
21
22    /// List accounts from a Trezor hardware wallet.
23    #[arg(long, short, group = "hw-wallets")]
24    trezor: bool,
25
26    /// List accounts from AWS KMS.
27    ///
28    /// Ensure either one of AWS_KMS_KEY_IDS (comma-separated) or AWS_KMS_KEY_ID environment
29    /// variables are set.
30    #[arg(long, hide = !cfg!(feature = "aws-kms"))]
31    aws: bool,
32
33    /// List accounts from Google Cloud KMS.
34    ///
35    /// Ensure the following environment variables are set: GCP_PROJECT_ID, GCP_LOCATION,
36    /// GCP_KEY_RING, GCP_KEY_NAME, GCP_KEY_VERSION.
37    ///
38    /// See: <https://cloud.google.com/kms/docs>
39    #[arg(long, hide = !cfg!(feature = "gcp-kms"))]
40    gcp: bool,
41
42    /// List accounts from Turnkey.
43    #[arg(long, hide = !cfg!(feature = "turnkey"))]
44    turnkey: bool,
45
46    /// List all configured accounts.
47    #[arg(long, group = "hw-wallets")]
48    all: bool,
49
50    /// Max number of addresses to display from hardware wallets.
51    #[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        // list local accounts as files in keystore dir, no need to unlock / provide password
61        if self.dir.is_some()
62            || self.all
63            || (!self.ledger && !self.trezor && !self.aws && !self.gcp)
64        {
65            match self.list_local_senders() {
66                Ok(local) => accounts.extend(local),
67                Err(e) if !self.all => {
68                    sh_err!("{}", e)?;
69                }
70                _ => {}
71            }
72        }
73
74        // Create options for multi wallet - ledger, trezor and AWS
75        let list_opts = MultiWalletOptsBuilder::default()
76            .ledger(self.ledger || self.all)
77            .mnemonic_indexes(Some(vec![0]))
78            .trezor(self.trezor || self.all)
79            .aws(self.aws || self.all)
80            .gcp(self.gcp || (self.all && gcp_env_vars_set()))
81            .turnkey(self.turnkey || self.all)
82            .interactives(0)
83            .interactive(false)
84            .browser(Default::default())
85            .build()
86            .expect("build multi wallet");
87
88        // macro to collect or print senders for a list of signers
89        macro_rules! list_senders {
90            ($signers:expr, $label:literal) => {
91                match $signers.await {
92                    Ok(signers) => {
93                        for signer in signers.unwrap_or_default().iter() {
94                            for sender in signer.available_senders(self.max_senders).await? {
95                                if format_json {
96                                    accounts.push(WalletAccount {
97                                        address: sender.to_string(),
98                                        source: $label,
99                                    });
100                                } else {
101                                    let _ = sh_println!("{} ({})", sender, $label);
102                                }
103                            }
104                        }
105                    }
106                    Err(e) if !self.all => {
107                        sh_err!("{}", e)?;
108                    }
109                    _ => {}
110                }
111            };
112        }
113
114        list_senders!(list_opts.ledgers(), "Ledger");
115        list_senders!(list_opts.trezors(), "Trezor");
116        list_senders!(list_opts.aws_signers(), "AWS");
117        list_senders!(list_opts.gcp_signers(), "GCP");
118
119        if format_json {
120            print_json_success(accounts)?;
121        }
122
123        Ok(())
124    }
125
126    fn list_local_senders(&self) -> Result<Vec<WalletAccount>> {
127        let keystore_path = self.dir.as_deref().unwrap_or_default();
128        let keystore_dir = if keystore_path.is_empty() {
129            // Create the keystore default directory if it doesn't exist
130            let default_dir = Config::foundry_keystores_dir().unwrap();
131            fs::create_dir_all(&default_dir)?;
132            default_dir
133        } else {
134            dunce::canonicalize(keystore_path)?
135        };
136
137        let mut accounts = Vec::new();
138
139        // List all files within the keystore directory.
140        let mut paths = std::fs::read_dir(keystore_dir)?
141            .map(|entry| entry.map(|entry| entry.path()))
142            .collect::<Result<Vec<_>, _>>()?;
143        paths.sort();
144        for path in paths {
145            if path.is_file()
146                && let Some(file_name) = path.file_name()
147                && let Some(name) = file_name.to_str()
148                // Skip recognized Touch ID sidecars while retaining ambiguous files.
149                && !matches!(super::is_touch_id_sidecar(&path), Ok(true))
150            {
151                let account = local_account_name(name);
152                if shell::is_json() {
153                    accounts.push(WalletAccount { address: account.into_owned(), source: "Local" });
154                } else {
155                    sh_println!("{} (Local)", account)?;
156                }
157            }
158        }
159
160        Ok(accounts)
161    }
162}
163
164/// Extracts the address from a Geth-style keystore filename, preserving custom names.
165fn local_account_name(name: &str) -> Cow<'_, str> {
166    if let Some((timestamp, address)) =
167        name.strip_prefix("UTC--").and_then(|suffix| suffix.rsplit_once("--"))
168        && !timestamp.is_empty()
169        && address.len() == 40
170        && address.bytes().all(|byte| byte.is_ascii_hexdigit())
171    {
172        Cow::Owned(format!("0x{address}"))
173    } else {
174        Cow::Borrowed(name)
175    }
176}
177
178#[derive(Serialize)]
179struct WalletAccount {
180    address: String,
181    source: &'static str,
182}
183
184fn gcp_env_vars_set() -> bool {
185    let required_vars =
186        ["GCP_PROJECT_ID", "GCP_LOCATION", "GCP_KEY_RING", "GCP_KEY_NAME", "GCP_KEY_VERSION"];
187
188    required_vars.iter().all(|&var| env::var(var).is_ok())
189}