leo_lang/cli/commands/
account.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use 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/// Commands to manage Aleo accounts.
38#[derive(Parser, Debug)]
39pub enum Account {
40    /// Generates a new Aleo account
41    New {
42        /// Seed the RNG with a numeric value.
43        #[clap(short = 's', long)]
44        seed: Option<u64>,
45        /// Write the private key to the .env file.
46        #[clap(short = 'w', long)]
47        write: bool,
48        /// Print sensitive information (such as private key) discreetly to an alternate screen
49        #[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    /// Derive an Aleo account from a private key.
62    Import {
63        /// Private key plaintext
64        private_key: Option<String>,
65        /// Write the private key to the .env file.
66        #[clap(short = 'w', long)]
67        write: bool,
68        /// Print sensitive information (such as private key) discreetly to an alternate screen
69        #[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 a message using your Aleo private key.
82    Sign {
83        /// Specify the account private key of the node
84        #[clap(long = "private-key")]
85        private_key: Option<String>,
86        /// Specify the path to a file containing the account private key of the node
87        #[clap(long = "private-key-file")]
88        private_key_file: Option<String>,
89        /// Message (Aleo value) to sign
90        #[clap(short = 'm', long)]
91        message: String,
92        /// When enabled, parses the message as bytes instead of Aleo literals
93        #[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 a message from an Aleo address.
99    Verify {
100        /// Address to use for verification
101        #[clap(short = 'a', long)]
102        address: String,
103        /// Signature to verify
104        #[clap(short = 's', long)]
105        signature: String,
106        /// Message (Aleo value) to verify the signature against
107        #[clap(short = 'm', long)]
108        message: String,
109        /// When enabled, parses the message as bytes instead of Aleo literals
110        #[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                // Parse the network.
135                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                // Parse the network.
156                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                // Parse the network.
177                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                // Parse the network.
197                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
220// Helper functions
221
222// Generate a new account.
223fn 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    // Sample a new Aleo account.
232    let private_key = match seed {
233        // Recover the field element deterministically.
234        Some(seed) => PrivateKey::<N>::new(&mut ChaChaRng::seed_from_u64(seed)),
235        // Sample a random field element.
236        None => PrivateKey::new(&mut ChaChaRng::from_entropy()),
237    }
238    .map_err(CliError::failed_to_parse_seed)?;
239
240    // Derive the view key and address and print to stdout.
241    print_keys(private_key, discreet)?;
242
243    // Save key data to .env file.
244    if write {
245        write_to_env_file(network, private_key, ctx, endpoint)?;
246    }
247    Ok(())
248}
249
250// Import an account.
251fn 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    // Derive the view key and address and print to stdout.
276    print_keys::<N>(priv_key, discreet)?;
277
278    // Save key data to .env file.
279    if write {
280        write_to_env_file::<N>(network, priv_key, ctx, endpoint)?;
281    }
282
283    Ok(())
284}
285
286// Print keys as a formatted string without log level.
287fn 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
309// Sign a message with an Aleo private key
310pub(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            // Attempt to pull private key from env, then .env file
329            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    // Sample a random field element.
342    let mut rng = ChaChaRng::from_entropy();
343
344    // Sign the message
345    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    // Return the signature as a string
356    Ok(signature)
357}
358
359// Verify a signature with an Aleo address
360pub(crate) fn verify_message<N: Network>(
361    address: String,
362    signature: String,
363    message: String,
364    raw: bool,
365) -> Result<String> {
366    // Parse the address.
367    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    // Verify the signature
373    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    // Return the verification result
383    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
389// Write the network and private key to the .env file in project directory.
390fn 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
404/// Print the string to an alternate screen, so that the string won't been printed to the terminal.
405fn 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    // print msg on the alternate screen
413    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        // test signature of "Hello, world!"
455        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        // test signature of "Hello, world!" against the message "Different Message"
461        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        // test signature of "Hello, world!" against the wrong address
466        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        // test a valid signature of "Different Message"
472        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        // test signature of 5u8
480        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        // test signature of 5u8 against the message 10u8
486        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        // test signature of 5u8 against the wrong address
491        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        // test a valid signature of 10u8
497        let signature = "sign1t9v2t5tljk8pr5t6vkcqgkus0a3v69vryxmfrtwrwg0xtj7yv5qj2nz59e5zcyl50w23lhntxvt6vzeqfyu6dt56698zvfj2l6lz6q0esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk8rh9kt".to_string();
498        let message = "10field".to_string();
499        assert!(verify_message::<CurrentNetwork>(address, signature, message, false).is_ok());
500    }
501}