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