Skip to main content

cast/cmd/wallet/
vanity.rs

1use alloy_primitives::{Address, hex};
2use alloy_signer::{k256::ecdsa::SigningKey, utils::secret_key_to_address};
3use alloy_signer_local::PrivateKeySigner;
4use clap::Parser;
5use eyre::{Result, WrapErr};
6use foundry_cli::json::print_json_success;
7use foundry_common::{sh_println, shell};
8use itertools::Either;
9use rayon::iter::{self, ParallelIterator};
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use serde_json::json;
13#[cfg(unix)]
14use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
15use std::{
16    fs,
17    io::Write,
18    path::{Path, PathBuf},
19    time::Instant,
20};
21
22/// Type alias for the result of [generate_wallet].
23pub type GeneratedWallet = (SigningKey, Address);
24
25/// CLI arguments for `cast wallet vanity`.
26#[derive(Clone, Debug, Parser)]
27pub struct VanityArgs {
28    /// Prefix regex pattern or hex string.
29    #[arg(long, value_name = "PATTERN", required_unless_present = "ends_with")]
30    pub starts_with: Option<String>,
31
32    /// Suffix regex pattern or hex string.
33    #[arg(long, value_name = "PATTERN")]
34    pub ends_with: Option<String>,
35
36    // 2^64-1 is max possible nonce per [eip-2681](https://eips.ethereum.org/EIPS/eip-2681).
37    /// Generate a vanity contract address created by the generated keypair with the specified
38    /// nonce.
39    #[arg(long)]
40    pub nonce: Option<u64>,
41
42    /// Path to save the generated vanity contract address to.
43    ///
44    /// If provided, the generated vanity addresses will appended to a JSON array in the specified
45    /// file.
46    #[arg(
47        long,
48        value_hint = clap::ValueHint::FilePath,
49        value_name = "PATH",
50    )]
51    pub save_path: Option<PathBuf>,
52}
53
54/// WalletData contains address and private_key information for a wallet.
55#[derive(Serialize, Deserialize)]
56struct WalletData {
57    address: String,
58    private_key: String,
59}
60
61/// Wallets is a collection of WalletData.
62#[derive(Default, Serialize, Deserialize)]
63struct Wallets {
64    wallets: Vec<WalletData>,
65}
66
67impl WalletData {
68    pub fn new(wallet: &PrivateKeySigner) -> Self {
69        Self {
70            address: wallet.address().to_checksum(None),
71            private_key: format!("0x{}", hex::encode(wallet.credential().to_bytes())),
72        }
73    }
74}
75
76impl VanityArgs {
77    pub fn run(self) -> Result<PrivateKeySigner> {
78        let Self { starts_with, ends_with, nonce, save_path } = self;
79
80        let mut left_exact_hex = None;
81        let mut left_regex = None;
82        if let Some(prefix) = starts_with {
83            match parse_pattern(&prefix, true)? {
84                Either::Left(left) => left_exact_hex = Some(left),
85                Either::Right(re) => left_regex = Some(re),
86            }
87        }
88
89        let mut right_exact_hex = None;
90        let mut right_regex = None;
91        if let Some(suffix) = ends_with {
92            match parse_pattern(&suffix, false)? {
93                Either::Left(right) => right_exact_hex = Some(right),
94                Either::Right(re) => right_regex = Some(re),
95            }
96        }
97
98        macro_rules! find_vanity {
99            ($m:ident, $nonce:ident) => {
100                if let Some(nonce) = $nonce {
101                    find_vanity_address_with_nonce($m, nonce)
102                } else {
103                    find_vanity_address($m)
104                }
105            };
106        }
107
108        sh_status!("Starting to generate vanity address...")?;
109        let timer = Instant::now();
110
111        let wallet = match (left_exact_hex, left_regex, right_exact_hex, right_regex) {
112            (Some(left), _, Some(right), _) => {
113                let matcher = HexMatcher { left, right };
114                find_vanity!(matcher, nonce)
115            }
116            (Some(left), _, _, Some(right)) => {
117                let matcher = LeftExactRightRegexMatcher { left, right };
118                find_vanity!(matcher, nonce)
119            }
120            (_, Some(left), _, Some(right)) => {
121                let matcher = RegexMatcher { left, right };
122                find_vanity!(matcher, nonce)
123            }
124            (_, Some(left), Some(right), _) => {
125                let matcher = LeftRegexRightExactMatcher { left, right };
126                find_vanity!(matcher, nonce)
127            }
128            (Some(left), None, None, None) => {
129                let matcher = LeftHexMatcher { left };
130                find_vanity!(matcher, nonce)
131            }
132            (None, None, Some(right), None) => {
133                let matcher = RightHexMatcher { right };
134                find_vanity!(matcher, nonce)
135            }
136            (None, Some(re), None, None) => {
137                let matcher = SingleRegexMatcher { re };
138                find_vanity!(matcher, nonce)
139            }
140            (None, None, None, Some(re)) => {
141                let matcher = SingleRegexMatcher { re };
142                find_vanity!(matcher, nonce)
143            }
144            _ => unreachable!(),
145        }
146        .expect("failed to generate vanity wallet");
147
148        // If a save path is provided, save the generated vanity wallet to the specified path.
149        if let Some(save_path) = save_path {
150            save_wallet_to_file(&wallet, &save_path)?;
151        }
152
153        let contract_address = nonce.map(|nonce| wallet.address().create(nonce).to_checksum(None));
154        let address = wallet.address().to_checksum(None);
155        let private_key = format!("0x{}", hex::encode(wallet.credential().to_bytes()));
156
157        if shell::is_json() {
158            print_json_success(json!({
159                "address": address,
160                "private_key": private_key,
161                "contract_address": contract_address,
162            }))?;
163        } else {
164            sh_println!(
165                "Successfully found vanity address in {:.3} seconds.{}{}\nAddress: {}\nPrivate Key: {}",
166                timer.elapsed().as_secs_f64(),
167                if contract_address.is_some() { "\nContract address: " } else { "" },
168                contract_address.unwrap_or_default(),
169                address,
170                private_key,
171            )?;
172        }
173
174        Ok(wallet)
175    }
176}
177
178/// Saves the specified `wallet` to a 'vanity_addresses.json' file at the given `save_path`.
179/// If the file exists, the wallet data is appended to the existing content;
180/// otherwise, a new file is created.
181fn save_wallet_to_file(wallet: &PrivateKeySigner, path: &Path) -> Result<()> {
182    let mut wallets = if path.exists() {
183        let data = fs::read_to_string(path)?;
184        if data.trim().is_empty() {
185            Wallets::default()
186        } else {
187            serde_json::from_str::<Wallets>(&data)
188                .wrap_err_with(|| format!("failed to parse wallet file {}", path.display()))?
189        }
190    } else {
191        Wallets::default()
192    };
193
194    wallets.wallets.push(WalletData::new(wallet));
195
196    let contents = serde_json::to_string_pretty(&wallets)?;
197    let mut options = fs::File::options();
198    options.write(true).create(true);
199    #[cfg(unix)]
200    options.mode(0o600);
201
202    let mut file = options.open(path)?;
203    #[cfg(unix)]
204    file.set_permissions(fs::Permissions::from_mode(0o600))?;
205    file.set_len(0)?;
206    file.write_all(contents.as_bytes())?;
207    Ok(())
208}
209
210/// Generates random wallets until `matcher` matches the wallet address, returning the wallet.
211pub fn find_vanity_address<T: VanityMatcher>(matcher: T) -> Option<PrivateKeySigner> {
212    wallet_generator().find_any(create_matcher(matcher)).map(|(key, _)| key.into())
213}
214
215/// Generates random wallets until `matcher` matches the contract address created at `nonce`,
216/// returning the wallet.
217pub fn find_vanity_address_with_nonce<T: VanityMatcher>(
218    matcher: T,
219    nonce: u64,
220) -> Option<PrivateKeySigner> {
221    wallet_generator().find_any(create_nonce_matcher(matcher, nonce)).map(|(key, _)| key.into())
222}
223
224/// Creates a matcher function, which takes a reference to a [GeneratedWallet] and returns
225/// whether it found a match or not by using `matcher`.
226#[inline]
227pub fn create_matcher<T: VanityMatcher>(matcher: T) -> impl Fn(&GeneratedWallet) -> bool {
228    move |(_, addr)| matcher.is_match(addr)
229}
230
231/// Creates a contract address matcher function that uses the specified nonce.
232/// The returned function takes a reference to a [GeneratedWallet] and returns
233/// whether the contract address created with the nonce matches using `matcher`.
234#[inline]
235pub fn create_nonce_matcher<T: VanityMatcher>(
236    matcher: T,
237    nonce: u64,
238) -> impl Fn(&GeneratedWallet) -> bool {
239    move |(_, addr)| {
240        let contract_addr = addr.create(nonce);
241        matcher.is_match(&contract_addr)
242    }
243}
244
245/// Returns an infinite parallel iterator which yields a [GeneratedWallet].
246#[inline]
247pub fn wallet_generator() -> iter::Map<iter::Repeat<()>, impl Fn(()) -> GeneratedWallet> {
248    iter::repeat(()).map(|()| generate_wallet())
249}
250
251/// Generates a random K-256 signing key and derives its Ethereum address.
252pub fn generate_wallet() -> GeneratedWallet {
253    let key = SigningKey::random(&mut rand_08::thread_rng());
254    let address = secret_key_to_address(&key);
255    (key, address)
256}
257
258/// A trait to match vanity addresses.
259pub trait VanityMatcher: Send + Sync {
260    fn is_match(&self, addr: &Address) -> bool;
261}
262
263/// Matches start and end hex.
264pub struct HexMatcher {
265    pub left: Vec<u8>,
266    pub right: Vec<u8>,
267}
268
269impl VanityMatcher for HexMatcher {
270    #[inline]
271    fn is_match(&self, addr: &Address) -> bool {
272        let bytes = addr.as_slice();
273        bytes.starts_with(&self.left) && bytes.ends_with(&self.right)
274    }
275}
276
277/// Matches only start hex.
278pub struct LeftHexMatcher {
279    pub left: Vec<u8>,
280}
281
282impl VanityMatcher for LeftHexMatcher {
283    #[inline]
284    fn is_match(&self, addr: &Address) -> bool {
285        let bytes = addr.as_slice();
286        bytes.starts_with(&self.left)
287    }
288}
289
290/// Matches only end hex.
291pub struct RightHexMatcher {
292    pub right: Vec<u8>,
293}
294
295impl VanityMatcher for RightHexMatcher {
296    #[inline]
297    fn is_match(&self, addr: &Address) -> bool {
298        let bytes = addr.as_slice();
299        bytes.ends_with(&self.right)
300    }
301}
302
303/// Matches start hex and end regex.
304pub struct LeftExactRightRegexMatcher {
305    pub left: Vec<u8>,
306    pub right: Regex,
307}
308
309impl VanityMatcher for LeftExactRightRegexMatcher {
310    #[inline]
311    fn is_match(&self, addr: &Address) -> bool {
312        let bytes = addr.as_slice();
313        bytes.starts_with(&self.left) && self.right.is_match(&hex::encode(bytes))
314    }
315}
316
317/// Matches start regex and end hex.
318pub struct LeftRegexRightExactMatcher {
319    pub left: Regex,
320    pub right: Vec<u8>,
321}
322
323impl VanityMatcher for LeftRegexRightExactMatcher {
324    #[inline]
325    fn is_match(&self, addr: &Address) -> bool {
326        let bytes = addr.as_slice();
327        bytes.ends_with(&self.right) && self.left.is_match(&hex::encode(bytes))
328    }
329}
330
331/// Matches a single regex.
332pub struct SingleRegexMatcher {
333    pub re: Regex,
334}
335
336impl VanityMatcher for SingleRegexMatcher {
337    #[inline]
338    fn is_match(&self, addr: &Address) -> bool {
339        let addr = hex::encode(addr);
340        self.re.is_match(&addr)
341    }
342}
343
344/// Matches start and end regex.
345pub struct RegexMatcher {
346    pub left: Regex,
347    pub right: Regex,
348}
349
350impl VanityMatcher for RegexMatcher {
351    #[inline]
352    fn is_match(&self, addr: &Address) -> bool {
353        let addr = hex::encode(addr);
354        self.left.is_match(&addr) && self.right.is_match(&addr)
355    }
356}
357
358fn parse_pattern(pattern: &str, is_start: bool) -> Result<Either<Vec<u8>, Regex>> {
359    let pattern =
360        pattern.strip_prefix("0x").or_else(|| pattern.strip_prefix("0X")).unwrap_or(pattern);
361    if pattern.is_empty() {
362        return Err(eyre::eyre!("Vanity pattern cannot be empty"));
363    }
364
365    let is_hex = pattern.bytes().all(|byte| byte.is_ascii_hexdigit());
366    if is_hex && pattern.len() > 40 {
367        return Err(eyre::eyre!("Hex pattern must be less than 20 bytes"));
368    }
369
370    if let Ok(decoded) = hex::decode(pattern) {
371        if decoded.len() > 20 {
372            return Err(eyre::eyre!("Hex pattern must be less than 20 bytes"));
373        }
374        Ok(Either::Left(decoded))
375    } else {
376        // a non regex literal containing non-hex characters can never match
377        if !is_hex && pattern.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
378            return Err(eyre::eyre!("Pattern contains non-hex characters and can never match"));
379        }
380        let (prefix, suffix) = if is_start { ("^", "") } else { ("", "$") };
381        let pattern = if is_hex { pattern.to_ascii_lowercase() } else { pattern.to_string() };
382        Ok(Either::Right(Regex::new(&format!("{prefix}{pattern}{suffix}"))?))
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn find_simple_vanity_start() {
392        let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--starts-with", "00"]);
393        let wallet = args.run().unwrap();
394        let addr = wallet.address();
395        let addr = format!("{addr:x}");
396        assert!(addr.starts_with("00"));
397    }
398
399    #[test]
400    fn find_simple_vanity_start2() {
401        let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--starts-with", "9"]);
402        let wallet = args.run().unwrap();
403        let addr = wallet.address();
404        let addr = format!("{addr:x}");
405        assert!(addr.starts_with('9'));
406    }
407
408    #[test]
409    fn find_simple_vanity_end() {
410        let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--ends-with", "00"]);
411        let wallet = args.run().unwrap();
412        let addr = wallet.address();
413        let addr = format!("{addr:x}");
414        assert!(addr.ends_with("00"));
415    }
416
417    #[test]
418    fn save_path() {
419        let tmp = tempfile::NamedTempFile::new().unwrap();
420        let args: VanityArgs = VanityArgs::parse_from([
421            "foundry-cli",
422            "--starts-with",
423            "00",
424            "--save-path",
425            tmp.path().to_str().unwrap(),
426        ]);
427        args.run().unwrap();
428        assert!(tmp.path().exists());
429        let s = fs::read_to_string(tmp.path()).unwrap();
430        let wallets: Wallets = serde_json::from_str(&s).unwrap();
431        assert!(!wallets.wallets.is_empty());
432    }
433
434    #[test]
435    fn malformed_wallet_file_is_not_overwritten() {
436        let tmp = tempfile::NamedTempFile::new().unwrap();
437        let original = "{\"wallets\":[";
438        fs::write(tmp.path(), original).unwrap();
439
440        let err = save_wallet_to_file(&PrivateKeySigner::random(), tmp.path()).unwrap_err();
441
442        assert!(err.to_string().contains("failed to parse wallet file"));
443        assert_eq!(fs::read_to_string(tmp.path()).unwrap(), original);
444    }
445
446    #[cfg(unix)]
447    #[test]
448    fn new_wallet_file_is_owner_only() {
449        let tmp = tempfile::tempdir().unwrap();
450        let path = tmp.path().join("wallets.json");
451
452        save_wallet_to_file(&PrivateKeySigner::random(), &path).unwrap();
453
454        assert_eq!(fs::metadata(path).unwrap().permissions().mode() & 0o777, 0o600);
455    }
456
457    #[cfg(unix)]
458    #[test]
459    fn existing_wallet_file_is_made_owner_only() {
460        let tmp = tempfile::NamedTempFile::new().unwrap();
461        fs::set_permissions(tmp.path(), fs::Permissions::from_mode(0o644)).unwrap();
462
463        save_wallet_to_file(&PrivateKeySigner::random(), tmp.path()).unwrap();
464
465        assert_eq!(fs::metadata(tmp.path()).unwrap().permissions().mode() & 0o777, 0o600);
466    }
467
468    #[test]
469    fn parse_odd_length_hex_case_insensitively() {
470        let mut starts_with = [0; 20];
471        starts_with[0] = 0xa0;
472        let Either::Right(pattern) = parse_pattern("A", true).unwrap() else {
473            panic!("expected a regex pattern");
474        };
475        assert!(SingleRegexMatcher { re: pattern }.is_match(&Address::from(starts_with)));
476
477        let mut ends_with = [0; 20];
478        ends_with[19] = 0x0a;
479        let Either::Right(pattern) = parse_pattern("A", false).unwrap() else {
480            panic!("expected a regex pattern");
481        };
482        assert!(SingleRegexMatcher { re: pattern }.is_match(&Address::from(ends_with)));
483    }
484
485    #[test]
486    fn reject_overlong_odd_length_hex_pattern() {
487        let err = parse_pattern(&"1".repeat(41), true).unwrap_err();
488        assert_eq!(err.to_string(), "Hex pattern must be less than 20 bytes");
489    }
490
491    #[test]
492    fn parse_prefixed_vanity_patterns() {
493        let Either::Left(lowercase) = parse_pattern("0xdead", true).unwrap() else {
494            panic!("expected an exact hex pattern");
495        };
496        assert_eq!(lowercase, hex::decode("dead").unwrap());
497        let mut matching = [0; 20];
498        matching[..2].copy_from_slice(&lowercase);
499        assert!(LeftHexMatcher { left: lowercase }.is_match(&Address::from(matching)));
500
501        let Either::Left(uppercase) = parse_pattern("0Xdead", true).unwrap() else {
502            panic!("expected an exact hex pattern");
503        };
504        assert_eq!(uppercase, hex::decode("dead").unwrap());
505
506        let Either::Right(odd_nibble) = parse_pattern("0x9", true).unwrap() else {
507            panic!("expected a regex pattern");
508        };
509        let mut matching = [0; 20];
510        matching[0] = 0x90;
511        assert!(SingleRegexMatcher { re: odd_nibble }.is_match(&Address::from(matching)));
512    }
513
514    #[test]
515    fn reject_empty_prefixed_vanity_pattern() {
516        let err = parse_pattern("0x", true).unwrap_err();
517        assert_eq!(err.to_string(), "Vanity pattern cannot be empty");
518    }
519
520    #[test]
521    fn reject_unmatchable_pattern() {
522        // non-hex chars can never appear in a hex address
523        assert!(parse_pattern("zzz", true).is_err());
524        assert!(parse_pattern("foobar", false).is_err());
525
526        // regex patterns stay supported
527        assert!(parse_pattern("a.c", true).is_ok());
528    }
529}