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
22pub type GeneratedWallet = (SigningKey, Address);
24
25#[derive(Clone, Debug, Parser)]
27pub struct VanityArgs {
28 #[arg(long, value_name = "PATTERN", required_unless_present = "ends_with")]
30 pub starts_with: Option<String>,
31
32 #[arg(long, value_name = "PATTERN")]
34 pub ends_with: Option<String>,
35
36 #[arg(long)]
40 pub nonce: Option<u64>,
41
42 #[arg(
47 long,
48 value_hint = clap::ValueHint::FilePath,
49 value_name = "PATH",
50 )]
51 pub save_path: Option<PathBuf>,
52}
53
54#[derive(Serialize, Deserialize)]
56struct WalletData {
57 address: String,
58 private_key: String,
59}
60
61#[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 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
178fn 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
210pub fn find_vanity_address<T: VanityMatcher>(matcher: T) -> Option<PrivateKeySigner> {
212 wallet_generator().find_any(create_matcher(matcher)).map(|(key, _)| key.into())
213}
214
215pub 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#[inline]
227pub fn create_matcher<T: VanityMatcher>(matcher: T) -> impl Fn(&GeneratedWallet) -> bool {
228 move |(_, addr)| matcher.is_match(addr)
229}
230
231#[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#[inline]
247pub fn wallet_generator() -> iter::Map<iter::Repeat<()>, impl Fn(()) -> GeneratedWallet> {
248 iter::repeat(()).map(|()| generate_wallet())
249}
250
251pub 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
258pub trait VanityMatcher: Send + Sync {
260 fn is_match(&self, addr: &Address) -> bool;
261}
262
263pub 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
277pub 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
290pub 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
303pub 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
317pub 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
331pub 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
344pub 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 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 assert!(parse_pattern("zzz", true).is_err());
524 assert!(parse_pattern("foobar", false).is_err());
525
526 assert!(parse_pattern("a.c", true).is_ok());
528 }
529}