1use fuel_crypto::{fuel_types::Address, PublicKey, SecretKey};
2use fuels_accounts::signers::{
3 derivation::DEFAULT_DERIVATION_PATH, private_key::generate_mnemonic_phrase,
4};
5use fuels_core::types::checksum_address::checksum_encode;
6use rayon::iter::{self, Either, ParallelIterator};
7use regex::Regex;
8use serde_json::json;
9use std::{
10 path::PathBuf,
11 time::{Duration, Instant},
12};
13use tokio::runtime::Runtime;
14
15forc_util::cli_examples! {
16 crate::Command {
17 [ Generate a checksummed vanity address with a given prefix => "forc crypto vanity --starts-with \"aaa\"" ]
18 [ Generate a checksummed vanity address with a given suffix => "forc crypto vanity --ends-with \"aaa\"" ]
19 [ Generate a checksummed vanity address with a given prefix and suffix => "forc crypto vanity --starts-with \"00\" --ends-with \"ff\"" ]
20 [ Generate a checksummed vanity address with a given regex pattern => "forc crypto vanity --regex \"^00.*ff$\"" ]
21 }
22}
23
24fn validate_hex_string(s: &str) -> Result<String, String> {
25 if !s.chars().all(|c| c.is_ascii_hexdigit()) {
26 return Err("Pattern must contain only hex characters (0-9, a-f)".to_string());
27 }
28 Ok(s.to_string())
29}
30
31fn validate_regex_pattern(s: &str) -> Result<String, String> {
32 if s.len() > 128 {
33 return Err("Regex pattern too long: max 128 characters".to_string());
34 }
35
36 if let Err(e) = Regex::new(&format!("(?i){s}")) {
37 return Err(format!("Invalid regex pattern: {e}"));
38 }
39
40 Ok(s.to_string())
41}
42
43#[derive(Debug, clap::Parser)]
44#[clap(
45 version,
46 about = "Generate a vanity address",
47 after_help = "Generate vanity addresses for the Fuel blockchain"
48)]
49pub struct Arg {
50 #[arg(
52 long,
53 value_name = "HEX_STRING",
54 required_unless_present = "ends_with",
55 required_unless_present = "regex",
56 conflicts_with = "regex",
57 value_parser = validate_hex_string,
58 )]
59 pub starts_with: Option<String>,
60
61 #[arg(long, value_name = "HEX_STRING", conflicts_with = "regex", value_parser = validate_hex_string)]
63 pub ends_with: Option<String>,
64
65 #[arg(long, value_name = "PATTERN", conflicts_with = "starts_with", value_parser = validate_regex_pattern)]
67 pub regex: Option<String>,
68
69 #[arg(long, value_name = "SECONDS")]
71 pub timeout: Option<u64>,
72
73 #[arg(long)]
75 pub mnemonic: bool,
76
77 #[arg(long, value_hint = clap::ValueHint::FilePath, value_name = "PATH")]
79 pub save_path: Option<PathBuf>,
80}
81
82impl Arg {
83 pub fn validate(&self) -> anyhow::Result<()> {
84 let total_length = self.starts_with.as_ref().map_or(0, |s| s.len())
85 + self.ends_with.as_ref().map_or(0, |s| s.len());
86 if total_length > 64 {
87 return Err(anyhow::anyhow!(
88 "Combined pattern length exceeds 64 characters"
89 ));
90 }
91 Ok(())
92 }
93}
94
95pub fn handler(args: Arg) -> anyhow::Result<serde_json::Value> {
96 args.validate()?;
97
98 let Arg {
99 starts_with,
100 ends_with,
101 regex,
102 mnemonic,
103 timeout,
104 save_path,
105 } = args;
106
107 let matcher = if let Some(pattern) = regex {
108 Either::Left(RegexMatcher::new(&pattern)?)
109 } else {
110 let starts_with = starts_with.as_deref().unwrap_or("");
111 let ends_with = ends_with.as_deref().unwrap_or("");
112 Either::Right(HexMatcher::new(starts_with, ends_with)?)
113 };
114
115 println!("Starting to generate vanity address...");
116 let start_time = Instant::now();
117
118 let result = find_vanity_address_with_timeout(matcher, mnemonic, timeout)?;
119 let (address, secret_key, mnemonic) = result;
120
121 let duration = start_time.elapsed();
122 println!(
123 "Successfully found vanity address in {:.3} seconds.\n",
124 duration.as_secs_f64()
125 );
126
127 let checksum_address = checksum_encode(&address.to_string())?;
128 let result = if let Some(mnemonic) = mnemonic {
129 json!({
130 "Address": checksum_address,
131 "PrivateKey": hex::encode(secret_key.as_ref()),
132 "Mnemonic": mnemonic,
133 })
134 } else {
135 json!({
136 "Address": checksum_address,
137 "PrivateKey": hex::encode(secret_key.as_ref()),
138 })
139 };
140
141 if let Some(path) = save_path {
142 std::fs::write(path, serde_json::to_string_pretty(&result)?)?;
143 }
144
145 Ok(result)
146}
147
148pub trait VanityMatcher: Send + Sync + 'static {
149 fn is_match(&self, addr: &Address) -> bool;
150}
151
152pub struct HexMatcher {
153 prefix: String,
154 suffix: String,
155}
156
157impl HexMatcher {
158 pub fn new(prefix: &str, suffix: &str) -> anyhow::Result<Self> {
159 Ok(Self {
160 prefix: prefix.to_lowercase(),
161 suffix: suffix.to_lowercase(),
162 })
163 }
164}
165
166impl VanityMatcher for HexMatcher {
167 fn is_match(&self, addr: &Address) -> bool {
168 let hex_addr = hex::encode(addr.as_ref()).to_lowercase();
169 hex_addr.starts_with(&self.prefix) && hex_addr.ends_with(&self.suffix)
170 }
171}
172
173pub struct RegexMatcher {
174 re: Regex,
175}
176
177impl RegexMatcher {
178 pub fn new(pattern: &str) -> anyhow::Result<Self> {
179 let re = Regex::new(&format!("(?i){pattern}"))?;
180 Ok(Self { re })
181 }
182}
183
184impl VanityMatcher for RegexMatcher {
185 fn is_match(&self, addr: &Address) -> bool {
186 let addr = hex::encode(addr.as_ref());
187 self.re.is_match(&addr)
188 }
189}
190
191use std::sync::atomic::{AtomicBool, Ordering};
192use std::sync::Arc;
193
194pub fn find_vanity_address_with_timeout(
195 matcher: Either<RegexMatcher, HexMatcher>,
196 use_mnemonic: bool,
197 timeout_secs: Option<u64>,
198) -> anyhow::Result<(Address, SecretKey, Option<String>)> {
199 let should_stop = Arc::new(AtomicBool::new(false));
200 let should_stop_clone = should_stop.clone();
201
202 let generate_wallet = move || {
203 let breakpoint = if use_mnemonic { 1_000 } else { 100_000 };
204 let start = Instant::now();
205 let attempts = std::sync::atomic::AtomicUsize::new(0);
206
207 wallet_generator(use_mnemonic)
208 .find_any(|result| {
209 if should_stop.load(Ordering::Relaxed) {
211 return true; }
213
214 let current = attempts.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
215 if current != 0 && current.is_multiple_of(breakpoint) {
216 let elapsed = start.elapsed().as_secs_f64();
217 let rate = current as f64 / elapsed;
218 println!("└─ tried {current} addresses ({rate:.2} addresses/sec)...");
219 }
220
221 if let Ok((addr, _, _)) = result {
222 match &matcher {
223 Either::Left(regex_matcher) => regex_matcher.is_match(addr),
224 Either::Right(hex_matcher) => hex_matcher.is_match(addr),
225 }
226 } else {
227 false
228 }
229 })
230 .ok_or_else(|| anyhow::anyhow!("No matching address found"))?
231 };
232
233 let Some(secs) = timeout_secs else {
234 return generate_wallet();
235 };
236
237 Runtime::new()?.block_on(async {
238 let generation_task = tokio::task::spawn_blocking(generate_wallet);
239
240 tokio::select! {
241 result = generation_task => {
242 match result {
243 Ok(wallet_result) => wallet_result,
244 Err(_) => Err(anyhow::anyhow!("No matching address found")),
245 }
246 }
247 _ = tokio::time::sleep(Duration::from_secs(secs)) => {
248 should_stop_clone.store(true, Ordering::Relaxed);
250 tokio::time::sleep(Duration::from_millis(100)).await;
252 Err(anyhow::anyhow!("Vanity address generation timed out after {} seconds", secs))
253 }
254 }
255 })
256}
257
258#[inline]
259fn wallet_generator(
260 use_mnemonic: bool,
261) -> impl ParallelIterator<Item = anyhow::Result<(Address, SecretKey, Option<String>)>> {
262 iter::repeat(()).map(move |()| generate_wallet(use_mnemonic))
263}
264
265fn generate_wallet(use_mnemonic: bool) -> anyhow::Result<(Address, SecretKey, Option<String>)> {
266 let mut rng = rand::thread_rng();
267
268 let (private_key, mnemonic) = if use_mnemonic {
269 let mnemonic = generate_mnemonic_phrase(&mut rng, 24)?;
270 let private_key =
271 SecretKey::new_from_mnemonic_phrase_with_path(&mnemonic, DEFAULT_DERIVATION_PATH)?;
272 (private_key, Some(mnemonic))
273 } else {
274 (SecretKey::random(&mut rng), None)
275 };
276
277 let public = PublicKey::from(&private_key);
278 let hashed = public.hash();
279 let address = Address::from(*hashed);
280
281 Ok((address, private_key, mnemonic))
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use clap::Parser;
288
289 fn parse_args(args: Vec<&str>) -> Result<Arg, String> {
291 let args =
292 Arg::try_parse_from(std::iter::once("test").chain(args)).map_err(|e| e.to_string())?;
293 args.validate().map_err(|e| e.to_string())?;
294 Ok(args)
295 }
296
297 #[test]
298 fn test_invalid_hex_characters() {
299 let result = parse_args(vec!["--starts-with", "xyz"]);
300 assert!(result.is_err());
301 assert_eq!(result.unwrap_err(), "error: invalid value 'xyz' for '--starts-with <HEX_STRING>': Pattern must contain only hex characters (0-9, a-f)\n\nFor more information, try '--help'.\n");
302 }
303
304 #[test]
305 fn test_pattern_too_long() {
306 let result = parse_args(vec![
307 "--starts-with",
308 &"a".repeat(32),
309 "--ends-with",
310 &"b".repeat(33),
311 ]);
312 assert!(result.is_err());
313 assert_eq!(
314 result.unwrap_err(),
315 "Combined pattern length exceeds 64 characters"
316 );
317 }
318
319 #[test]
320 fn test_invalid_regex_syntax() {
321 let result = parse_args(vec!["--regex", "["]);
322 assert!(result.is_err());
323 assert_eq!(result.unwrap_err(), "error: invalid value '[' for '--regex <PATTERN>': Invalid regex pattern: regex parse error:\n (?i)[\n ^\nerror: unclosed character class\n\nFor more information, try '--help'.\n");
324 }
325
326 #[test]
327 fn test_regex_too_long() {
328 let result = parse_args(vec!["--regex", &"a".repeat(129)]);
329 assert!(result.is_err());
330 assert_eq!(result.unwrap_err(), "error: invalid value 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' for '--regex <PATTERN>': Regex pattern too long: max 128 characters\n\nFor more information, try '--help'.\n");
331 }
332
333 #[test]
334 fn test_conflicting_args() {
335 let result = parse_args(vec!["--starts-with", "aa", "--regex", "^aa"]);
336 assert!(result.is_err());
337 assert_eq!(result.unwrap_err(), "error: the argument '--starts-with <HEX_STRING>' cannot be used with '--regex <PATTERN>'\n\nUsage: test --starts-with <HEX_STRING>\n\nFor more information, try '--help'.\n");
338 }
339
340 #[test]
341 fn test_timeout_respected() {
342 let args = parse_args(vec!["--starts-with", "fffffffffffff", "--timeout", "1"]).unwrap();
344
345 let result = handler(args);
346 assert!(result.is_err());
347 assert_eq!(
348 result.unwrap_err().to_string(),
349 "Vanity address generation timed out after 1 seconds"
350 );
351 }
352
353 #[test]
355 fn test_valid_short_prefix() {
356 let args = parse_args(vec!["--starts-with", "a"]).unwrap();
357 let result = handler(args).unwrap();
358 let address = result["Address"].as_str().unwrap();
359 assert!(
360 address.to_lowercase().starts_with("0xa"),
361 "Address should start with 'a'"
362 );
363 }
364
365 #[test]
366 fn test_valid_short_suffix() {
367 let args = parse_args(vec!["--ends-with", "a"]).unwrap();
368 let result = handler(args).unwrap();
369 let address = result["Address"].as_str().unwrap();
370 assert!(
371 address.to_lowercase().ends_with('a'),
372 "Address should end with 'a'"
373 );
374 }
375
376 #[test]
377 fn test_both_prefix_and_suffix() {
378 let args = parse_args(vec!["--starts-with", "a", "--ends-with", "b"]).unwrap();
379 let result = handler(args).unwrap();
380 let address = result["Address"].as_str().unwrap().to_lowercase();
381 assert!(address.starts_with("0xa"), "Address should start with 'a'");
382 assert!(address.ends_with('b'), "Address should end with 'b'");
383 }
384
385 #[test]
386 fn test_simple_regex() {
387 let args = parse_args(vec!["--regex", "^a.*b$"]).unwrap();
388 let result = handler(args).unwrap();
389 let address = result["Address"].as_str().unwrap().to_lowercase();
390 assert!(address.starts_with("0xa"), "Address should start with 'a'");
391 assert!(address.ends_with('b'), "Address should end with 'b'");
392 }
393
394 #[test]
395 fn test_simple_regex_uppercase() {
396 let args = parse_args(vec!["--regex", "^A.*B$"]).unwrap();
397 let result = handler(args).unwrap();
398 let address = result["Address"].as_str().unwrap().to_lowercase();
399 assert!(address.starts_with("0xa"), "Address should start with 'a'");
400 assert!(address.ends_with('b'), "Address should end with 'b'");
401 }
402
403 #[test]
404 fn test_mnemonic_generation() {
405 let args = parse_args(vec!["--starts-with", "a", "--mnemonic"]).unwrap();
406 let result = handler(args).unwrap();
407
408 assert!(result["Mnemonic"].is_string(), "Mnemonic should be present");
409 assert_eq!(
410 result["Mnemonic"]
411 .as_str()
412 .unwrap()
413 .split_whitespace()
414 .count(),
415 24,
416 "Mnemonic should have 24 words"
417 );
418
419 let address = result["Address"].as_str().unwrap();
420 assert!(
421 address.to_lowercase().starts_with("0xa"),
422 "Address should start with 'a'"
423 );
424 }
425
426 #[test]
427 fn test_save_path() {
428 let tmp = tempfile::NamedTempFile::new().unwrap();
429 let args = parse_args(vec![
430 "--starts-with",
431 "a",
432 "--save-path",
433 tmp.path().to_str().unwrap(),
434 ])
435 .unwrap();
436
437 handler(args).unwrap();
438
439 assert!(tmp.path().exists(), "File should exist");
440 let content = std::fs::read_to_string(tmp.path()).unwrap();
441 let saved_result: serde_json::Value = serde_json::from_str(&content).unwrap();
442 assert!(
443 saved_result["Address"].is_string(),
444 "Saved result should contain an Address"
445 );
446 assert!(
447 saved_result["PrivateKey"].is_string(),
448 "Saved result should contain a PrivateKey"
449 );
450 }
451}