Skip to main content

snarkos_cli/commands/
account.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::helpers::args::{network_id_parser, parse_private_key};
17
18mod sign;
19use sign::Sign;
20
21use snarkvm::console::{
22    account::{Address, PrivateKey, Signature},
23    network::{CanaryV0, MainnetV0, Network, TestnetV0},
24    prelude::{Environment, Uniform},
25    program::{ToFields, Value},
26    types::Field,
27};
28
29use anyhow::{Result, anyhow, bail};
30use clap::Parser;
31use colored::Colorize;
32use core::str::FromStr;
33use crossterm::ExecutableCommand;
34use rand::SeedableRng;
35use rand_chacha::ChaChaRng;
36use rayon::prelude::*;
37use std::{
38    fs::File,
39    io::{Read, Write},
40};
41
42use zeroize::Zeroize;
43
44/// Commands to manage Aleo accounts.
45#[derive(Debug, Parser, Zeroize)]
46#[command(
47    // Use kebab-case for all arguments (e.g., use the `private-key` flag for the `private_key` field).
48    // This is already the default, but we specify it in case clap's default changes in the future.
49    rename_all = "kebab-case",
50)]
51pub enum Account {
52    /// Generates a new Aleo account
53    New {
54        /// Specify the network to create an execution for.
55        /// [options: 0 = mainnet, 1 = testnet, 2 = canary]
56        #[clap(long, default_value_t=MainnetV0::ID, long, value_parser = network_id_parser())]
57        network: u16,
58        /// Seed the RNG with a numeric value
59        #[clap(short = 's', long)]
60        seed: Option<String>,
61        /// Try until an address with the vanity string is found
62        #[clap(short = 'v', long, conflicts_with_all = &["seed", "save_to_file"])]
63        vanity: Option<String>,
64        /// Print sensitive information (such as the private key) discreetly in an alternate screen
65        #[clap(long)]
66        discreet: bool,
67        /// Specify the path to a file where to save the account in addition to printing it
68        #[clap(long, conflicts_with = "discreet")]
69        save_to_file: Option<String>,
70    },
71    /// Derive an Aleo account from a private key
72    Import {
73        /// Account private key
74        private_key: Option<String>,
75        /// Specify the network to create an execution for.
76        /// [options: 0 = mainnet, 1 = testnet, 2 = canary]
77        #[clap(long, default_value_t=MainnetV0::ID, long, value_parser = network_id_parser())]
78        network: u16,
79        /// Print sensitive information (such as the private key) discreetly in an alternate screen
80        #[clap(long)]
81        discreet: bool,
82        /// Specify the path to a file where to save the account in addition to printing it
83        #[clap(long, conflicts_with = "discreet")]
84        save_to_file: Option<String>,
85    },
86    Sign(Sign),
87    Verify {
88        /// Specify the network to create an execution for.
89        /// [options: 0 = mainnet, 1 = testnet, 2 = canary]
90        #[clap(long, default_value_t=MainnetV0::ID, long, value_parser = network_id_parser())]
91        network: u16,
92        /// Address to use for verification
93        #[clap(short = 'a', long)]
94        address: String,
95        /// Signature to verify
96        #[clap(short = 's', long)]
97        signature: String,
98        /// Message (Aleo value) to verify the signature against
99        #[clap(short = 'm', long)]
100        message: String,
101        /// When enabled, parses the message as bytes instead of Aleo literals
102        #[clap(short = 'r', long)]
103        raw: bool,
104    },
105}
106
107/// Parse a raw Aleo input into fields
108fn aleo_literal_to_fields<N: Network>(input: &str) -> Result<Vec<Field<N>>> {
109    Value::<N>::from_str(input)?.to_fields()
110}
111
112impl Account {
113    /// Run a account command
114    pub fn parse(self) -> Result<String> {
115        match self {
116            Self::New { network, seed, vanity, discreet, save_to_file } => {
117                match vanity {
118                    // Generate a vanity account for the specified network.
119                    Some(vanity) => match network {
120                        MainnetV0::ID => Self::new_vanity::<MainnetV0>(vanity.as_str(), discreet),
121                        TestnetV0::ID => Self::new_vanity::<TestnetV0>(vanity.as_str(), discreet),
122                        CanaryV0::ID => Self::new_vanity::<CanaryV0>(vanity.as_str(), discreet),
123                        unknown_id => bail!("Unknown network ID ({unknown_id})"),
124                    },
125                    // Generate a seeded account for the specified network.
126                    None => match network {
127                        MainnetV0::ID => Self::new_seeded::<MainnetV0>(seed, discreet, save_to_file),
128                        TestnetV0::ID => Self::new_seeded::<TestnetV0>(seed, discreet, save_to_file),
129                        CanaryV0::ID => Self::new_seeded::<CanaryV0>(seed, discreet, save_to_file),
130                        unknown_id => bail!("Unknown network ID ({unknown_id})"),
131                    },
132                }
133            }
134            Self::Import { private_key, network, discreet, save_to_file } => {
135                // Import the account for the specified network.
136                match network {
137                    MainnetV0::ID => Self::import::<MainnetV0>(private_key, discreet, save_to_file),
138                    TestnetV0::ID => Self::import::<TestnetV0>(private_key, discreet, save_to_file),
139                    CanaryV0::ID => Self::import::<CanaryV0>(private_key, discreet, save_to_file),
140                    unknown_id => bail!("Unknown network ID ({unknown_id})"),
141                }
142            }
143            Self::Sign(sign) => sign.execute(),
144            Self::Verify { network, address, signature, message, raw } => {
145                // Verify the signature for the specified network.
146                match network {
147                    MainnetV0::ID => Self::verify::<MainnetV0>(address, signature, message, raw),
148                    TestnetV0::ID => Self::verify::<TestnetV0>(address, signature, message, raw),
149                    CanaryV0::ID => Self::verify::<CanaryV0>(address, signature, message, raw),
150                    unknown_id => bail!("Unknown network ID ({unknown_id})"),
151                }
152            }
153        }
154    }
155
156    /// Generates a new Aleo account with the given vanity string.
157    fn new_vanity<N: Network>(vanity: &str, discreet: bool) -> Result<String> {
158        // A closure to generate a new Aleo account.
159        let sample_account = || snarkos_account::Account::<N>::new(&mut rand::thread_rng());
160
161        const ITERATIONS: u128 = u16::MAX as u128;
162        const ITERATIONS_STR: &str = "65,535";
163
164        // Ensure the vanity string is valid.
165        if !crate::helpers::is_in_bech32m_charset(vanity) {
166            bail!(
167                "The vanity string '{vanity}' contains invalid bech32m characters. Try using characters from the bech32m character set: {}",
168                crate::helpers::BECH32M_CHARSET
169            );
170        }
171
172        // Output a message if the character set is more than 4 characters.
173        if vanity.len() > 4 {
174            let message =
175                format!(" The vanity string '{vanity}' contains 5 or more characters and will take a while to find.\n");
176            println!("{}", message.yellow());
177        }
178
179        loop {
180            // Initialize a timer.
181            let timer = std::time::Instant::now();
182
183            // Generates bech32m addresses in parallel until one is found that
184            // includes the desired vanity string at the start or end of the address.
185            let account = (0..ITERATIONS).into_par_iter().find_map_any(|_| {
186                // Initialize the result.
187                let mut account = None;
188                // Sample a random account.
189                if let Ok(candidate) = sample_account() {
190                    // Encode the address as a bech32m string.
191                    let address = candidate.address().to_string();
192                    // Set the candidate if the address includes the desired vanity string
193                    // at the start or end of the address.
194                    if crate::helpers::has_vanity_string(&address, vanity) {
195                        account = Some(candidate);
196                    }
197                }
198                // Return the result.
199                account
200            });
201
202            // Return the result if a candidate was found.
203            if let Some(account) = account {
204                println!(); // Add a newline for formatting.
205                if !discreet {
206                    return Ok(account.to_string());
207                }
208                display_string_discreetly(
209                    &format!("{:>12}  {}", "Private Key".cyan().bold(), account.private_key()),
210                    "### Do not share or lose this private key! Press any key to complete. ###",
211                )
212                .unwrap();
213                let account_info = format!(
214                    " {:>12}  {}\n {:>12}  {}",
215                    "View Key".cyan().bold(),
216                    account.view_key(),
217                    "Address".cyan().bold(),
218                    account.address()
219                );
220                return Ok(account_info);
221            } else {
222                let rate = ITERATIONS / timer.elapsed().as_millis();
223                let rate = format!("[{rate} a/ms]");
224                println!(" {} Sampled {ITERATIONS_STR} accounts, searching...", rate.dimmed());
225            }
226        }
227    }
228
229    /// Generates a new Aleo account with an optional seed.
230    fn new_seeded<N: Network>(seed: Option<String>, discreet: bool, save_to_file: Option<String>) -> Result<String> {
231        // Recover the seed.
232        let seed = match seed {
233            // Recover the field element deterministically.
234            Some(seed) => {
235                Field::new(<N as Environment>::Field::from_str(&seed).map_err(|e| anyhow!("Invalid seed - {e}"))?)
236            }
237            // Sample a random field element.
238            None => Field::rand(&mut ChaChaRng::from_entropy()),
239        };
240        // Recover the private key from the seed as a field element.
241        let private_key =
242            PrivateKey::try_from(seed).map_err(|_| anyhow!("Failed to convert the seed into a valid private key"))?;
243        // Construct the account.
244        let account = snarkos_account::Account::<N>::try_from(private_key)?;
245        // Save to file in addition to printing it back to the user
246        if let Some(path) = save_to_file {
247            crate::check_parent_permissions(&path)?;
248            let mut file = File::create_new(path)?;
249            file.write_all(account.private_key().to_string().as_bytes())?;
250            crate::set_user_read_only(&file)?;
251        }
252        // Print the new Aleo account.
253        if !discreet {
254            return Ok(account.to_string());
255        }
256        display_string_discreetly(
257            &format!("{:>12}  {}", "Private Key".cyan().bold(), account.private_key()),
258            "### Do not share or lose this private key! Press any key to complete. ###",
259        )
260        .unwrap();
261        let account_info = format!(
262            " {:>12}  {}\n {:>12}  {}",
263            "View Key".cyan().bold(),
264            account.view_key(),
265            "Address".cyan().bold(),
266            account.address()
267        );
268        Ok(account_info)
269    }
270
271    /// Generates a new Aleo account from a given private key
272    fn import<N: Network>(private_key: Option<String>, discreet: bool, save_to_file: Option<String>) -> Result<String> {
273        // Read the private key.
274        let private_key = match discreet {
275            true => {
276                let private_key_input = rpassword::prompt_password("Please enter your private key: ").unwrap();
277                PrivateKey::from_str(&private_key_input)
278            }
279            false => match private_key {
280                Some(private_key) => PrivateKey::from_str(&private_key),
281                None => bail!("PRIVATE_KEY shouldn't be empty when --discreet is false"),
282            },
283        }?;
284
285        // Construct the account.
286        let account = snarkos_account::Account::<N>::try_from(private_key)?;
287        // Save to file in addition to printing it back to the user
288        if let Some(path) = save_to_file {
289            crate::check_parent_permissions(&path)?;
290            let mut file = File::create_new(path)?;
291            file.write_all(account.private_key().to_string().as_bytes())?;
292            crate::set_user_read_only(&file)?;
293        }
294        // Print the new Aleo account.
295        if !discreet {
296            return Ok(account.to_string());
297        }
298        display_string_discreetly(
299            &format!("{:>12}  {}", "Private Key".cyan().bold(), account.private_key()),
300            "### Do not share or lose this private key! Press any key to complete. ###",
301        )
302        .unwrap();
303        let account_info = format!(
304            " {:>12}  {}\n {:>12}  {}",
305            "View Key".cyan().bold(),
306            account.view_key(),
307            "Address".cyan().bold(),
308            account.address()
309        );
310        Ok(account_info)
311    }
312
313    // Verify a signature with an Aleo address
314    fn verify<N: Network>(address: String, signature: String, message: String, raw: bool) -> Result<String> {
315        // Parse the address
316        let address = Address::<N>::from_str(&address).map_err(|_| anyhow!("Failed to parse a valid address"))?;
317        // Parse the signature
318        let signature =
319            Signature::<N>::from_str(&signature).map_err(|_| anyhow!("Failed to parse a valid signature"))?;
320        // Verify the signature
321        let verified = if raw {
322            signature.verify_bytes(&address, message.as_bytes())
323        } else {
324            let fields =
325                aleo_literal_to_fields(&message).map_err(|_| anyhow!("Failed to parse a valid Aleo literal"))?;
326            signature.verify(&address, &fields)
327        };
328
329        // Return the verification result
330        match verified {
331            true => Ok("✅ The signature is valid".to_string()),
332            false => bail!("❌ The signature is invalid"),
333        }
334    }
335}
336
337// Print the string to an alternate screen, so that the string won't been printed to the terminal.
338fn display_string_discreetly(discreet_string: &str, continue_message: &str) -> Result<()> {
339    use crossterm::{
340        style::Print,
341        terminal::{EnterAlternateScreen, LeaveAlternateScreen},
342    };
343    let mut stdout = std::io::stdout();
344    stdout.execute(EnterAlternateScreen)?;
345    // print msg on the alternate screen
346    stdout.execute(Print(format!("{discreet_string}\n{continue_message}")))?;
347    stdout.flush()?;
348    wait_for_keypress();
349    stdout.execute(LeaveAlternateScreen)?;
350    Ok(())
351}
352
353fn wait_for_keypress() {
354    let mut single_key = [0u8];
355    std::io::stdin().read_exact(&mut single_key).unwrap();
356}
357
358#[cfg(test)]
359mod tests {
360    use super::{Account, Sign};
361
362    use std::{fs, fs::Permissions, io::Write};
363    use tempfile::{NamedTempFile, TempDir};
364
365    use anyhow::{Context, Result};
366    use colored::Colorize;
367
368    #[test]
369    fn test_new() -> Result<()> {
370        for _ in 0..3 {
371            let account = Account::New { network: 0, seed: None, vanity: None, discreet: false, save_to_file: None };
372            account.parse().with_context(|| "Account creation failed")?;
373        }
374
375        Ok(())
376    }
377
378    #[test]
379    fn test_new_seeded() -> Result<()> {
380        let seed = Some(1231275789u64.to_string());
381
382        let mut expected = format!(
383            " {:>12}  {}\n",
384            "Private Key".cyan().bold(),
385            "APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X"
386        );
387        expected += &format!(
388            " {:>12}  {}\n",
389            "View Key".cyan().bold(),
390            "AViewKey1pNxZHn79XVJ4D2WG5Vn2YWsAzf5wzAs3dAuQtUAmUFF7"
391        );
392        expected += &format!(
393            " {:>12}  {}",
394            "Address".cyan().bold(),
395            "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5"
396        );
397
398        let vanity = None;
399        let account = Account::New { network: 0, seed, vanity, discreet: false, save_to_file: None };
400        let actual = account.parse().with_context(|| "Command execution failed")?;
401        assert_eq!(expected, actual);
402
403        Ok(())
404    }
405
406    #[test]
407    fn test_new_seeded_with_256bits_input() -> anyhow::Result<()> {
408        let seed = Some("38868010450269069756484274649022187108349082664538872491798902858296683054657".to_string());
409
410        let mut expected = format!(
411            " {:>12}  {}\n",
412            "Private Key".cyan().bold(),
413            "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p"
414        );
415        expected += &format!(
416            " {:>12}  {}\n",
417            "View Key".cyan().bold(),
418            "AViewKey1eYEGtb78FVg38SSYyzAeXnBdnWCba5t5YxUxtkTtvNAE"
419        );
420        expected += &format!(
421            " {:>12}  {}",
422            "Address".cyan().bold(),
423            "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j"
424        );
425
426        let vanity = None;
427        let account = Account::New { network: 0, seed, vanity, discreet: false, save_to_file: None };
428
429        let actual = account.parse().with_context(|| "Command execution failed")?;
430        assert_eq!(expected, actual);
431
432        Ok(())
433    }
434
435    #[cfg(unix)]
436    #[test]
437    fn test_new_save_to_file() -> anyhow::Result<()> {
438        use std::os::unix::fs::PermissionsExt;
439
440        let dir = TempDir::new().expect("Failed to create temp folder");
441        let dir_path = dir.path();
442        fs::set_permissions(dir_path, Permissions::from_mode(0o700)).expect("Failed to set permissions");
443
444        let mut file = dir.path().to_owned();
445        file.push("my-private-key-file");
446        let file = file.display().to_string();
447
448        let seed = Some(1231275789u64.to_string());
449        let vanity = None;
450        let discreet = false;
451        let save_to_file = Some(file.clone());
452        let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
453
454        let actual = account.parse().with_context(|| "Command execution failed")?;
455
456        let expected = "APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X";
457        assert!(actual.contains(expected));
458
459        let content = fs::read_to_string(&file).expect("Failed to read private-key-file");
460        assert_eq!(expected, content);
461
462        // check the permissions - to read-only for the owner
463        let metadata = fs::metadata(file).unwrap();
464        let permissions = metadata.permissions();
465        assert_eq!(permissions.mode() & 0o777, 0o400, "File permissions are not 0o400");
466
467        Ok(())
468    }
469
470    #[cfg(unix)]
471    #[test]
472    fn test_new_prevent_save_to_file_in_non_protected_folder() {
473        use std::os::unix::fs::PermissionsExt;
474
475        let dir = TempDir::new().expect("Failed to create temp folder");
476        let dir_path = dir.path();
477        fs::set_permissions(dir_path, Permissions::from_mode(0o444)).expect("Failed to set permissions");
478
479        let mut file = dir.path().to_owned();
480        file.push("my-private-key-file");
481        let file = file.display().to_string();
482
483        let seed = None;
484        let vanity = None;
485        let discreet = false;
486        let save_to_file = Some(file);
487        let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
488
489        let res = account.parse();
490        assert!(res.is_err());
491    }
492
493    #[test]
494    fn test_new_prevent_save_to_file_in_non_existing_folder() {
495        let dir = TempDir::new().expect("Failed to create temp folder");
496
497        let mut file = dir.path().to_owned();
498        file.push("missing-folder");
499        file.push("my-private-key-file");
500        let file = file.display().to_string();
501
502        let seed = None;
503        let vanity = None;
504        let discreet = false;
505        let save_to_file = Some(file);
506        let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
507
508        let res = account.parse();
509        assert!(res.is_err());
510    }
511
512    #[test]
513    fn test_new_prevent_overwrite_existing_file() {
514        let mut file = NamedTempFile::new().expect("Failed to create temp file");
515        write!(file, "don't overwrite me").expect("Failed to write secret to file");
516
517        let seed = None;
518        let vanity = None;
519        let discreet = false;
520        let path = file.path().display().to_string();
521        let account = Account::New { network: 0, seed, vanity, discreet, save_to_file: Some(path) };
522
523        let res = account.parse();
524        assert!(res.is_err());
525
526        let expected = "don't overwrite me";
527        let content = fs::read_to_string(file).expect("Failed to read private-key-file");
528        assert_eq!(expected, content);
529    }
530
531    #[test]
532    fn test_new_disallow_save_to_file_with_discreet() {
533        let seed = None;
534        let vanity = None;
535        let discreet = true;
536        let save_to_file = Some("/tmp/not-important".to_string());
537        let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
538
539        let res = account.parse();
540        assert!(res.is_err());
541    }
542
543    #[test]
544    fn test_new_disallow_save_to_file_with_vanity() {
545        let seed = None;
546        let vanity = Some("foo".to_string());
547        let discreet = false;
548        let save_to_file = Some("/tmp/not-important".to_string());
549        let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
550
551        let res = account.parse();
552        assert!(res.is_err());
553    }
554
555    #[test]
556    fn test_import() -> Result<()> {
557        let account = Account::Import { network: 0, private_key: None, discreet: false, save_to_file: None };
558        assert!(account.parse().is_err());
559
560        let mut expected = format!(
561            " {:>12}  {}\n",
562            "Private Key".cyan().bold(),
563            "APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X"
564        );
565        expected += &format!(
566            " {:>12}  {}\n",
567            "View Key".cyan().bold(),
568            "AViewKey1pNxZHn79XVJ4D2WG5Vn2YWsAzf5wzAs3dAuQtUAmUFF7"
569        );
570        expected += &format!(
571            " {:>12}  {}",
572            "Address".cyan().bold(),
573            "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5"
574        );
575
576        let account = Account::Import {
577            network: 0,
578            private_key: Some("APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X".to_string()),
579            discreet: false,
580            save_to_file: None,
581        };
582        let actual = account.parse().with_context(|| "Command execution failed")?;
583        assert_eq!(expected, actual);
584
585        Ok(())
586    }
587
588    #[cfg(unix)]
589    #[test]
590    fn test_import_save_to_file() -> anyhow::Result<()> {
591        use std::os::unix::fs::PermissionsExt;
592
593        let dir = TempDir::new().expect("Failed to create temp folder");
594        let dir_path = dir.path();
595        fs::set_permissions(dir_path, Permissions::from_mode(0o700)).expect("Failed to set permissions");
596
597        let mut file = dir.path().to_owned();
598        file.push("my-private-key-file");
599        let file = file.display().to_string();
600
601        let discreet = false;
602        let save_to_file = Some(file.clone());
603        let account = Account::Import {
604            network: 0,
605            private_key: Some("APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X".to_string()),
606            discreet,
607            save_to_file,
608        };
609
610        let actual = account.parse().with_context(|| "Command execution failed")?;
611
612        let expected = "APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X";
613        assert!(actual.contains(expected));
614
615        let content = fs::read_to_string(&file).expect("Failed to read private-key-file");
616        assert_eq!(expected, content);
617
618        // check the permissions - to read-only for the owner
619        let metadata = fs::metadata(file).unwrap();
620        let permissions = metadata.permissions();
621        assert_eq!(permissions.mode() & 0o777, 0o400, "File permissions are not 0o400");
622
623        Ok(())
624    }
625
626    #[test]
627    fn test_signature_raw() -> Result<()> {
628        let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
629        let message = "Hello, world!".to_string();
630        let account = Account::Sign(Sign {
631            network: 0,
632            private_key: Some(key),
633            private_key_file: None,
634            message,
635            raw: true,
636            dev_key: None,
637        });
638
639        account.parse().with_context(|| "Command execution failed")?;
640        Ok(())
641    }
642
643    #[test]
644    fn test_signature_raw_using_private_key_file() -> Result<()> {
645        let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
646        let message = "Hello, world!".to_string();
647
648        let mut file = NamedTempFile::new().expect("Failed to create temp file");
649        writeln!(file, "{key}").expect("Failed to write key to temp file");
650
651        let path = file.path().display().to_string();
652        let account = Account::Sign(Sign {
653            network: 0,
654            private_key: None,
655            private_key_file: Some(path),
656            message,
657            raw: true,
658            dev_key: None,
659        });
660
661        account.parse().with_context(|| "Command execution failed")?;
662        Ok(())
663    }
664
665    #[cfg(unix)]
666    #[test]
667    fn test_signature_raw_using_private_key_file_from_account_new() -> Result<()> {
668        use std::os::unix::fs::PermissionsExt;
669
670        let message = "Hello, world!".to_string();
671
672        let dir = TempDir::new().expect("Failed to create temp folder");
673        let dir_path = dir.path();
674        fs::set_permissions(dir_path, Permissions::from_mode(0o700)).expect("Failed to set permissions");
675
676        let mut file = dir.path().to_owned();
677        file.push("my-private-key-file");
678        let file = file.display().to_string();
679
680        let seed = None;
681        let vanity = None;
682        let discreet = false;
683        let account = Account::New { network: 0, seed, vanity, discreet, save_to_file: Some(file.clone()) };
684        account.parse().with_context(|| "Command execution failed")?;
685
686        let account = Account::Sign(Sign {
687            network: 0,
688            private_key: None,
689            private_key_file: Some(file),
690            message,
691            raw: true,
692            dev_key: None,
693        });
694
695        account.parse().with_context(|| "Command execution failed")?;
696        Ok(())
697    }
698
699    #[test]
700    fn test_signature() -> Result<()> {
701        let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
702        let message = "5field".to_string();
703        let account = Account::Sign(Sign {
704            network: 0,
705            private_key: Some(key),
706            private_key_file: None,
707            message,
708            raw: false,
709            dev_key: None,
710        });
711
712        account.parse().with_context(|| "Command execution failed")?;
713        Ok(())
714    }
715
716    #[test]
717    fn test_signature_fail() {
718        let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
719        let message = "not a literal value".to_string();
720        let account = Account::Sign(Sign {
721            network: 0,
722            private_key: Some(key),
723            private_key_file: None,
724            message,
725            raw: false,
726            dev_key: None,
727        });
728        assert!(account.parse().is_err());
729    }
730
731    #[test]
732    fn test_verify_raw() -> Result<()> {
733        // test signature of "Hello, world!"
734        let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j";
735        let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
736        let message = "Hello, world!".to_string();
737        let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: true };
738
739        account.parse().with_context(|| "Command execution failed")?;
740
741        // test signature of "Hello, world!" against the message "Different Message"
742        let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
743        let message = "Different Message".to_string();
744        let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: true };
745        let actual = account.parse();
746        assert!(actual.is_err());
747
748        // test signature of "Hello, world!" against the wrong address
749        let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
750        let message = "Hello, world!".to_string();
751        let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
752        let account = Account::Verify { network: 0, address: wrong_address, signature, message, raw: true };
753        let actual = account.parse();
754        assert!(actual.is_err());
755
756        // test a valid signature of "Different Message"
757        let signature = "sign1424ztyt9hcm77nq450gvdszrvtg9kvhc4qadg4nzy9y0ah7wdqq7t36cxal42p9jj8e8pjpmc06lfev9nvffcpqv0cxwyr0a2j2tjqlesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk3yrr50".to_string();
758        let message = "Different Message".to_string();
759        let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: true };
760        account.parse().with_context(|| "Command execution failed")?;
761
762        Ok(())
763    }
764
765    #[test]
766    fn test_verify() -> Result<()> {
767        // test signature of 5u8
768        let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j";
769        let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
770        let message = "5field".to_string();
771        let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: false };
772        account.parse().with_context(|| "Command execution failed")?;
773
774        // test signature of 5u8 against the message 10u8
775        let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
776        let message = "10field".to_string();
777        let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: false };
778        let actual = account.parse();
779        assert!(actual.is_err());
780
781        // test signature of 5u8 against the wrong address
782        let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
783        let message = "5field".to_string();
784        let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
785        let account = Account::Verify { network: 0, address: wrong_address, signature, message, raw: false };
786        let actual = account.parse();
787        assert!(actual.is_err());
788
789        // test a valid signature of 10u8
790        let signature = "sign1t9v2t5tljk8pr5t6vkcqgkus0a3v69vryxmfrtwrwg0xtj7yv5qj2nz59e5zcyl50w23lhntxvt6vzeqfyu6dt56698zvfj2l6lz6q0esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk8rh9kt".to_string();
791        let message = "10field".to_string();
792        let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: false };
793
794        account.parse().with_context(|| "Command execution failed")?;
795
796        Ok(())
797    }
798}