Skip to main content

cast/cmd/wallet/
vanity.rs

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