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