aleo/commands/
execute.rs

1// Copyright (C) 2019-2023 Aleo Systems Inc.
2// This file is part of the Aleo SDK library.
3
4// The Aleo SDK 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 Aleo SDK 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 Aleo SDK library. If not, see <https://www.gnu.org/licenses/>.
16
17use crate::{Aleo, CurrentNetwork};
18use aleo_rust::{AleoAPIClient, Encryptor, ProgramManager, RecordFinder};
19use snarkvm::prelude::{Ciphertext, Identifier, Plaintext, PrivateKey, ProgramID, Record, Value};
20
21use anyhow::{anyhow, ensure, Result};
22use clap::Parser;
23use colored::Colorize;
24
25/// Executes an Aleo program function
26#[derive(Debug, Parser)]
27pub struct Execute {
28    /// The program identifier
29    program_id: ProgramID<CurrentNetwork>,
30    /// The function name
31    function: Identifier<CurrentNetwork>,
32    /// The function inputs
33    inputs: Vec<Value<CurrentNetwork>>,
34    /// Estimate the execution fee in credits. If set, this will estimate the fee for executing the
35    /// program but will NOT execute the program
36    #[clap(long)]
37    estimate_fee: bool,
38    #[clap(long)]
39    /// Use private fee
40    private_fee: bool,
41    /// Aleo Network peer to broadcast the transaction to
42    #[clap(short, long)]
43    endpoint: Option<String>,
44    /// Execution fee in credits
45    #[clap(long)]
46    fee: Option<f64>,
47    /// The record to spend the fee from
48    #[clap(short, long)]
49    record: Option<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>,
50    /// Private key used to generate the execution
51    #[clap(short='k', long, conflicts_with_all = &["ciphertext", "password"])]
52    private_key: Option<PrivateKey<CurrentNetwork>>,
53    /// Private key ciphertext used to generate the execution (requires password to decrypt)
54    #[clap(short, long, conflicts_with = "private_key", requires = "password")]
55    ciphertext: Option<Ciphertext<CurrentNetwork>>,
56    /// Password to decrypt the private key
57    #[clap(short, long, conflicts_with = "private_key", requires = "ciphertext")]
58    password: Option<String>,
59}
60
61impl Execute {
62    pub fn parse(self) -> Result<String> {
63        if self.estimate_fee {
64            println!(
65                "Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network"
66            );
67        }
68
69        // Check for config errors
70        ensure!(
71            !(self.private_key.is_none() && self.ciphertext.is_none()),
72            "Private key or private key ciphertext required to execute a function"
73        );
74
75        // Get strings for the program and function for logging
76        let program_string = self.program_id.to_string();
77        let function_string = self.function.to_string();
78
79        // Get fee in credits, and microcredits
80        let fee_microcredits = if !self.estimate_fee {
81            ensure!(self.fee.is_some(), "Fee must be specified when executing a program");
82            let fee = self.fee.unwrap();
83            ensure!(fee > 0.0, "Execution fee must be greater than 0");
84            println!(
85                "{}",
86                format!(
87                    "Attempting to execute function '{}:{}' with a fee of {} credits",
88                    &program_string, &function_string, fee
89                )
90                .bright_blue()
91            );
92            (fee * 1000000.0) as u64
93        } else {
94            println!(
95                "{}",
96                format!("Attempting to estimate the fee for '{}:{}'", &program_string, &function_string).bright_blue()
97            );
98            0u64
99        };
100
101        // Setup the API client to use configured peer or default to https://api.explorer.aleo.org/v1/testnet3
102        let api_client = self
103            .endpoint
104            .clone()
105            .map_or_else(
106                || {
107                    println!("Using default peer: https://api.explorer.aleo.org/v1/testnet3");
108                    Ok(AleoAPIClient::<CurrentNetwork>::testnet3())
109                },
110                |peer| AleoAPIClient::<CurrentNetwork>::new(&peer, "testnet3"),
111            )
112            .map_err(|e| anyhow!("{:?}", e))?;
113
114        // Create the program manager and find the program
115        println!("Attempting to find program: {}", program_string.bright_blue());
116        let mut program_manager = ProgramManager::<CurrentNetwork>::new(
117            self.private_key,
118            self.ciphertext.clone(),
119            Some(api_client.clone()),
120            None,
121            false,
122        )?;
123        let program = program_manager.find_program(&self.program_id)?;
124
125        if self.estimate_fee {
126            let (total, (storage, finalize)) =
127                program_manager.estimate_execution_fee::<Aleo>(&program, self.function, self.inputs.iter())?;
128            let (total, storage, finalize) =
129                ((total as f64) / 1_000_000.0, (storage as f64) / 1_000_000.0, (finalize as f64) / 1_000_000.0);
130            let function_id = &self.function;
131            let program_id = program.id();
132            println!(
133                "\n{} {} {} {} {} {} {} {} {}",
134                "Function".bright_green(),
135                format!("{program_id}:{function_id:?}").bright_blue(),
136                "has a storage fee of".bright_green(),
137                format!("{storage}").bright_blue(),
138                "credits and a finalize fee of".bright_green(),
139                format!("{finalize}").bright_blue(),
140                "credits for a total execution fee of".bright_green(),
141                format!("{total}").bright_blue(),
142                "credits".bright_green()
143            );
144            return Ok("".to_string());
145        }
146
147        // Find a fee record to pay the fee if necessary
148        let fee_record = if self.record.is_none() {
149            println!("Searching for a record to spend the execution fee from, this may take a while..");
150            let private_key = if let Some(private_key) = self.private_key {
151                private_key
152            } else {
153                let ciphertext = self.ciphertext.as_ref().unwrap();
154                Encryptor::decrypt_private_key_with_secret(ciphertext, self.password.as_ref().unwrap())?
155            };
156            let record_finder = RecordFinder::new(api_client);
157            if self.private_fee {
158                Some(record_finder.find_one_record(&private_key, fee_microcredits, None)?)
159            } else {
160                None
161            }
162        } else {
163            self.record
164        };
165
166        // Execute the program function
167        println!("Executing '{}:{}'", program_string.bright_blue(), function_string.bright_blue());
168        let result = program_manager.execute_program(
169            self.program_id,
170            self.function,
171            self.inputs.iter(),
172            fee_microcredits,
173            fee_record,
174            self.password.as_deref(),
175            None,
176        );
177
178        // Inform the user of the result of the program execution
179        if result.is_err() {
180            println!(
181                "Execution of function '{}:{}' failed with error:",
182                program_string.red().bold(),
183                function_string.red().bold()
184            );
185        } else {
186            println!(
187                "Execution of function {} from {} successful!",
188                function_string.green().bold(),
189                program_string.green().bold()
190            );
191            println!("Transaction ID:");
192        }
193        result
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use snarkvm::prelude::TestRng;
201
202    #[test]
203    fn test_execution_config_errors() {
204        // Generate key material
205        let recipient_private_key = PrivateKey::<CurrentNetwork>::new(&mut TestRng::default()).unwrap();
206        let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap());
207
208        // Assert execute fails without a private key or private key ciphertext
209        let execute_missing_key_material =
210            Execute::try_parse_from(["aleo", "hello.aleo", "hello", "1337u32", "42u32", "--fee", "0.7"]);
211
212        assert!(execute_missing_key_material.unwrap().parse().is_err());
213
214        // Assert execute fails if both a private key and ciphertext are provided
215        let execute_conflicting_inputs = Execute::try_parse_from([
216            "aleo",
217            "hello.aleo",
218            "hello",
219            "1337u32",
220            "42u32",
221            "-k",
222            &recipient_private_key.to_string(),
223            "--fee",
224            "0.7",
225            "--ciphertext",
226            &ciphertext.as_ref().unwrap().to_string(),
227            "--password",
228            "password",
229        ]);
230
231        assert_eq!(execute_conflicting_inputs.unwrap_err().kind(), clap::error::ErrorKind::ArgumentConflict);
232
233        // Assert execute fails if a ciphertext is provided without a password
234        let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap());
235        let execute_no_password = Execute::try_parse_from([
236            "aleo",
237            "hello.aleo",
238            "hello",
239            "1337u32",
240            "42u32",
241            "--fee",
242            "0.7",
243            "--ciphertext",
244            &ciphertext.as_ref().unwrap().to_string(),
245        ]);
246
247        assert_eq!(execute_no_password.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument);
248
249        // Assert execute fails if only a password is provided
250        let execute_password_only =
251            Execute::try_parse_from(["aleo", "hello.aleo", "hello", "1337u32", "42u32", "--password", "password"]);
252
253        assert_eq!(execute_password_only.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument);
254
255        // Assert execute fails if invalid peer is specified
256        let execute_bad_peer = Execute::try_parse_from([
257            "aleo",
258            "hello.aleo",
259            "hello",
260            "1337u32",
261            "42u32",
262            "-k",
263            &recipient_private_key.to_string(),
264            "--fee",
265            "0.7",
266            "-e",
267            "localhost:3033",
268        ]);
269
270        assert!(execute_bad_peer.unwrap().parse().is_err());
271    }
272}