soil_cli/commands/
vanity.rs1use crate::{
10 error, utils, with_crypto_scheme, CryptoSchemeFlag, NetworkSchemeFlag, OutputTypeFlag,
11};
12use clap::Parser;
13use rand::{rngs::OsRng, RngCore};
14use subsoil::core::crypto::{unwrap_or_default_ss58_version, Ss58AddressFormat, Ss58Codec};
15use subsoil::runtime::traits::IdentifyAccount;
16use utils::print_from_uri;
17
18#[derive(Debug, Clone, Parser)]
20#[command(name = "vanity", about = "Generate a seed that provides a vanity address")]
21pub struct VanityCmd {
22 #[arg(long, value_parser = assert_non_empty_string)]
24 pattern: String,
25
26 #[allow(missing_docs)]
27 #[clap(flatten)]
28 network_scheme: NetworkSchemeFlag,
29
30 #[allow(missing_docs)]
31 #[clap(flatten)]
32 output_scheme: OutputTypeFlag,
33
34 #[allow(missing_docs)]
35 #[clap(flatten)]
36 crypto_scheme: CryptoSchemeFlag,
37}
38
39impl VanityCmd {
40 pub fn run(&self) -> error::Result<()> {
42 let formatted_seed = with_crypto_scheme!(
43 self.crypto_scheme.scheme,
44 generate_key(
45 &self.pattern,
46 unwrap_or_default_ss58_version(self.network_scheme.network)
47 ),
48 )?;
49
50 with_crypto_scheme!(
51 self.crypto_scheme.scheme,
52 print_from_uri(
53 &formatted_seed,
54 None,
55 self.network_scheme.network,
56 self.output_scheme.output_type,
57 ),
58 );
59 Ok(())
60 }
61}
62
63fn generate_key<Pair>(
65 desired: &str,
66 network_override: Ss58AddressFormat,
67) -> Result<String, &'static str>
68where
69 Pair: subsoil::core::Pair,
70 Pair::Public: IdentifyAccount,
71 <Pair::Public as IdentifyAccount>::AccountId: Ss58Codec,
72{
73 println!("Generating key containing pattern '{}'", desired);
74
75 let top = 45 + (desired.len() * 48);
76 let mut best = 0;
77 let mut seed = Pair::Seed::default();
78 let mut done = 0;
79
80 loop {
81 if done % 100000 == 0 {
82 OsRng.fill_bytes(seed.as_mut());
83 } else {
84 next_seed(seed.as_mut());
85 }
86
87 let p = Pair::from_seed(&seed);
88 let ss58 = p.public().into_account().to_ss58check_with_version(network_override);
89 let score = calculate_score(desired, &ss58);
90 if score > best || desired.len() < 2 {
91 best = score;
92 if best >= top {
93 println!("best: {} == top: {}", best, top);
94 return Ok(utils::format_seed::<Pair>(seed.clone()));
95 }
96 }
97 done += 1;
98
99 if done % good_waypoint(done) == 0 {
100 println!("{} keys searched; best is {}/{} complete", done, best, top);
101 }
102 }
103}
104
105fn good_waypoint(done: u64) -> u64 {
106 match done {
107 0..=1_000_000 => 100_000,
108 1_000_001..=10_000_000 => 1_000_000,
109 10_000_001..=100_000_000 => 10_000_000,
110 100_000_001.. => 100_000_000,
111 }
112}
113
114fn next_seed(seed: &mut [u8]) {
115 for s in seed {
116 match s {
117 255 => {
118 *s = 0;
119 },
120 _ => {
121 *s += 1;
122 break;
123 },
124 }
125 }
126}
127
128fn calculate_score(_desired: &str, key: &str) -> usize {
131 for truncate in 0.._desired.len() {
132 let snip_size = _desired.len() - truncate;
133 let truncated = &_desired[0..snip_size];
134 if let Some(pos) = key.find(truncated) {
135 return (47 - pos) + (snip_size * 48);
136 }
137 }
138 0
139}
140
141fn assert_non_empty_string(pattern: &str) -> Result<String, &'static str> {
143 if pattern.is_empty() {
144 Err("Pattern must not be empty")
145 } else {
146 Ok(pattern.to_string())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use subsoil::core::{
154 crypto::{default_ss58_version, Ss58AddressFormatRegistry, Ss58Codec},
155 sr25519, Pair,
156 };
157
158 #[test]
159 fn vanity() {
160 let vanity = VanityCmd::parse_from(&["vanity", "--pattern", "j"]);
161 assert!(vanity.run().is_ok());
162 }
163
164 #[test]
165 fn test_generation_with_single_char() {
166 let seed = generate_key::<sr25519::Pair>("ab", default_ss58_version()).unwrap();
167 assert!(sr25519::Pair::from_seed_slice(&array_bytes::hex2bytes_unchecked(&seed))
168 .unwrap()
169 .public()
170 .to_ss58check()
171 .contains("ab"));
172 }
173
174 #[test]
175 fn generate_key_respects_network_override() {
176 let seed =
177 generate_key::<sr25519::Pair>("ab", Ss58AddressFormatRegistry::PolkadotAccount.into())
178 .unwrap();
179 assert!(sr25519::Pair::from_seed_slice(&array_bytes::hex2bytes_unchecked(&seed))
180 .unwrap()
181 .public()
182 .to_ss58check_with_version(Ss58AddressFormatRegistry::PolkadotAccount.into())
183 .contains("ab"));
184 }
185
186 #[test]
187 fn test_score_1_char_100() {
188 let score = calculate_score("j", "5jolkadotwHY5k9GpdTgpqs9xjuNvtv8EcwCFpEeyEf3KHim");
189 assert_eq!(score, 94);
190 }
191
192 #[test]
193 fn test_score_100() {
194 let score = calculate_score("Polkadot", "5PolkadotwHY5k9GpdTgpqs9xjuNvtv8EcwCFpEeyEf3KHim");
195 assert_eq!(score, 430);
196 }
197
198 #[test]
199 fn test_score_50_2() {
200 assert_eq!(
202 calculate_score("Polkadot", "5PolkXXXXwHY5k9GpdTgpqs9xjuNvtv8EcwCFpEeyEf3KHim"),
203 238
204 );
205 }
206
207 #[test]
208 fn test_score_0() {
209 assert_eq!(
210 calculate_score("Polkadot", "5GUWv4bLCchGUHJrzULXnh4JgXsMpTKRnjuXTY7Qo1Kh9uYK"),
211 0
212 );
213 }
214}