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#[derive(Clone, Debug, Parser)]
23pub struct VanityArgs {
24 #[arg(long, value_name = "PATTERN", required_unless_present = "ends_with")]
26 pub starts_with: Option<String>,
27
28 #[arg(long, value_name = "PATTERN")]
30 pub ends_with: Option<String>,
31
32 #[arg(long)]
36 pub nonce: Option<u64>,
37
38 #[arg(
43 long,
44 value_hint = clap::ValueHint::FilePath,
45 value_name = "PATH",
46 )]
47 pub save_path: Option<PathBuf>,
48}
49
50#[derive(Serialize, Deserialize)]
52struct WalletData {
53 address: String,
54 private_key: String,
55}
56
57#[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
113fn 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
145fn 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#[derive(Debug)]
159enum Pattern {
160 Exact(Vec<u8>),
161 Re(Regex),
162}
163
164struct 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 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 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 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 assert!(parse_pattern("a.c", true).is_ok());
289
290 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 ("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}