1use super::*;
18
19use check_transaction::TransactionStatus;
20use leo_ast::NetworkName;
21use leo_package::{Package, ProgramData, fetch_program_from_network};
22
23use aleo_std::StorageMode;
24use snarkvm::prelude::{Execution, Itertools, Network, Program, execution_cost};
25
26use clap::Parser;
27use colored::*;
28use std::{convert::TryFrom, path::PathBuf};
29
30#[cfg(not(feature = "only_testnet"))]
31use snarkvm::circuit::{AleoCanaryV0, AleoV0};
32use snarkvm::{
33 circuit::{Aleo, AleoTestnetV0},
34 prelude::{
35 ConsensusVersion,
36 Identifier,
37 ProgramID,
38 VM,
39 query::Query as SnarkVMQuery,
40 store::{
41 ConsensusStore,
42 helpers::memory::{BlockMemory, ConsensusMemory},
43 },
44 },
45};
46
47#[derive(Parser, Debug)]
49pub struct LeoExecute {
50 #[clap(
51 name = "NAME",
52 help = "The name of the function to execute, e.g `helloworld.aleo/main` or `main`.",
53 default_value = "main"
54 )]
55 name: String,
56 #[clap(
57 name = "INPUTS",
58 help = "The program inputs e.g. `1u32`, `record1...` (record ciphertext), or `{ owner: ...}` "
59 )]
60 inputs: Vec<String>,
61 #[clap(flatten)]
62 pub(crate) fee_options: FeeOptions,
63 #[clap(flatten)]
64 pub(crate) action: TransactionAction,
65 #[clap(flatten)]
66 pub(crate) env_override: EnvOptions,
67 #[clap(flatten)]
68 pub(crate) extra: ExtraOptions,
69 #[clap(flatten)]
70 build_options: BuildOptions,
71}
72
73impl Command for LeoExecute {
74 type Input = Option<Package>;
75 type Output = ();
76
77 fn log_span(&self) -> Span {
78 tracing::span!(tracing::Level::INFO, "Leo")
79 }
80
81 fn prelude(&self, context: Context) -> Result<Self::Input> {
82 let path = context.dir()?;
84 let home_path = context.home()?;
86 let network = get_network(&self.env_override.network)?;
88 let endpoint = get_endpoint(&self.env_override.endpoint)?;
90 if Package::from_directory_no_graph(path, home_path, Some(network), Some(&endpoint)).is_ok() {
92 let package = LeoBuild {
93 env_override: self.env_override.clone(),
94 options: {
95 let mut options = self.build_options.clone();
96 options.no_cache = true;
97 options
98 },
99 }
100 .execute(context)?;
101 Ok(Some(package))
103 } else {
104 Ok(None)
105 }
106 }
107
108 fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
109 let network = get_network(&self.env_override.network)?;
111 match network {
113 NetworkName::TestnetV0 => handle_execute::<AleoTestnetV0>(self, context, network, input),
114 NetworkName::MainnetV0 => {
115 #[cfg(feature = "only_testnet")]
116 panic!("Mainnet chosen with only_testnet feature");
117 #[cfg(not(feature = "only_testnet"))]
118 handle_execute::<AleoV0>(self, context, network, input)
119 }
120 NetworkName::CanaryV0 => {
121 #[cfg(feature = "only_testnet")]
122 panic!("Canary chosen with only_testnet feature");
123 #[cfg(not(feature = "only_testnet"))]
124 handle_execute::<AleoCanaryV0>(self, context, network, input)
125 }
126 }
127 }
128}
129
130fn handle_execute<A: Aleo>(
132 command: LeoExecute,
133 context: Context,
134 network: NetworkName,
135 package: Option<Package>,
136) -> Result<<LeoExecute as Command>::Output> {
137 let private_key = get_private_key(&command.env_override.private_key)?;
139 let address = Address::<A::Network>::try_from(&private_key)
140 .map_err(|e| CliError::custom(format!("Failed to parse address: {e}")))?;
141
142 let endpoint = get_endpoint(&command.env_override.endpoint)?;
144
145 let is_devnet = get_is_devnet(command.env_override.devnet);
147
148 let consensus_heights =
150 command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet));
151 validate_consensus_heights(&consensus_heights)
153 .map_err(|e| CliError::custom(format!("Invalid consensus heights: {e}")))?;
154 let consensus_heights_string = consensus_heights.iter().format(",").to_string();
156 println!(
157 "\nπ’ Using the following consensus heights: {consensus_heights_string}\n To override, pass in `--consensus-heights` or override the environment variable `CONSENSUS_VERSION_HEIGHTS`.\n"
158 );
159
160 #[allow(unsafe_code)]
162 unsafe {
163 std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
170 }
171
172 let (program_name, function_name) = match command.name.split_once('/') {
175 Some((program_name, function_name)) => (program_name.to_string(), function_name.to_string()),
176 None => match &package {
177 Some(package) => (
178 format!(
179 "{}.aleo",
180 package.programs.last().expect("There must be at least one program in a Leo package").name
181 ),
182 command.name,
183 ),
184 None => {
185 return Err(CliError::custom(format!(
186 "Running `leo execute {} ...`, without an explicit program name requires that your current working directory is a valid Leo project.",
187 command.name
188 )).into());
189 }
190 },
191 };
192
193 let program_id = ProgramID::<A::Network>::from_str(&program_name)
195 .map_err(|e| CliError::custom(format!("Failed to parse program name: {e}")))?;
196 let function_id = Identifier::<A::Network>::from_str(&function_name)
198 .map_err(|e| CliError::custom(format!("Failed to parse function name: {e}")))?;
199
200 let programs = if let Some(package) = &package {
203 let build_directory = package.build_directory();
205 let imports_directory = package.imports_directory();
206 let source_directory = package.source_directory();
207 package
209 .programs
210 .iter()
211 .clone()
212 .map(|program| {
213 let program_id = ProgramID::<A::Network>::from_str(&format!("{}.aleo", program.name))
214 .map_err(|e| CliError::custom(format!("Failed to parse program ID: {e}")))?;
215 match &program.data {
216 ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string(), program.edition)),
217 ProgramData::SourcePath { source, .. } => {
218 let bytecode_path = if source.as_path() == source_directory.join("main.leo") {
220 build_directory.join("main.aleo")
221 } else {
222 imports_directory.join(format!("{}.aleo", program.name))
223 };
224 let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| {
226 CliError::custom(format!("Failed to read bytecode at {}: {e}", bytecode_path.display()))
227 })?;
228 Ok((program_id, bytecode, program.edition))
230 }
231 }
232 })
233 .collect::<Result<Vec<_>>>()?
234 } else {
235 Vec::new()
236 };
237
238 let mut programs = programs
240 .into_iter()
241 .map(|(_, bytecode, edition)| {
242 let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
244 .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
245 Ok((program, edition))
247 })
248 .collect::<Result<Vec<_>>>()?;
249
250 let is_local = programs.iter().any(|(program, _)| program.id() == &program_id);
252
253 if is_local {
255 let program = &programs
256 .iter()
257 .find(|(program, _)| program.id() == &program_id)
258 .expect("Program should exist since it is local")
259 .0;
260 if !program.contains_function(&function_id) {
261 return Err(CliError::custom(format!(
262 "Function `{function_name}` does not exist in program `{program_name}`."
263 ))
264 .into());
265 }
266 }
267
268 let inputs =
269 command.inputs.into_iter().map(|string| parse_input(&string, &private_key)).collect::<Result<Vec<_>>>()?;
270
271 let (_, priority_fee, record) =
273 parse_fee_options(&private_key, &command.fee_options, 1)?.into_iter().next().unwrap_or((None, None, None));
274
275 let consensus_version =
277 get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
278
279 print_execution_plan::<A::Network>(
281 &private_key,
282 &address,
283 &endpoint,
284 &network,
285 &program_name,
286 &function_name,
287 is_local,
288 priority_fee.unwrap_or(0),
289 record.is_some(),
290 &command.action,
291 consensus_version,
292 &check_task_for_warnings(&endpoint, network, &programs, consensus_version),
293 );
294
295 if !confirm("Do you want to proceed with execution?", command.extra.yes)? {
297 println!("β Execution aborted.");
298 return Ok(());
299 }
300
301 let rng = &mut rand::thread_rng();
303
304 let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
306
307 let query = SnarkVMQuery::<A::Network, BlockMemory<A::Network>>::from(
309 endpoint
310 .parse::<Uri>()
311 .map_err(|e| CliError::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
312 );
313 if !is_local {
316 println!("β¬οΈ Downloading {program_name} and its dependencies from {endpoint}...");
317 programs = load_latest_programs_from_network(&context, program_id, network, &endpoint)?;
318 };
319
320 println!("\nβAdding programs to the VM in the following order:");
322 let programs_and_editions = programs
323 .into_iter()
324 .map(|(program, edition)| {
325 let edition = edition.unwrap_or(1);
327 let id = program.id().to_string();
329 match id == "credits.aleo" {
331 true => println!(" - {id} (already included)"),
332 false => println!(" - {id} (edition: {edition})"),
333 }
334 (program, edition)
335 })
336 .collect::<Vec<_>>();
337 vm.process().write().add_programs_with_editions(&programs_and_editions)?;
338
339 let (transaction, response) = vm.execute_with_response(
341 &private_key,
342 (&program_name, &function_name),
343 inputs.iter(),
344 record,
345 priority_fee.unwrap_or(0),
346 Some(&query),
347 rng,
348 )?;
349
350 print_execution_stats::<A::Network>(
352 &vm,
353 &program_name,
354 transaction.execution().expect("Expected execution"),
355 priority_fee,
356 consensus_version,
357 )?;
358
359 if command.action.print {
363 let transaction_json = serde_json::to_string_pretty(&transaction)
364 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
365 println!("π¨οΈ Printing execution for {program_name}\n{transaction_json}");
366 }
367
368 if let Some(path) = &command.action.save {
372 std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
374 let file_path = PathBuf::from(path).join(format!("{program_name}.execution.json"));
376 println!("πΎ Saving execution for {program_name} at {}", file_path.display());
377 let transaction_json = serde_json::to_string_pretty(&transaction)
378 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
379 std::fs::write(file_path, transaction_json)
380 .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
381 }
382
383 match response.outputs().len() {
384 0 => (),
385 1 => println!("\nβ‘οΈ Output\n"),
386 _ => println!("\nβ‘οΈ Outputs\n"),
387 };
388 for output in response.outputs() {
389 println!(" β’ {output}");
390 }
391 println!();
392
393 if command.action.broadcast {
395 println!("π‘ Broadcasting execution for {program_name}...");
396 let mut fee_id = None;
398 if let Some(fee) = transaction.fee_transition() {
399 if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
401 println!("β Execution aborted.");
402 return Ok(());
403 }
404 fee_id = Some(fee.id().to_string());
405 }
406 let id = transaction.id().to_string();
407 let height_before = check_transaction::current_height(&endpoint, network)?;
408 let (message, status) =
410 handle_broadcast(&format!("{endpoint}/{network}/transaction/broadcast"), &transaction, &program_name)?;
411
412 let fail = |msg| {
413 println!("β Failed to broadcast execution: {msg}.");
414 Ok(())
415 };
416
417 match status {
418 200..=299 => {
419 let status = check_transaction::check_transaction_with_message(
420 &id,
421 fee_id.as_deref(),
422 &endpoint,
423 network,
424 height_before + 1,
425 command.extra.max_wait,
426 command.extra.blocks_to_check,
427 )?;
428 if status == Some(TransactionStatus::Accepted) {
429 println!("β
Execution confirmed!");
430 }
431 }
432 _ => {
433 return fail(&message);
434 }
435 }
436 }
437
438 Ok(())
439}
440
441fn check_task_for_warnings<N: Network>(
445 endpoint: &str,
446 network: NetworkName,
447 programs: &[(Program<N>, Option<u16>)],
448 consensus_version: ConsensusVersion,
449) -> Vec<String> {
450 let mut warnings = Vec::new();
451 for (program, _) in programs {
452 if let Ok(remote_program) = fetch_program_from_network(&program.id().to_string(), endpoint, network) {
454 let remote_program = match Program::<N>::from_str(&remote_program) {
456 Ok(program) => program,
457 Err(e) => {
458 warnings.push(format!("Could not parse '{}' from the network. Error: {e}", program.id()));
459 continue;
460 }
461 };
462 if remote_program != *program {
464 warnings.push(format!(
465 "The program '{}' on the network does not match the local copy. If you have a local dependency, you may use the `--no-local` flag to use the network version instead.",
466 program.id()
467 ));
468 }
469 } else {
470 warnings.push(format!(
471 "The program '{}' does not exist on the network. You may use `leo deploy --broadcast` to deploy it.",
472 program.id()
473 ));
474 }
475 }
476 if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
478 warnings.push(format!("{e}. In some cases, the execution may fail"));
479 }
480 warnings
481}
482
483#[allow(clippy::too_many_arguments)]
485fn print_execution_plan<N: Network>(
486 private_key: &PrivateKey<N>,
487 address: &Address<N>,
488 endpoint: &str,
489 network: &NetworkName,
490 program_name: &str,
491 function_name: &str,
492 is_local: bool,
493 priority_fee: u64,
494 fee_record: bool,
495 action: &TransactionAction,
496 consensus_version: ConsensusVersion,
497 warnings: &[String],
498) {
499 println!("\n{}", "π Execution Plan Summary".bold().underline());
500 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
501
502 println!("{}", "π§ Configuration:".bold());
503 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
504 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
505 println!(" {:20}{}", "Endpoint:", endpoint.yellow());
506 println!(" {:20}{}", "Network:", network.to_string().yellow());
507 println!(" {:20}{}", "Consensus Version:", (consensus_version as u8).to_string().yellow());
508
509 println!("\n{}", "π― Execution Target:".bold());
510 println!(" {:16}{}", "Program:", program_name.cyan());
511 println!(" {:16}{}", "Function:", function_name.cyan());
512 println!(" {:16}{}", "Source:", if is_local { "local" } else { "remote" });
513
514 println!("\n{}", "πΈ Fee Info:".bold());
515 println!(" {:16}{}", "Priority Fee:", format!("{priority_fee} ΞΌcredits").green());
516 println!(" {:16}{}", "Fee Record:", if fee_record { "yes" } else { "no (public fee)" });
517
518 println!("\n{}", "βοΈ Actions:".bold());
519 if !is_local {
520 println!(" - Program and its dependencies will be downloaded from the network.");
521 }
522 if action.print {
523 println!(" - Transaction will be printed to the console.");
524 } else {
525 println!(" - Transaction will NOT be printed to the console.");
526 }
527 if let Some(path) = &action.save {
528 println!(" - Transaction will be saved to {}", path.bold());
529 } else {
530 println!(" - Transaction will NOT be saved to a file.");
531 }
532 if action.broadcast {
533 println!(" - Transaction will be broadcast to {}", endpoint.bold());
534 } else {
535 println!(" - Transaction will NOT be broadcast to the network.");
536 }
537
538 if !warnings.is_empty() {
540 println!("\n{}", "β οΈ Warnings:".bold().red());
541 for warning in warnings {
542 println!(" β’ {}", warning.dimmed());
543 }
544 }
545
546 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ\n".dimmed());
547}
548
549fn print_execution_stats<N: Network>(
552 vm: &VM<N, ConsensusMemory<N>>,
553 program_name: &str,
554 execution: &Execution<N>,
555 priority_fee: Option<u64>,
556 consensus_version: ConsensusVersion,
557) -> Result<()> {
558 use colored::*;
559
560 let (base_fee, (storage_cost, execution_cost)) =
562 execution_cost(&vm.process().read(), execution, consensus_version)?;
563
564 let base_cr = base_fee as f64 / 1_000_000.0;
565 let prio_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0;
566 let total_cr = base_cr + prio_cr;
567
568 println!("\n{} {}", "π Execution Summary for".bold(), program_name.bold());
570 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
571
572 println!("{}", "π° Cost Breakdown (credits)".bold());
574 println!(" {:22}{}{:.6}", "Transaction Storage:".cyan(), "".yellow(), storage_cost as f64 / 1_000_000.0);
575 println!(" {:22}{}{:.6}", "Onβchain Execution:".cyan(), "".yellow(), execution_cost as f64 / 1_000_000.0);
576 println!(" {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_cr);
577 println!(" {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_cr);
578
579 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
581 Ok(())
582}