1use alloy_primitives::{hex, Address};
2use alloy_signer::{k256::ecdsa::SigningKey, utils::secret_key_to_address};
3use alloy_signer_local::PrivateKeySigner;
4use clap::Parser;
5use eyre::Result;
6use foundry_common::sh_println;
7use itertools::Either;
8use rayon::iter::{self, ParallelIterator};
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use std::{
12 fs,
13 path::{Path, PathBuf},
14 time::Instant,
15};
16
17pub type GeneratedWallet = (SigningKey, Address);
19
20#[derive(Clone, Debug, Parser)]
22pub struct VanityArgs {
23 #[arg(long, value_name = "PATTERN", required_unless_present = "ends_with")]
25 pub starts_with: Option<String>,
26
27 #[arg(long, value_name = "PATTERN")]
29 pub ends_with: Option<String>,
30
31 #[arg(long)]
35 pub nonce: Option<u64>,
36
37 #[arg(
42 long,
43 value_hint = clap::ValueHint::FilePath,
44 value_name = "PATH",
45 )]
46 pub save_path: Option<PathBuf>,
47}
48
49#[derive(Serialize, Deserialize)]
51struct WalletData {
52 address: String,
53 private_key: String,
54}
55
56#[derive(Default, Serialize, Deserialize)]
58struct Wallets {
59 wallets: Vec<WalletData>,
60}
61
62impl WalletData {
63 pub fn new(wallet: &PrivateKeySigner) -> Self {
64 Self {
65 address: wallet.address().to_checksum(None),
66 private_key: format!("0x{}", hex::encode(wallet.credential().to_bytes())),
67 }
68 }
69}
70
71impl VanityArgs {
72 pub fn run(self) -> Result<PrivateKeySigner> {
73 let Self { starts_with, ends_with, nonce, save_path } = self;
74
75 let mut left_exact_hex = None;
76 let mut left_regex = None;
77 if let Some(prefix) = starts_with {
78 match parse_pattern(&prefix, true)? {
79 Either::Left(left) => left_exact_hex = Some(left),
80 Either::Right(re) => left_regex = Some(re),
81 }
82 }
83
84 let mut right_exact_hex = None;
85 let mut right_regex = None;
86 if let Some(suffix) = ends_with {
87 match parse_pattern(&suffix, false)? {
88 Either::Left(right) => right_exact_hex = Some(right),
89 Either::Right(re) => right_regex = Some(re),
90 }
91 }
92
93 macro_rules! find_vanity {
94 ($m:ident, $nonce: ident) => {
95 if let Some(nonce) = $nonce {
96 find_vanity_address_with_nonce($m, nonce)
97 } else {
98 find_vanity_address($m)
99 }
100 };
101 }
102
103 sh_println!("Starting to generate vanity address...")?;
104 let timer = Instant::now();
105
106 let wallet = match (left_exact_hex, left_regex, right_exact_hex, right_regex) {
107 (Some(left), _, Some(right), _) => {
108 let matcher = HexMatcher { left, right };
109 find_vanity!(matcher, nonce)
110 }
111 (Some(left), _, _, Some(right)) => {
112 let matcher = LeftExactRightRegexMatcher { left, right };
113 find_vanity!(matcher, nonce)
114 }
115 (_, Some(left), _, Some(right)) => {
116 let matcher = RegexMatcher { left, right };
117 find_vanity!(matcher, nonce)
118 }
119 (_, Some(left), Some(right), _) => {
120 let matcher = LeftRegexRightExactMatcher { left, right };
121 find_vanity!(matcher, nonce)
122 }
123 (Some(left), None, None, None) => {
124 let matcher = LeftHexMatcher { left };
125 find_vanity!(matcher, nonce)
126 }
127 (None, None, Some(right), None) => {
128 let matcher = RightHexMatcher { right };
129 find_vanity!(matcher, nonce)
130 }
131 (None, Some(re), None, None) => {
132 let matcher = SingleRegexMatcher { re };
133 find_vanity!(matcher, nonce)
134 }
135 (None, None, None, Some(re)) => {
136 let matcher = SingleRegexMatcher { re };
137 find_vanity!(matcher, nonce)
138 }
139 _ => unreachable!(),
140 }
141 .expect("failed to generate vanity wallet");
142
143 if let Some(save_path) = save_path {
145 save_wallet_to_file(&wallet, &save_path)?;
146 }
147
148 sh_println!(
149 "Successfully found vanity address in {:.3} seconds.{}{}\nAddress: {}\nPrivate Key: 0x{}",
150 timer.elapsed().as_secs_f64(),
151 if nonce.is_some() { "\nContract address: " } else { "" },
152 if nonce.is_some() {
153 wallet.address().create(nonce.unwrap()).to_checksum(None)
154 } else {
155 String::new()
156 },
157 wallet.address().to_checksum(None),
158 hex::encode(wallet.credential().to_bytes()),
159 )?;
160
161 Ok(wallet)
162 }
163}
164
165fn save_wallet_to_file(wallet: &PrivateKeySigner, path: &Path) -> Result<()> {
169 let mut wallets = if path.exists() {
170 let data = fs::read_to_string(path)?;
171 serde_json::from_str::<Wallets>(&data).unwrap_or_default()
172 } else {
173 Wallets::default()
174 };
175
176 wallets.wallets.push(WalletData::new(wallet));
177
178 fs::write(path, serde_json::to_string_pretty(&wallets)?)?;
179 Ok(())
180}
181
182pub fn find_vanity_address<T: VanityMatcher>(matcher: T) -> Option<PrivateKeySigner> {
184 wallet_generator().find_any(create_matcher(matcher)).map(|(key, _)| key.into())
185}
186
187pub fn find_vanity_address_with_nonce<T: VanityMatcher>(
190 matcher: T,
191 nonce: u64,
192) -> Option<PrivateKeySigner> {
193 wallet_generator().find_any(create_nonce_matcher(matcher, nonce)).map(|(key, _)| key.into())
194}
195
196#[inline]
199pub fn create_matcher<T: VanityMatcher>(matcher: T) -> impl Fn(&GeneratedWallet) -> bool {
200 move |(_, addr)| matcher.is_match(addr)
201}
202
203#[inline]
206pub fn create_nonce_matcher<T: VanityMatcher>(
207 matcher: T,
208 nonce: u64,
209) -> impl Fn(&GeneratedWallet) -> bool {
210 move |(_, addr)| {
211 let contract_addr = addr.create(nonce);
212 matcher.is_match(&contract_addr)
213 }
214}
215
216#[inline]
218pub fn wallet_generator() -> iter::Map<iter::Repeat<()>, impl Fn(()) -> GeneratedWallet> {
219 iter::repeat(()).map(|()| generate_wallet())
220}
221
222pub fn generate_wallet() -> GeneratedWallet {
224 let key = SigningKey::random(&mut rand::thread_rng());
225 let address = secret_key_to_address(&key);
226 (key, address)
227}
228
229pub trait VanityMatcher: Send + Sync {
231 fn is_match(&self, addr: &Address) -> bool;
232}
233
234pub struct HexMatcher {
236 pub left: Vec<u8>,
237 pub right: Vec<u8>,
238}
239
240impl VanityMatcher for HexMatcher {
241 #[inline]
242 fn is_match(&self, addr: &Address) -> bool {
243 let bytes = addr.as_slice();
244 bytes.starts_with(&self.left) && bytes.ends_with(&self.right)
245 }
246}
247
248pub struct LeftHexMatcher {
250 pub left: Vec<u8>,
251}
252
253impl VanityMatcher for LeftHexMatcher {
254 #[inline]
255 fn is_match(&self, addr: &Address) -> bool {
256 let bytes = addr.as_slice();
257 bytes.starts_with(&self.left)
258 }
259}
260
261pub struct RightHexMatcher {
263 pub right: Vec<u8>,
264}
265
266impl VanityMatcher for RightHexMatcher {
267 #[inline]
268 fn is_match(&self, addr: &Address) -> bool {
269 let bytes = addr.as_slice();
270 bytes.ends_with(&self.right)
271 }
272}
273
274pub struct LeftExactRightRegexMatcher {
276 pub left: Vec<u8>,
277 pub right: Regex,
278}
279
280impl VanityMatcher for LeftExactRightRegexMatcher {
281 #[inline]
282 fn is_match(&self, addr: &Address) -> bool {
283 let bytes = addr.as_slice();
284 bytes.starts_with(&self.left) && self.right.is_match(&hex::encode(bytes))
285 }
286}
287
288pub struct LeftRegexRightExactMatcher {
290 pub left: Regex,
291 pub right: Vec<u8>,
292}
293
294impl VanityMatcher for LeftRegexRightExactMatcher {
295 #[inline]
296 fn is_match(&self, addr: &Address) -> bool {
297 let bytes = addr.as_slice();
298 bytes.ends_with(&self.right) && self.left.is_match(&hex::encode(bytes))
299 }
300}
301
302pub struct SingleRegexMatcher {
304 pub re: Regex,
305}
306
307impl VanityMatcher for SingleRegexMatcher {
308 #[inline]
309 fn is_match(&self, addr: &Address) -> bool {
310 let addr = hex::encode(addr);
311 self.re.is_match(&addr)
312 }
313}
314
315pub struct RegexMatcher {
317 pub left: Regex,
318 pub right: Regex,
319}
320
321impl VanityMatcher for RegexMatcher {
322 #[inline]
323 fn is_match(&self, addr: &Address) -> bool {
324 let addr = hex::encode(addr);
325 self.left.is_match(&addr) && self.right.is_match(&addr)
326 }
327}
328
329fn parse_pattern(pattern: &str, is_start: bool) -> Result<Either<Vec<u8>, Regex>> {
330 if let Ok(decoded) = hex::decode(pattern) {
331 if decoded.len() > 20 {
332 return Err(eyre::eyre!("Hex pattern must be less than 20 bytes"));
333 }
334 Ok(Either::Left(decoded))
335 } else {
336 let (prefix, suffix) = if is_start { ("^", "") } else { ("", "$") };
337 Ok(Either::Right(Regex::new(&format!("{prefix}{pattern}{suffix}"))?))
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn find_simple_vanity_start() {
347 let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--starts-with", "00"]);
348 let wallet = args.run().unwrap();
349 let addr = wallet.address();
350 let addr = format!("{addr:x}");
351 assert!(addr.starts_with("00"));
352 }
353
354 #[test]
355 fn find_simple_vanity_start2() {
356 let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--starts-with", "9"]);
357 let wallet = args.run().unwrap();
358 let addr = wallet.address();
359 let addr = format!("{addr:x}");
360 assert!(addr.starts_with('9'));
361 }
362
363 #[test]
364 fn find_simple_vanity_end() {
365 let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--ends-with", "00"]);
366 let wallet = args.run().unwrap();
367 let addr = wallet.address();
368 let addr = format!("{addr:x}");
369 assert!(addr.ends_with("00"));
370 }
371
372 #[test]
373 fn save_path() {
374 let tmp = tempfile::NamedTempFile::new().unwrap();
375 let args: VanityArgs = VanityArgs::parse_from([
376 "foundry-cli",
377 "--starts-with",
378 "00",
379 "--save-path",
380 tmp.path().to_str().unwrap(),
381 ]);
382 args.run().unwrap();
383 assert!(tmp.path().exists());
384 let s = fs::read_to_string(tmp.path()).unwrap();
385 let wallets: Wallets = serde_json::from_str(&s).unwrap();
386 assert!(!wallets.wallets.is_empty());
387 }
388}