1use 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#[derive(Debug, Parser)]
27pub struct Execute {
28 program_id: ProgramID<CurrentNetwork>,
30 function: Identifier<CurrentNetwork>,
32 inputs: Vec<Value<CurrentNetwork>>,
34 #[clap(long)]
37 estimate_fee: bool,
38 #[clap(long)]
39 private_fee: bool,
41 #[clap(short, long)]
43 endpoint: Option<String>,
44 #[clap(long)]
46 fee: Option<f64>,
47 #[clap(short, long)]
49 record: Option<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>,
50 #[clap(short='k', long, conflicts_with_all = &["ciphertext", "password"])]
52 private_key: Option<PrivateKey<CurrentNetwork>>,
53 #[clap(short, long, conflicts_with = "private_key", requires = "password")]
55 ciphertext: Option<Ciphertext<CurrentNetwork>>,
56 #[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 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 let program_string = self.program_id.to_string();
77 let function_string = self.function.to_string();
78
79 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 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 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 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 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 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 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 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 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 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 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 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}