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_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 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 contract address matcher function that uses the specified nonce.
204/// The returned function takes a reference to a [GeneratedWallet] and returns
205/// whether the contract address created with the nonce matches using `matcher`.
206#[inline]
207pub fn create_nonce_matcher<T: VanityMatcher>(
208    matcher: T,
209    nonce: u64,
210) -> impl Fn(&GeneratedWallet) -> bool {
211    move |(_, addr)| {
212        let contract_addr = addr.create(nonce);
213        matcher.is_match(&contract_addr)
214    }
215}
216
217/// Returns an infinite parallel iterator which yields a [GeneratedWallet].
218#[inline]
219pub fn wallet_generator() -> iter::Map<iter::Repeat<()>, impl Fn(()) -> GeneratedWallet> {
220    iter::repeat(()).map(|()| generate_wallet())
221}
222
223/// Generates a random K-256 signing key and derives its Ethereum address.
224pub fn generate_wallet() -> GeneratedWallet {
225    let key = SigningKey::random(&mut rand_08::thread_rng());
226    let address = secret_key_to_address(&key);
227    (key, address)
228}
229
230/// A trait to match vanity addresses.
231pub trait VanityMatcher: Send + Sync {
232    fn is_match(&self, addr: &Address) -> bool;
233}
234
235/// Matches start and end hex.
236pub struct HexMatcher {
237    pub left: Vec<u8>,
238    pub right: Vec<u8>,
239}
240
241impl VanityMatcher for HexMatcher {
242    #[inline]
243    fn is_match(&self, addr: &Address) -> bool {
244        let bytes = addr.as_slice();
245        bytes.starts_with(&self.left) && bytes.ends_with(&self.right)
246    }
247}
248
249/// Matches only start hex.
250pub struct LeftHexMatcher {
251    pub left: Vec<u8>,
252}
253
254impl VanityMatcher for LeftHexMatcher {
255    #[inline]
256    fn is_match(&self, addr: &Address) -> bool {
257        let bytes = addr.as_slice();
258        bytes.starts_with(&self.left)
259    }
260}
261
262/// Matches only end hex.
263pub struct RightHexMatcher {
264    pub right: Vec<u8>,
265}
266
267impl VanityMatcher for RightHexMatcher {
268    #[inline]
269    fn is_match(&self, addr: &Address) -> bool {
270        let bytes = addr.as_slice();
271        bytes.ends_with(&self.right)
272    }
273}
274
275/// Matches start hex and end regex.
276pub struct LeftExactRightRegexMatcher {
277    pub left: Vec<u8>,
278    pub right: Regex,
279}
280
281impl VanityMatcher for LeftExactRightRegexMatcher {
282    #[inline]
283    fn is_match(&self, addr: &Address) -> bool {
284        let bytes = addr.as_slice();
285        bytes.starts_with(&self.left) && self.right.is_match(&hex::encode(bytes))
286    }
287}
288
289/// Matches start regex and end hex.
290pub struct LeftRegexRightExactMatcher {
291    pub left: Regex,
292    pub right: Vec<u8>,
293}
294
295impl VanityMatcher for LeftRegexRightExactMatcher {
296    #[inline]
297    fn is_match(&self, addr: &Address) -> bool {
298        let bytes = addr.as_slice();
299        bytes.ends_with(&self.right) && self.left.is_match(&hex::encode(bytes))
300    }
301}
302
303/// Matches a single regex.
304pub struct SingleRegexMatcher {
305    pub re: Regex,
306}
307
308impl VanityMatcher for SingleRegexMatcher {
309    #[inline]
310    fn is_match(&self, addr: &Address) -> bool {
311        let addr = hex::encode(addr);
312        self.re.is_match(&addr)
313    }
314}
315
316/// Matches start and end regex.
317pub struct RegexMatcher {
318    pub left: Regex,
319    pub right: Regex,
320}
321
322impl VanityMatcher for RegexMatcher {
323    #[inline]
324    fn is_match(&self, addr: &Address) -> bool {
325        let addr = hex::encode(addr);
326        self.left.is_match(&addr) && self.right.is_match(&addr)
327    }
328}
329
330fn parse_pattern(pattern: &str, is_start: bool) -> Result<Either<Vec<u8>, Regex>> {
331    if let Ok(decoded) = hex::decode(pattern) {
332        if decoded.len() > 20 {
333            return Err(eyre::eyre!("Hex pattern must be less than 20 bytes"));
334        }
335        Ok(Either::Left(decoded))
336    } else {
337        let (prefix, suffix) = if is_start { ("^", "") } else { ("", "$") };
338        Ok(Either::Right(Regex::new(&format!("{prefix}{pattern}{suffix}"))?))
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn find_simple_vanity_start() {
348        let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--starts-with", "00"]);
349        let wallet = args.run().unwrap();
350        let addr = wallet.address();
351        let addr = format!("{addr:x}");
352        assert!(addr.starts_with("00"));
353    }
354
355    #[test]
356    fn find_simple_vanity_start2() {
357        let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--starts-with", "9"]);
358        let wallet = args.run().unwrap();
359        let addr = wallet.address();
360        let addr = format!("{addr:x}");
361        assert!(addr.starts_with('9'));
362    }
363
364    #[test]
365    fn find_simple_vanity_end() {
366        let args: VanityArgs = VanityArgs::parse_from(["foundry-cli", "--ends-with", "00"]);
367        let wallet = args.run().unwrap();
368        let addr = wallet.address();
369        let addr = format!("{addr:x}");
370        assert!(addr.ends_with("00"));
371    }
372
373    #[test]
374    fn save_path() {
375        let tmp = tempfile::NamedTempFile::new().unwrap();
376        let args: VanityArgs = VanityArgs::parse_from([
377            "foundry-cli",
378            "--starts-with",
379            "00",
380            "--save-path",
381            tmp.path().to_str().unwrap(),
382        ]);
383        args.run().unwrap();
384        assert!(tmp.path().exists());
385        let s = fs::read_to_string(tmp.path()).unwrap();
386        let wallets: Wallets = serde_json::from_str(&s).unwrap();
387        assert!(!wallets.wallets.is_empty());
388    }
389}