cast/cmd/wallet/
vanity.rs

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