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