// Copyright (C) 2019-2023 Aleo Systems Inc. // This file is part of the Aleo SDK library. // The Aleo SDK library is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // The Aleo SDK library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see . use crate::{Aleo, CurrentNetwork}; use aleo_rust::{AleoAPIClient, Encryptor, ProgramManager, RecordFinder}; use snarkvm::prelude::{Ciphertext, Identifier, Plaintext, PrivateKey, ProgramID, Record, Value}; use anyhow::{anyhow, ensure, Result}; use clap::Parser; use colored::Colorize; /// Executes an Aleo program function #[derive(Debug, Parser)] pub struct Execute { /// The program identifier program_id: ProgramID, /// The function name function: Identifier, /// The function inputs inputs: Vec>, /// Estimate the execution fee in credits. If set, this will estimate the fee for executing the /// program but will NOT execute the program #[clap(long)] estimate_fee: bool, #[clap(long)] /// Use private fee private_fee: bool, /// Aleo Network peer to broadcast the transaction to #[clap(short, long)] endpoint: Option, /// Execution fee in credits #[clap(long)] fee: Option, /// The record to spend the fee from #[clap(short, long)] record: Option>>, /// Private key used to generate the execution #[clap(short='k', long, conflicts_with_all = &["ciphertext", "password"])] private_key: Option>, /// Private key ciphertext used to generate the execution (requires password to decrypt) #[clap(short, long, conflicts_with = "private_key", requires = "password")] ciphertext: Option>, /// Password to decrypt the private key #[clap(short, long, conflicts_with = "private_key", requires = "ciphertext")] password: Option, } impl Execute { pub fn parse(self) -> Result { if self.estimate_fee { println!( "Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network" ); } // Check for config errors ensure!( !(self.private_key.is_none() && self.ciphertext.is_none()), "Private key or private key ciphertext required to execute a function" ); // Get strings for the program and function for logging let program_string = self.program_id.to_string(); let function_string = self.function.to_string(); // Get fee in credits, and microcredits let fee_microcredits = if !self.estimate_fee { ensure!(self.fee.is_some(), "Fee must be specified when executing a program"); let fee = self.fee.unwrap(); ensure!(fee > 0.0, "Execution fee must be greater than 0"); println!( "{}", format!( "Attempting to execute function '{}:{}' with a fee of {} credits", &program_string, &function_string, fee ) .bright_blue() ); (fee * 1000000.0) as u64 } else { println!( "{}", format!("Attempting to estimate the fee for '{}:{}'", &program_string, &function_string).bright_blue() ); 0u64 }; // Setup the API client to use configured peer or default to https://api.explorer.aleo.org/v1/testnet3 let api_client = self .endpoint .clone() .map_or_else( || { println!("Using default peer: https://api.explorer.aleo.org/v1/testnet3"); Ok(AleoAPIClient::::testnet3()) }, |peer| AleoAPIClient::::new(&peer, "testnet3"), ) .map_err(|e| anyhow!("{:?}", e))?; // Create the program manager and find the program println!("Attempting to find program: {}", program_string.bright_blue()); let mut program_manager = ProgramManager::::new( self.private_key, self.ciphertext.clone(), Some(api_client.clone()), None, false, )?; let program = program_manager.find_program(&self.program_id)?; if self.estimate_fee { let (total, (storage, finalize)) = program_manager.estimate_execution_fee::(&program, self.function, self.inputs.iter())?; let (total, storage, finalize) = ((total as f64) / 1_000_000.0, (storage as f64) / 1_000_000.0, (finalize as f64) / 1_000_000.0); let function_id = &self.function; let program_id = program.id(); println!( "\n{} {} {} {} {} {} {} {} {}", "Function".bright_green(), format!("{program_id}:{function_id:?}").bright_blue(), "has a storage fee of".bright_green(), format!("{storage}").bright_blue(), "credits and a finalize fee of".bright_green(), format!("{finalize}").bright_blue(), "credits for a total execution fee of".bright_green(), format!("{total}").bright_blue(), "credits".bright_green() ); return Ok("".to_string()); } // Find a fee record to pay the fee if necessary let fee_record = if self.record.is_none() { println!("Searching for a record to spend the execution fee from, this may take a while.."); let private_key = if let Some(private_key) = self.private_key { private_key } else { let ciphertext = self.ciphertext.as_ref().unwrap(); Encryptor::decrypt_private_key_with_secret(ciphertext, self.password.as_ref().unwrap())? }; let record_finder = RecordFinder::new(api_client); if self.private_fee { Some(record_finder.find_one_record(&private_key, fee_microcredits, None)?) } else { None } } else { self.record }; // Execute the program function println!("Executing '{}:{}'", program_string.bright_blue(), function_string.bright_blue()); let result = program_manager.execute_program( self.program_id, self.function, self.inputs.iter(), fee_microcredits, fee_record, self.password.as_deref(), None, ); // Inform the user of the result of the program execution if result.is_err() { println!( "Execution of function '{}:{}' failed with error:", program_string.red().bold(), function_string.red().bold() ); } else { println!( "Execution of function {} from {} successful!", function_string.green().bold(), program_string.green().bold() ); println!("Transaction ID:"); } result } } #[cfg(test)] mod tests { use super::*; use snarkvm::prelude::TestRng; #[test] fn test_execution_config_errors() { // Generate key material let recipient_private_key = PrivateKey::::new(&mut TestRng::default()).unwrap(); let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap()); // Assert execute fails without a private key or private key ciphertext let execute_missing_key_material = Execute::try_parse_from(["aleo", "hello.aleo", "hello", "1337u32", "42u32", "--fee", "0.7"]); assert!(execute_missing_key_material.unwrap().parse().is_err()); // Assert execute fails if both a private key and ciphertext are provided let execute_conflicting_inputs = Execute::try_parse_from([ "aleo", "hello.aleo", "hello", "1337u32", "42u32", "-k", &recipient_private_key.to_string(), "--fee", "0.7", "--ciphertext", &ciphertext.as_ref().unwrap().to_string(), "--password", "password", ]); assert_eq!(execute_conflicting_inputs.unwrap_err().kind(), clap::error::ErrorKind::ArgumentConflict); // Assert execute fails if a ciphertext is provided without a password let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap()); let execute_no_password = Execute::try_parse_from([ "aleo", "hello.aleo", "hello", "1337u32", "42u32", "--fee", "0.7", "--ciphertext", &ciphertext.as_ref().unwrap().to_string(), ]); assert_eq!(execute_no_password.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument); // Assert execute fails if only a password is provided let execute_password_only = Execute::try_parse_from(["aleo", "hello.aleo", "hello", "1337u32", "42u32", "--password", "password"]); assert_eq!(execute_password_only.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument); // Assert execute fails if invalid peer is specified let execute_bad_peer = Execute::try_parse_from([ "aleo", "hello.aleo", "hello", "1337u32", "42u32", "-k", &recipient_private_key.to_string(), "--fee", "0.7", "-e", "localhost:3033", ]); assert!(execute_bad_peer.unwrap().parse().is_err()); } }