1use super::*;
18use leo_errors::UtilError;
19use leo_package::{Env, NetworkName};
20
21#[cfg(not(feature = "only_testnet"))]
22use snarkvm::prelude::{CanaryV0, MainnetV0};
23use snarkvm::{
24 console::program::{Signature, ToFields, Value},
25 prelude::{Address, Network, PrivateKey, TestnetV0, ViewKey},
26};
27
28use crossterm::ExecutableCommand;
29use rand::SeedableRng;
30use rand_chacha::ChaChaRng;
31use std::{
32 io::{self, Read, Write},
33 path::PathBuf,
34 str::FromStr,
35};
36
37#[derive(Parser, Debug)]
39pub enum Account {
40 New {
42 #[clap(short = 's', long)]
44 seed: Option<u64>,
45 #[clap(short = 'w', long)]
47 write: bool,
48 #[clap(long)]
50 discreet: bool,
51 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
52 network: String,
53 #[clap(
54 short = 'e',
55 long,
56 help = "Endpoint to retrieve network state from.",
57 default_value = "https://api.explorer.provable.com/v1"
58 )]
59 endpoint: String,
60 },
61 Import {
63 private_key: Option<String>,
65 #[clap(short = 'w', long)]
67 write: bool,
68 #[clap(long)]
70 discreet: bool,
71 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
72 network: String,
73 #[clap(
74 short = 'e',
75 long,
76 help = "Endpoint to retrieve network state from.",
77 default_value = "https://api.explorer.provable.com/v1"
78 )]
79 endpoint: String,
80 },
81 Sign {
83 #[clap(long = "private-key")]
85 private_key: Option<String>,
86 #[clap(long = "private-key-file")]
88 private_key_file: Option<String>,
89 #[clap(short = 'm', long)]
91 message: String,
92 #[clap(short = 'r', long)]
94 raw: bool,
95 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
96 network: String,
97 },
98 Verify {
100 #[clap(short = 'a', long)]
102 address: String,
103 #[clap(short = 's', long)]
105 signature: String,
106 #[clap(short = 'm', long)]
108 message: String,
109 #[clap(short = 'r', long)]
111 raw: bool,
112 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
113 network: String,
114 },
115}
116
117impl Command for Account {
118 type Input = ();
119 type Output = ();
120
121 fn prelude(&self, _: Context) -> Result<Self::Input>
122 where
123 Self: Sized,
124 {
125 Ok(())
126 }
127
128 fn apply(self, ctx: Context, _: Self::Input) -> Result<Self::Output>
129 where
130 Self: Sized,
131 {
132 match self {
133 Account::New { seed, write, discreet, network, endpoint } => {
134 let network: NetworkName = network.parse()?;
136 match network {
137 NetworkName::TestnetV0 => {
138 generate_new_account::<TestnetV0>(network, seed, write, discreet, &ctx, endpoint)
139 }
140 NetworkName::MainnetV0 => {
141 #[cfg(feature = "only_testnet")]
142 panic!("Mainnet chosen with only_testnet feature");
143 #[cfg(not(feature = "only_testnet"))]
144 generate_new_account::<MainnetV0>(network, seed, write, discreet, &ctx, endpoint)
145 }
146 NetworkName::CanaryV0 => {
147 #[cfg(feature = "only_testnet")]
148 panic!("Canary chosen with only_testnet feature");
149 #[cfg(not(feature = "only_testnet"))]
150 generate_new_account::<CanaryV0>(network, seed, write, discreet, &ctx, endpoint)
151 }
152 }?
153 }
154 Account::Import { private_key, write, discreet, network, endpoint } => {
155 let network: NetworkName = network.parse()?;
157 match network {
158 NetworkName::TestnetV0 => {
159 import_account::<TestnetV0>(network, private_key, write, discreet, &ctx, endpoint)
160 }
161 NetworkName::MainnetV0 => {
162 #[cfg(feature = "only_testnet")]
163 panic!("Mainnet chosen with only_testnet feature");
164 #[cfg(not(feature = "only_testnet"))]
165 import_account::<MainnetV0>(network, private_key, write, discreet, &ctx, endpoint)
166 }
167 NetworkName::CanaryV0 => {
168 #[cfg(feature = "only_testnet")]
169 panic!("Canary chosen with only_testnet feature");
170 #[cfg(not(feature = "only_testnet"))]
171 import_account::<CanaryV0>(network, private_key, write, discreet, &ctx, endpoint)
172 }
173 }?
174 }
175 Self::Sign { message, raw, private_key, private_key_file, network } => {
176 let network: NetworkName = network.parse()?;
178 let result = match network {
179 NetworkName::TestnetV0 => sign_message::<TestnetV0>(message, raw, private_key, private_key_file),
180 NetworkName::MainnetV0 => {
181 #[cfg(feature = "only_testnet")]
182 panic!("Mainnet chosen with only_testnet feature");
183 #[cfg(not(feature = "only_testnet"))]
184 sign_message::<MainnetV0>(message, raw, private_key, private_key_file)
185 }
186 NetworkName::CanaryV0 => {
187 #[cfg(feature = "only_testnet")]
188 panic!("Canary chosen with only_testnet feature");
189 #[cfg(not(feature = "only_testnet"))]
190 sign_message::<CanaryV0>(message, raw, private_key, private_key_file)
191 }
192 }?;
193 println!("{result}")
194 }
195 Self::Verify { address, signature, message, raw, network } => {
196 let network: NetworkName = network.parse()?;
198 let result = match network {
199 NetworkName::TestnetV0 => verify_message::<TestnetV0>(address, signature, message, raw),
200 NetworkName::MainnetV0 => {
201 #[cfg(feature = "only_testnet")]
202 panic!("Mainnet chosen with only_testnet feature");
203 #[cfg(not(feature = "only_testnet"))]
204 verify_message::<MainnetV0>(address, signature, message, raw)
205 }
206 NetworkName::CanaryV0 => {
207 #[cfg(feature = "only_testnet")]
208 panic!("Canary chosen with only_testnet feature");
209 #[cfg(not(feature = "only_testnet"))]
210 verify_message::<CanaryV0>(address, signature, message, raw)
211 }
212 }?;
213 println!("{result}")
214 }
215 }
216 Ok(())
217 }
218}
219
220fn generate_new_account<N: Network>(
224 network: NetworkName,
225 seed: Option<u64>,
226 write: bool,
227 discreet: bool,
228 ctx: &Context,
229 endpoint: String,
230) -> Result<()> {
231 let private_key = match seed {
233 Some(seed) => PrivateKey::<N>::new(&mut ChaChaRng::seed_from_u64(seed)),
235 None => PrivateKey::new(&mut ChaChaRng::from_entropy()),
237 }
238 .map_err(CliError::failed_to_parse_seed)?;
239
240 print_keys(private_key, discreet)?;
242
243 if write {
245 write_to_env_file(network, private_key, ctx, endpoint)?;
246 }
247 Ok(())
248}
249
250fn import_account<N: Network>(
252 network: NetworkName,
253 private_key: Option<String>,
254 write: bool,
255 discreet: bool,
256 ctx: &Context,
257 endpoint: String,
258) -> Result<()> {
259 let priv_key = match discreet {
260 true => {
261 let private_key_input = rpassword::prompt_password("Please enter your private key: ").unwrap();
262 FromStr::from_str(&private_key_input).map_err(CliError::failed_to_parse_private_key)?
263 }
264 false => match private_key {
265 Some(private_key) => FromStr::from_str(&private_key).map_err(CliError::failed_to_parse_private_key)?,
266 None => {
267 return Err(CliError::failed_to_execute_account(
268 "PRIVATE_KEY shouldn't be empty when --discreet is false",
269 )
270 .into());
271 }
272 },
273 };
274
275 print_keys::<N>(priv_key, discreet)?;
277
278 if write {
280 write_to_env_file::<N>(network, priv_key, ctx, endpoint)?;
281 }
282
283 Ok(())
284}
285
286fn print_keys<N: Network>(private_key: PrivateKey<N>, discreet: bool) -> Result<()> {
288 let view_key = ViewKey::try_from(&private_key)?;
289 let address = Address::<N>::try_from(&view_key)?;
290
291 if !discreet {
292 println!(
293 "\n {:>12} {private_key}\n {:>12} {view_key}\n {:>12} {address}\n",
294 "Private Key".cyan().bold(),
295 "View Key".cyan().bold(),
296 "Address".cyan().bold(),
297 );
298 return Ok(());
299 }
300 display_string_discreetly(
301 &private_key.to_string(),
302 "### Do not share or lose this private key! Press any key to complete. ###",
303 )?;
304 println!("\n {:>12} {view_key}\n {:>12} {address}\n", "View Key".cyan().bold(), "Address".cyan().bold(),);
305
306 Ok(())
307}
308
309pub(crate) fn sign_message<N: Network>(
311 message: String,
312 raw: bool,
313 private_key: Option<String>,
314 private_key_file: Option<String>,
315) -> Result<String> {
316 let private_key = match (private_key, private_key_file) {
317 (Some(private_key), None) => PrivateKey::<N>::from_str(private_key.trim())
318 .map_err(|e| CliError::cli_invalid_input(format!("could not parse private key: {e}")))?,
319 (None, Some(private_key_file)) => {
320 let path = private_key_file
321 .parse::<PathBuf>()
322 .map_err(|e| CliError::cli_invalid_input(format!("invalid path - {e}")))?;
323 let key_str = std::fs::read_to_string(path).map_err(UtilError::failed_to_read_file)?;
324 PrivateKey::<N>::from_str(key_str.trim())
325 .map_err(|e| CliError::cli_invalid_input(format!("could not parse private key: {e}")))?
326 }
327 (None, None) => {
328 match dotenvy::var("PRIVATE_KEY") {
330 Ok(key) => PrivateKey::<N>::from_str(key.trim())
331 .map_err(|e| CliError::cli_invalid_input(format!("could not parse private key: {e}")))?,
332 Err(_) => Err(CliError::cli_invalid_input(
333 "missing the '--private-key', '--private-key-file', PRIVATE_KEY env, or .env",
334 ))?,
335 }
336 }
337 (Some(_), Some(_)) => {
338 Err(CliError::cli_invalid_input("cannot specify both the '--private-key' and '--private-key-file' flags"))?
339 }
340 };
341 let mut rng = ChaChaRng::from_entropy();
343
344 let signature = if raw {
346 private_key.sign_bytes(message.as_bytes(), &mut rng)
347 } else {
348 let fields = Value::<N>::from_str(&message)?
349 .to_fields()
350 .map_err(|_| CliError::cli_invalid_input("Failed to parse a valid Aleo value"))?;
351 private_key.sign(&fields, &mut rng)
352 }
353 .map_err(|_| CliError::cli_runtime_error("Failed to sign the message"))?
354 .to_string();
355 Ok(signature)
357}
358
359pub(crate) fn verify_message<N: Network>(
361 address: String,
362 signature: String,
363 message: String,
364 raw: bool,
365) -> Result<String> {
366 let address = Address::<N>::from_str(&address)?;
368
369 let signature = Signature::<N>::from_str(&signature)
370 .map_err(|e| CliError::cli_invalid_input(format!("Failed to parse a valid signature: {e}")))?;
371
372 let verified = if raw {
374 signature.verify_bytes(&address, message.as_bytes())
375 } else {
376 let fields = Value::<N>::from_str(&message)?
377 .to_fields()
378 .map_err(|_| CliError::cli_invalid_input("Failed to parse a valid Aleo value"))?;
379 signature.verify(&address, &fields)
380 };
381
382 match verified {
384 true => Ok("✅ The signature is valid".to_string()),
385 false => Err(CliError::cli_runtime_error("❌ The signature is invalid"))?,
386 }
387}
388
389fn write_to_env_file<N: Network>(
391 network: NetworkName,
392 private_key: PrivateKey<N>,
393 ctx: &Context,
394 endpoint: String,
395) -> Result<()> {
396 let program_dir = ctx.dir()?;
397 let env = Env { network, private_key: private_key.to_string(), endpoint };
398 let env_path = program_dir.join(leo_package::ENV_FILENAME);
399 env.write_to_file(env_path)?;
400 tracing::info!("✅ Private Key written to {}", program_dir.join(".env").display());
401 Ok(())
402}
403
404fn display_string_discreetly(discreet_string: &str, continue_message: &str) -> Result<()> {
406 use crossterm::{
407 style::Print,
408 terminal::{EnterAlternateScreen, LeaveAlternateScreen},
409 };
410 let mut stdout = io::stdout();
411 stdout.execute(EnterAlternateScreen).unwrap();
412 stdout.execute(Print(format!("{discreet_string}\n{continue_message}"))).unwrap();
414 stdout.flush().unwrap();
415 wait_for_keypress();
416 stdout.execute(LeaveAlternateScreen).unwrap();
417 Ok(())
418}
419
420fn wait_for_keypress() {
421 let mut single_key = [0u8];
422 std::io::stdin().read_exact(&mut single_key).unwrap();
423}
424
425#[cfg(test)]
426mod tests {
427 use super::{sign_message, verify_message};
428
429 type CurrentNetwork = snarkvm::prelude::MainnetV0;
430
431 #[test]
432 fn test_signature_raw() {
433 let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
434 let message = "Hello, world!".to_string();
435 assert!(sign_message::<CurrentNetwork>(message, true, Some(key), None).is_ok());
436 }
437
438 #[test]
439 fn test_signature() {
440 let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
441 let message = "5field".to_string();
442 assert!(sign_message::<CurrentNetwork>(message, false, Some(key), None).is_ok());
443 }
444
445 #[test]
446 fn test_signature_fail() {
447 let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
448 let message = "not a literal value".to_string();
449 assert!(sign_message::<CurrentNetwork>(message, false, Some(key), None).is_err());
450 }
451
452 #[test]
453 fn test_verify_raw() {
454 let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j".to_string();
456 let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
457 let message = "Hello, world!".to_string();
458 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, true).is_ok());
459
460 let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
462 let message = "Different Message".to_string();
463 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, true).is_err());
464
465 let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
467 let message = "Hello, world!".to_string();
468 let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
469 assert!(verify_message::<CurrentNetwork>(wrong_address, signature, message, true).is_err());
470
471 let signature = "sign1424ztyt9hcm77nq450gvdszrvtg9kvhc4qadg4nzy9y0ah7wdqq7t36cxal42p9jj8e8pjpmc06lfev9nvffcpqv0cxwyr0a2j2tjqlesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk3yrr50".to_string();
473 let message = "Different Message".to_string();
474 assert!(verify_message::<CurrentNetwork>(address, signature, message, true).is_ok());
475 }
476
477 #[test]
478 fn test_verify() {
479 let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j".to_string();
481 let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
482 let message = "5field".to_string();
483 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, false).is_ok());
484
485 let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
487 let message = "10field".to_string();
488 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, false).is_err());
489
490 let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
492 let message = "5field".to_string();
493 let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
494 assert!(verify_message::<CurrentNetwork>(wrong_address, signature, message, false).is_err());
495
496 let signature = "sign1t9v2t5tljk8pr5t6vkcqgkus0a3v69vryxmfrtwrwg0xtj7yv5qj2nz59e5zcyl50w23lhntxvt6vzeqfyu6dt56698zvfj2l6lz6q0esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk8rh9kt".to_string();
498 let message = "10field".to_string();
499 assert!(verify_message::<CurrentNetwork>(address, signature, message, false).is_ok());
500 }
501}