leo_lang/cli/commands/
execute.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo 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 Leo 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 Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use 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/// Build, Prove and Run Leo program with inputs
48#[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        // Get the path to the current directory.
83        let path = context.dir()?;
84        // Get the path to the home directory.
85        let home_path = context.home()?;
86        // Get the network, accounting for overrides.
87        let network = get_network(&self.env_override.network)?;
88        // Get the endpoint, accounting for overrides.
89        let endpoint = get_endpoint(&self.env_override.endpoint)?;
90        // If the current directory is a valid Leo package, then build it.
91        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            // Return the package.
102            Ok(Some(package))
103        } else {
104            Ok(None)
105        }
106    }
107
108    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
109        // Get the network, accounting for overrides.
110        let network = get_network(&self.env_override.network)?;
111        // Handle each network with the appropriate parameterization.
112        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
130// A helper function to handle the `execute` command.
131fn handle_execute<A: Aleo>(
132    command: LeoExecute,
133    context: Context,
134    network: NetworkName,
135    package: Option<Package>,
136) -> Result<<LeoExecute as Command>::Output> {
137    // Get the private key and associated address, accounting for overrides.
138    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    // Get the endpoint, accounting for overrides.
143    let endpoint = get_endpoint(&command.env_override.endpoint)?;
144
145    // Get whether the network is a devnet, accounting for overrides.
146    let is_devnet = get_is_devnet(command.env_override.devnet);
147
148    // If the consensus heights are provided, use them; otherwise, use the default heights for the network.
149    let consensus_heights =
150        command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet));
151    // Validate the provided consensus heights.
152    validate_consensus_heights(&consensus_heights)
153        .map_err(|e| CliError::custom(format!("Invalid consensus heights: {e}")))?;
154    // Print the consensus heights being used.
155    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    // Set the consensus heights in the environment.
161    #[allow(unsafe_code)]
162    unsafe {
163        // SAFETY:
164        //  - `CONSENSUS_VERSION_HEIGHTS` is only set once and is only read in `snarkvm::prelude::load_consensus_heights`.
165        //  - There are no concurrent threads running at this point in the execution.
166        // WHY:
167        //  - This is needed because there is no way to set the desired consensus heights for a particular `VM` instance
168        //    without using the environment variable `CONSENSUS_VERSION_HEIGHTS`. Which is itself read once, and stored in a `OnceLock`.
169        std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
170    }
171
172    // Parse the <NAME> into an optional program name and a function name.
173    // If only a function name is provided, then use the program name from the package.
174    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    // Parse the program name as a `ProgramID`.
194    let program_id = ProgramID::<A::Network>::from_str(&program_name)
195        .map_err(|e| CliError::custom(format!("Failed to parse program name: {e}")))?;
196    // Parse the function name as an `Identifier`.
197    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    // Get all the dependencies in the package if it exists.
201    // Get the programs and optional manifests for all programs.
202    let programs = if let Some(package) = &package {
203        // Get the package directories.
204        let build_directory = package.build_directory();
205        let imports_directory = package.imports_directory();
206        let source_directory = package.source_directory();
207        // Get the program names and their bytecode.
208        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                        // Get the path to the built bytecode.
219                        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                        // Fetch the bytecode.
225                        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                        // Return the bytecode and the manifest.
229                        Ok((program_id, bytecode, program.edition))
230                    }
231                }
232            })
233            .collect::<Result<Vec<_>>>()?
234    } else {
235        Vec::new()
236    };
237
238    // Parse the program strings into AVM programs.
239    let mut programs = programs
240        .into_iter()
241        .map(|(_, bytecode, edition)| {
242            // Parse the program.
243            let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
244                .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
245            // Return the program and its name.
246            Ok((program, edition))
247        })
248        .collect::<Result<Vec<_>>>()?;
249
250    // Determine whether the program is local or remote.
251    let is_local = programs.iter().any(|(program, _)| program.id() == &program_id);
252
253    // If the program is local, then check that the function exists.
254    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    // Get the first fee option.
272    let (_, priority_fee, record) =
273        parse_fee_options(&private_key, &command.fee_options, 1)?.into_iter().next().unwrap_or((None, None, None));
274
275    // Get the consensus version.
276    let consensus_version =
277        get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
278
279    // Print the execution plan.
280    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    // Prompt the user to confirm the plan.
296    if !confirm("Do you want to proceed with execution?", command.extra.yes)? {
297        println!("❌ Execution aborted.");
298        return Ok(());
299    }
300
301    // Initialize an RNG.
302    let rng = &mut rand::thread_rng();
303
304    // Initialize a new VM.
305    let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
306
307    // Remove version suffixes from the endpoint.
308    let re = regex::Regex::new(r"v\d+$").unwrap();
309    let query_endpoint = re.replace(&endpoint, "").to_string();
310
311    // Specify the query.
312    let query = SnarkVMQuery::<A::Network, BlockMemory<A::Network>>::from(
313        query_endpoint
314            .parse::<Uri>()
315            .map_err(|e| CliError::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
316    );
317
318    // If the program is not local, then download it and its dependencies for the network.
319    // Note: The dependencies are downloaded in "post-order" (child before parent).
320    if !is_local {
321        println!("⬇️ Downloading {program_name} and its dependencies from {endpoint}...");
322        programs = load_latest_programs_from_network(&context, program_id, network, &endpoint)?;
323    };
324
325    // Add the programs to the VM.
326    println!("\nβž•Adding programs to the VM in the following order:");
327    let programs_and_editions = programs
328        .into_iter()
329        .map(|(program, edition)| {
330            // Note: We default to edition 1 since snarkVM execute may produce spurious errors if the program does not have a constructor but uses edition 0.
331            let edition = edition.unwrap_or(1);
332            // Get the program ID.
333            let id = program.id().to_string();
334            // Print the program ID and edition.
335            match id == "credits.aleo" {
336                true => println!("  - {id} (already included)"),
337                false => println!("  - {id} (edition: {edition})"),
338            }
339            (program, edition)
340        })
341        .collect::<Vec<_>>();
342    vm.process().write().add_programs_with_editions(&programs_and_editions)?;
343
344    // Execute the program and produce a transaction.
345    println!("\nβš™οΈ Executing {program_name}/{function_name}...");
346    let (transaction, response) = vm.execute_with_response(
347        &private_key,
348        (&program_name, &function_name),
349        inputs.iter(),
350        record,
351        priority_fee.unwrap_or(0),
352        Some(&query),
353        rng,
354    )?;
355
356    // Print the execution stats.
357    print_execution_stats::<A::Network>(
358        &vm,
359        &program_name,
360        transaction.execution().expect("Expected execution"),
361        priority_fee,
362        consensus_version,
363    )?;
364
365    // Print the transaction.
366    // If the `print` option is set, print the execution transaction to the console.
367    // The transaction is printed in JSON format.
368    if command.action.print {
369        let transaction_json = serde_json::to_string_pretty(&transaction)
370            .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
371        println!("πŸ–¨οΈ Printing execution for {program_name}\n{transaction_json}");
372    }
373
374    // If the `save` option is set, save the execution transaction to a file in the specified directory.
375    // The file format is `program_name.execution.json`.
376    // The directory is created if it doesn't exist.
377    if let Some(path) = &command.action.save {
378        // Create the directory if it doesn't exist.
379        std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
380        // Save the transaction to a file.
381        let file_path = PathBuf::from(path).join(format!("{program_name}.execution.json"));
382        println!("πŸ’Ύ Saving execution for {program_name} at {}", file_path.display());
383        let transaction_json = serde_json::to_string_pretty(&transaction)
384            .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
385        std::fs::write(file_path, transaction_json)
386            .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
387    }
388
389    match response.outputs().len() {
390        0 => (),
391        1 => println!("\n➑️  Output\n"),
392        _ => println!("\n➑️  Outputs\n"),
393    };
394    for output in response.outputs() {
395        println!(" β€’ {output}");
396    }
397    println!();
398
399    // If the `broadcast` option is set, broadcast each deployment transaction to the network.
400    if command.action.broadcast {
401        println!("πŸ“‘ Broadcasting execution for {program_name}...");
402        // Get and confirm the fee with the user.
403        let mut fee_id = None;
404        if let Some(fee) = transaction.fee_transition() {
405            // Most transactions will have fees, but some, like credits.aleo/upgrade executions, may not.
406            if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
407                println!("❌ Execution aborted.");
408                return Ok(());
409            }
410            fee_id = Some(fee.id().to_string());
411        }
412        let id = transaction.id().to_string();
413        let height_before = check_transaction::current_height(&endpoint, network)?;
414        // Broadcast the transaction to the network.
415        let (message, status) =
416            handle_broadcast(&format!("{endpoint}/{network}/transaction/broadcast"), &transaction, &program_name)?;
417
418        let fail = |msg| {
419            println!("❌ Failed to broadcast execution: {msg}.");
420            Ok(())
421        };
422
423        match status {
424            200..=299 => {
425                let status = check_transaction::check_transaction_with_message(
426                    &id,
427                    fee_id.as_deref(),
428                    &endpoint,
429                    network,
430                    height_before + 1,
431                    command.extra.max_wait,
432                    command.extra.blocks_to_check,
433                )?;
434                if status == Some(TransactionStatus::Accepted) {
435                    println!("βœ… Execution confirmed!");
436                }
437            }
438            _ => {
439                return fail(&message);
440            }
441        }
442    }
443
444    Ok(())
445}
446
447/// Check the execution task for warnings.
448/// The following properties are checked:
449///   - The component programs exist on the network and match the local ones.
450fn check_task_for_warnings<N: Network>(
451    endpoint: &str,
452    network: NetworkName,
453    programs: &[(Program<N>, Option<u16>)],
454    consensus_version: ConsensusVersion,
455) -> Vec<String> {
456    let mut warnings = Vec::new();
457    for (program, _) in programs {
458        // Check if the program exists on the network.
459        if let Ok(remote_program) = fetch_program_from_network(&program.id().to_string(), endpoint, network) {
460            // Parse the program.
461            let remote_program = match Program::<N>::from_str(&remote_program) {
462                Ok(program) => program,
463                Err(e) => {
464                    warnings.push(format!("Could not parse '{}' from the network. Error: {e}", program.id()));
465                    continue;
466                }
467            };
468            // Check if the program matches the local one.
469            if remote_program != *program {
470                warnings.push(format!(
471                    "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.",
472                    program.id()
473                ));
474            }
475        } else {
476            warnings.push(format!(
477                "The program '{}' does not exist on the network. You may use `leo deploy --broadcast` to deploy it.",
478                program.id()
479            ));
480        }
481    }
482    // Check for a consensus version mismatch.
483    if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
484        warnings.push(format!("{e}. In some cases, the execution may fail"));
485    }
486    warnings
487}
488
489/// Pretty-print the execution plan in a readable format.
490#[allow(clippy::too_many_arguments)]
491fn print_execution_plan<N: Network>(
492    private_key: &PrivateKey<N>,
493    address: &Address<N>,
494    endpoint: &str,
495    network: &NetworkName,
496    program_name: &str,
497    function_name: &str,
498    is_local: bool,
499    priority_fee: u64,
500    fee_record: bool,
501    action: &TransactionAction,
502    consensus_version: ConsensusVersion,
503    warnings: &[String],
504) {
505    println!("\n{}", "πŸš€ Execution Plan Summary".bold().underline());
506    println!("{}", "──────────────────────────────────────────────".dimmed());
507
508    println!("{}", "πŸ”§ Configuration:".bold());
509    println!("  {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
510    println!("  {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
511    println!("  {:20}{}", "Endpoint:", endpoint.yellow());
512    println!("  {:20}{}", "Network:", network.to_string().yellow());
513    println!("  {:20}{}", "Consensus Version:", (consensus_version as u8).to_string().yellow());
514
515    println!("\n{}", "🎯 Execution Target:".bold());
516    println!("  {:16}{}", "Program:", program_name.cyan());
517    println!("  {:16}{}", "Function:", function_name.cyan());
518    println!("  {:16}{}", "Source:", if is_local { "local" } else { "remote" });
519
520    println!("\n{}", "πŸ’Έ Fee Info:".bold());
521    println!("  {:16}{}", "Priority Fee:", format!("{priority_fee} ΞΌcredits").green());
522    println!("  {:16}{}", "Fee Record:", if fee_record { "yes" } else { "no (public fee)" });
523
524    println!("\n{}", "βš™οΈ Actions:".bold());
525    if !is_local {
526        println!("  - Program and its dependencies will be downloaded from the network.");
527    }
528    if action.print {
529        println!("  - Transaction will be printed to the console.");
530    } else {
531        println!("  - Transaction will NOT be printed to the console.");
532    }
533    if let Some(path) = &action.save {
534        println!("  - Transaction will be saved to {}", path.bold());
535    } else {
536        println!("  - Transaction will NOT be saved to a file.");
537    }
538    if action.broadcast {
539        println!("  - Transaction will be broadcast to {}", endpoint.bold());
540    } else {
541        println!("  - Transaction will NOT be broadcast to the network.");
542    }
543
544    // ── Warnings ─────────────────────────────────────────────────────────
545    if !warnings.is_empty() {
546        println!("\n{}", "⚠️ Warnings:".bold().red());
547        for warning in warnings {
548            println!("  β€’ {}", warning.dimmed());
549        }
550    }
551
552    println!("{}", "──────────────────────────────────────────────\n".dimmed());
553}
554
555/// Pretty‑print execution statistics without a table, using the same UI
556/// conventions as `print_deployment_plan`.
557fn print_execution_stats<N: Network>(
558    vm: &VM<N, ConsensusMemory<N>>,
559    program_name: &str,
560    execution: &Execution<N>,
561    priority_fee: Option<u64>,
562    consensus_version: ConsensusVersion,
563) -> Result<()> {
564    use colored::*;
565
566    // ── Gather cost components ────────────────────────────────────────────
567    let (base_fee, (storage_cost, execution_cost)) =
568        execution_cost(&vm.process().read(), execution, consensus_version)?;
569
570    let base_cr = base_fee as f64 / 1_000_000.0;
571    let prio_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0;
572    let total_cr = base_cr + prio_cr;
573
574    // ── Header ────────────────────────────────────────────────────────────
575    println!("\n{} {}", "πŸ“Š Execution Summary for".bold(), program_name.bold());
576    println!("{}", "──────────────────────────────────────────────".dimmed());
577
578    // ── Cost breakdown ────────────────────────────────────────────────────
579    println!("{}", "πŸ’° Cost Breakdown (credits)".bold());
580    println!("  {:22}{}{:.6}", "Transaction Storage:".cyan(), "".yellow(), storage_cost as f64 / 1_000_000.0);
581    println!("  {:22}{}{:.6}", "On‑chain Execution:".cyan(), "".yellow(), execution_cost as f64 / 1_000_000.0);
582    println!("  {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_cr);
583    println!("  {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_cr);
584
585    // ── Footer rule ───────────────────────────────────────────────────────
586    println!("{}", "──────────────────────────────────────────────".dimmed());
587    Ok(())
588}