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    // Specify the query
308    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 the program is not local, then download it and its dependencies for the network.
314    // Note: The dependencies are downloaded in "post-order" (child before parent).
315    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    // Add the programs to the VM.
321    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            // 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.
326            let edition = edition.unwrap_or(1);
327            // Get the program ID.
328            let id = program.id().to_string();
329            // Print the program ID and edition.
330            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    // Execute the program and produce a transaction.
340    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 the execution stats.
351    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    // Print the transaction.
360    // If the `print` option is set, print the execution transaction to the console.
361    // The transaction is printed in JSON format.
362    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 the `save` option is set, save the execution transaction to a file in the specified directory.
369    // The file format is `program_name.execution.json`.
370    // The directory is created if it doesn't exist.
371    if let Some(path) = &command.action.save {
372        // Create the directory if it doesn't exist.
373        std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
374        // Save the transaction to a file.
375        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 the `broadcast` option is set, broadcast each deployment transaction to the network.
394    if command.action.broadcast {
395        println!("πŸ“‘ Broadcasting execution for {program_name}...");
396        // Get and confirm the fee with the user.
397        let mut fee_id = None;
398        if let Some(fee) = transaction.fee_transition() {
399            // Most transactions will have fees, but some, like credits.aleo/upgrade executions, may not.
400            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        // Broadcast the transaction to the network.
409        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
441/// Check the execution task for warnings.
442/// The following properties are checked:
443///   - The component programs exist on the network and match the local ones.
444fn 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        // Check if the program exists on the network.
453        if let Ok(remote_program) = fetch_program_from_network(&program.id().to_string(), endpoint, network) {
454            // Parse the program.
455            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            // Check if the program matches the local one.
463            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    // Check for a consensus version mismatch.
477    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/// Pretty-print the execution plan in a readable format.
484#[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    // ── Warnings ─────────────────────────────────────────────────────────
539    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
549/// Pretty‑print execution statistics without a table, using the same UI
550/// conventions as `print_deployment_plan`.
551fn 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    // ── Gather cost components ────────────────────────────────────────────
561    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    // ── Header ────────────────────────────────────────────────────────────
569    println!("\n{} {}", "πŸ“Š Execution Summary for".bold(), program_name.bold());
570    println!("{}", "──────────────────────────────────────────────".dimmed());
571
572    // ── Cost breakdown ────────────────────────────────────────────────────
573    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    // ── Footer rule ───────────────────────────────────────────────────────
580    println!("{}", "──────────────────────────────────────────────".dimmed());
581    Ok(())
582}