Skip to main content

cast/cmd/wallet/
vanity.rs

1use alloy_primitives::{Address, hex};
2use alloy_signer_local::PrivateKeySigner;
3use clap::Parser;
4use eyre::{Result, WrapErr};
5use foundry_cli::json::print_json_success;
6use foundry_common::{fs::write_sensitive_json_file, sh_println, shell};
7use rayon::iter::{self, ParallelIterator};
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::{
12    fs,
13    path::{Path, PathBuf},
14    time::Instant,
15};
16
17/// CLI arguments for `cast wallet vanity`.
18#[derive(Clone, Debug, Parser)]
19pub struct VanityArgs {
20    /// Prefix regex pattern or hex string.
21    #[arg(long, value_name = "PATTERN", required_unless_present = "ends_with")]
22    pub starts_with: Option<String>,
23
24    /// Suffix regex pattern or hex string.
25    #[arg(long, value_name = "PATTERN")]
26    pub ends_with: Option<String>,
27
28    // 2^64-1 is max possible nonce per [eip-2681](https://eips.ethereum.org/EIPS/eip-2681).
29    /// Generate a vanity contract address created by the generated keypair with the specified
30    /// nonce.
31    #[arg(long)]
32    pub nonce: Option<u64>,
33
34    /// Path to save the generated vanity contract address to.
35    ///
36    /// If provided, the generated vanity addresses will appended to a JSON array in the specified
37    /// file.
38    #[arg(
39        long,
40        value_hint = clap::ValueHint::FilePath,
41        value_name = "PATH",
42    )]
43    pub save_path: Option<PathBuf>,
44}
45
46/// WalletData contains address and private_key information for a wallet.
47#[derive(Serialize, Deserialize)]
48struct WalletData {
49    address: String,
50    private_key: String,
51}
52
53/// Wallets is a collection of WalletData.
54#[derive(Default, Serialize, Deserialize)]
55struct Wallets {
56    wallets: Vec<WalletData>,
57}
58
59impl WalletData {
60    fn new(wallet: &PrivateKeySigner) -> Self {
61        Self {
62            address: wallet.address().to_checksum(None),
63            private_key: hex::encode_prefixed(wallet.credential().to_bytes()),
64        }
65    }
66}
67
68impl VanityArgs {
69    pub fn run(self) -> Result<PrivateKeySigner> {
70        let Self { starts_with, ends_with, nonce, save_path } = self;
71
72        let matcher = Matcher {
73            left: starts_with.as_deref().map(|p| parse_pattern(p, true)).transpose()?,
74            right: ends_with.as_deref().map(|p| parse_pattern(p, false)).transpose()?,
75        };
76
77        sh_status!("Starting to generate vanity address...")?;
78        let timer = Instant::now();
79        let wallet = find_vanity(&matcher, nonce);
80
81        if let Some(save_path) = save_path {
82            save_wallet_to_file(&wallet, &save_path)?;
83        }
84
85        let contract_address = nonce.map(|nonce| wallet.address().create(nonce).to_checksum(None));
86        let WalletData { address, private_key } = WalletData::new(&wallet);
87
88        if shell::is_json() {
89            print_json_success(json!({
90                "address": address,
91                "private_key": private_key,
92                "contract_address": contract_address,
93            }))?;
94        } else {
95            sh_println!(
96                "Successfully found vanity address in {:.3} seconds.{}{}\nAddress: {}\nPrivate Key: {}",
97                timer.elapsed().as_secs_f64(),
98                if contract_address.is_some() { "\nContract address: " } else { "" },
99                contract_address.unwrap_or_default(),
100                address,
101                private_key,
102            )?;
103        }
104
105        Ok(wallet)
106    }
107}
108
109/// Saves the specified `wallet` to a 'vanity_addresses.json' file at the given `save_path`.
110/// If the file exists, the wallet data is appended to the existing content;
111/// otherwise, a new file is created.
112fn save_wallet_to_file(wallet: &PrivateKeySigner, path: &Path) -> Result<()> {
113    let mut wallets = if path.exists() {
114        let data = fs::read_to_string(path)?;
115        if data.trim().is_empty() {
116            Wallets::default()
117        } else {
118            serde_json::from_str::<Wallets>(&data)
119                .wrap_err_with(|| format!("failed to parse wallet file {}", path.display()))?
120        }
121    } else {
122        Wallets::default()
123    };
124
125    wallets.wallets.push(WalletData::new(wallet));
126
127    write_sensitive_json_file(path, &wallets)?;
128    Ok(())
129}
130
131/// Generates random wallets in parallel until `matcher` matches the wallet address, or the
132/// contract address it creates at `nonce` when one is given.
133fn find_vanity(matcher: &Matcher, nonce: Option<u64>) -> PrivateKeySigner {
134    iter::repeat(())
135        .map(|()| PrivateKeySigner::random())
136        .find_any(|wallet| {
137            let address = wallet.address();
138            matcher.is_match(&nonce.map_or(address, |nonce| address.create(nonce)))
139        })
140        .expect("infinite iterator")
141}
142
143/// A vanity pattern: an exact byte prefix/suffix, or a regex over the lowercase hex address.
144#[derive(Debug)]
145enum Pattern {
146    Exact(Vec<u8>),
147    Re(Regex),
148}
149
150/// Optional start and end patterns an address must satisfy.
151struct Matcher {
152    left: Option<Pattern>,
153    right: Option<Pattern>,
154}
155
156impl Matcher {
157    fn is_match(&self, addr: &Address) -> bool {
158        let bytes = addr.as_slice();
159        let mut encoded = None;
160        let mut matches = |pattern: &Pattern, exact: fn(&[u8], &[u8]) -> bool| match pattern {
161            Pattern::Exact(hex) => exact(bytes, hex),
162            Pattern::Re(re) => re.is_match(encoded.get_or_insert_with(|| hex::encode(bytes))),
163        };
164        self.left.as_ref().is_none_or(|p| matches(p, <[u8]>::starts_with))
165            && self.right.as_ref().is_none_or(|p| matches(p, <[u8]>::ends_with))
166    }
167}
168
169fn parse_pattern(pattern: &str, is_start: bool) -> Result<Pattern> {
170    let pattern =
171        pattern.strip_prefix("0x").or_else(|| pattern.strip_prefix("0X")).unwrap_or(pattern);
172    if pattern.is_empty() {
173        return Err(eyre::eyre!("Vanity pattern cannot be empty"));
174    }
175
176    let is_hex = pattern.bytes().all(|byte| byte.is_ascii_hexdigit());
177    if is_hex && pattern.len() > 40 {
178        return Err(eyre::eyre!("Hex pattern must be less than 20 bytes"));
179    }
180
181    if let Ok(decoded) = hex::decode(pattern) {
182        return Ok(Pattern::Exact(decoded));
183    }
184    // a non regex literal containing non-hex characters can never match
185    if !is_hex && pattern.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
186        return Err(eyre::eyre!("Pattern contains non-hex characters and can never match"));
187    }
188    let (prefix, suffix) = if is_start { ("^", "") } else { ("", "$") };
189    let pattern = if is_hex { pattern.to_ascii_lowercase() } else { pattern.to_string() };
190    Ok(Pattern::Re(Regex::new(&format!("{prefix}{pattern}{suffix}"))?))
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[cfg(unix)]
198    use std::os::unix::fs::PermissionsExt;
199
200    fn single(pattern: &str, is_start: bool) -> Matcher {
201        let pattern = parse_pattern(pattern, is_start).unwrap();
202        if is_start {
203            Matcher { left: Some(pattern), right: None }
204        } else {
205            Matcher { left: None, right: Some(pattern) }
206        }
207    }
208
209    fn address(index: usize, byte: u8) -> Address {
210        let mut bytes = [0; 20];
211        bytes[index] = byte;
212        Address::from(bytes)
213    }
214
215    #[test]
216    fn save_path() {
217        let tmp = tempfile::NamedTempFile::new().unwrap();
218        let args: VanityArgs = VanityArgs::parse_from([
219            "foundry-cli",
220            "--starts-with",
221            "00",
222            "--save-path",
223            tmp.path().to_str().unwrap(),
224        ]);
225        let wallet = args.run().unwrap();
226        assert!(wallet.address().as_slice().starts_with(&[0]));
227        let s = fs::read_to_string(tmp.path()).unwrap();
228        let wallets: Wallets = serde_json::from_str(&s).unwrap();
229        assert!(!wallets.wallets.is_empty());
230    }
231
232    #[test]
233    fn malformed_wallet_file_is_not_overwritten() {
234        let tmp = tempfile::NamedTempFile::new().unwrap();
235        let original = "{\"wallets\":[";
236        fs::write(tmp.path(), original).unwrap();
237
238        let err = save_wallet_to_file(&PrivateKeySigner::random(), tmp.path()).unwrap_err();
239
240        assert!(err.to_string().contains("failed to parse wallet file"));
241        assert_eq!(fs::read_to_string(tmp.path()).unwrap(), original);
242    }
243
244    #[cfg(unix)]
245    #[test]
246    fn wallet_file_is_owner_only() {
247        let tmp = tempfile::tempdir().unwrap();
248        let path = tmp.path().join("wallets.json");
249        save_wallet_to_file(&PrivateKeySigner::random(), &path).unwrap();
250        assert_eq!(fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
251
252        fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
253        save_wallet_to_file(&PrivateKeySigner::random(), &path).unwrap();
254        assert_eq!(fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
255    }
256
257    #[test]
258    fn parse_patterns() {
259        // odd-length hex is matched case-insensitively as a regex
260        assert!(matches!(parse_pattern("A", true).unwrap(), Pattern::Re(_)));
261        assert!(single("A", true).is_match(&address(0, 0xa0)));
262        assert!(single("A", false).is_match(&address(19, 0x0a)));
263        assert!(single("0x9", true).is_match(&address(0, 0x90)));
264
265        // 0x/0X prefixes are stripped from exact hex patterns
266        for prefixed in ["0xdead", "0Xdead"] {
267            let Pattern::Exact(bytes) = parse_pattern(prefixed, true).unwrap() else {
268                panic!("expected an exact hex pattern");
269            };
270            assert_eq!(bytes, hex::decode("dead").unwrap());
271        }
272        let mut matching = [0; 20];
273        matching[..2].copy_from_slice(&[0xde, 0xad]);
274        assert!(single("0xdead", true).is_match(&Address::from(matching)));
275
276        // regex patterns stay supported
277        assert!(parse_pattern("a.c", true).is_ok());
278
279        // exact suffixes match the end of the address only
280        assert!(matches!(parse_pattern("00", false).unwrap(), Pattern::Exact(_)));
281        assert!(single("00", false).is_match(&address(0, 0xff)));
282        assert!(!single("00", false).is_match(&address(19, 0x01)));
283        assert!(!single("dead", true).is_match(&address(19, 0xde)));
284    }
285
286    #[test]
287    fn reject_invalid_patterns() {
288        for (pattern, err) in [
289            (&*"1".repeat(41), "Hex pattern must be less than 20 bytes"),
290            ("0x", "Vanity pattern cannot be empty"),
291            // non-hex chars can never appear in a hex address
292            ("zzz", "Pattern contains non-hex characters and can never match"),
293            ("foobar", "Pattern contains non-hex characters and can never match"),
294        ] {
295            assert_eq!(parse_pattern(pattern, true).unwrap_err().to_string(), err);
296        }
297    }
298}