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_package::{NetworkName, Package, ProgramData};
21
22use aleo_std::StorageMode;
23use clap::Parser;
24use colored::*;
25use snarkvm::prelude::{Execution, Network};
26use std::{convert::TryFrom, path::PathBuf};
27
28#[cfg(not(feature = "only_testnet"))]
29use snarkvm::circuit::{AleoCanaryV0, AleoV0};
30use snarkvm::{
31    circuit::{Aleo, AleoTestnetV0},
32    prelude::{
33        ConsensusVersion,
34        Identifier,
35        ProgramID,
36        VM,
37        Value,
38        execution_cost_v1,
39        execution_cost_v2,
40        query::Query as SnarkVMQuery,
41        store::{ConsensusStore, helpers::memory::ConsensusMemory},
42    },
43};
44
45/// Build, Prove and Run Leo program with inputs
46#[derive(Parser, Debug)]
47pub struct LeoExecute {
48    #[clap(
49        name = "NAME",
50        help = "The name of the function to execute, e.g `helloworld.aleo/main` or `main`.",
51        default_value = "main"
52    )]
53    name: String,
54    #[clap(name = "INPUTS", help = "The inputs to the program.")]
55    inputs: Vec<String>,
56    #[clap(flatten)]
57    pub(crate) fee_options: FeeOptions,
58    #[clap(flatten)]
59    pub(crate) action: TransactionAction,
60    #[clap(flatten)]
61    pub(crate) env_override: EnvOptions,
62    #[clap(flatten)]
63    pub(crate) extra: ExtraOptions,
64    #[clap(flatten)]
65    build_options: BuildOptions,
66}
67
68impl Command for LeoExecute {
69    type Input = Option<Package>;
70    type Output = ();
71
72    fn log_span(&self) -> Span {
73        tracing::span!(tracing::Level::INFO, "Leo")
74    }
75
76    fn prelude(&self, context: Context) -> Result<Self::Input> {
77        // Get the path to the current directory.
78        let path = context.dir()?;
79        // Get the path to the home directory.
80        let home_path = context.home()?;
81        // If the current directory is a valid Leo package, then build it.
82        if Package::from_directory_no_graph(path, home_path).is_ok() {
83            let package = LeoBuild {
84                env_override: self.env_override.clone(),
85                options: {
86                    let mut options = self.build_options.clone();
87                    options.no_cache = true;
88                    options
89                },
90            }
91            .execute(context)?;
92            // Return the package.
93            Ok(Some(package))
94        } else {
95            Ok(None)
96        }
97    }
98
99    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
100        // Get the network, accounting for overrides.
101        let network = context.get_network(&self.env_override.network)?.parse()?;
102        // Handle each network with the appropriate parameterization.
103        match network {
104            NetworkName::TestnetV0 => handle_execute::<AleoTestnetV0>(self, context, network, input),
105            NetworkName::MainnetV0 => {
106                #[cfg(feature = "only_testnet")]
107                panic!("Mainnet chosen with only_testnet feature");
108                #[cfg(not(feature = "only_testnet"))]
109                return handle_execute::<AleoV0>(self, context, network, input);
110            }
111            NetworkName::CanaryV0 => {
112                #[cfg(feature = "only_testnet")]
113                panic!("Canary chosen with only_testnet feature");
114                #[cfg(not(feature = "only_testnet"))]
115                return handle_execute::<AleoCanaryV0>(self, context, network, input);
116            }
117        }
118    }
119}
120
121// A helper function to handle the `execute` command.
122fn handle_execute<A: Aleo>(
123    command: LeoExecute,
124    context: Context,
125    network: NetworkName,
126    package: Option<Package>,
127) -> Result<<LeoExecute as Command>::Output> {
128    // Get the private key and associated address, accounting for overrides.
129    let private_key = context.get_private_key(&command.env_override.private_key)?;
130    let address = Address::<A::Network>::try_from(&private_key)
131        .map_err(|e| CliError::custom(format!("Failed to parse address: {e}")))?;
132
133    // Get the endpoint, accounting for overrides.
134    let endpoint = context.get_endpoint(&command.env_override.endpoint)?;
135
136    // Parse the <NAME> into an optional program name and a function name.
137    // If only a function name is provided, then use the program name from the package.
138    let (program_name, function_name) = match command.name.split_once('/') {
139        Some((program_name, function_name)) => (program_name.to_string(), function_name.to_string()),
140        None => match &package {
141            Some(package) => (
142                package.programs.last().expect("There must be at least one program in a Leo package").name.to_string(),
143                command.name,
144            ),
145            None => {
146                return Err(CliError::custom(format!(
147                    "Running `leo execute {} ...`, without an explicit program name requires that your current working directory is a valid Leo project.",
148                    command.name
149                )).into());
150            }
151        },
152    };
153
154    // Parse the program name as a `ProgramID`.
155    let program_id = ProgramID::<A::Network>::from_str(&program_name)
156        .map_err(|e| CliError::custom(format!("Failed to parse program name: {e}")))?;
157    // Parse the function name as an `Identifier`.
158    let function_id = Identifier::<A::Network>::from_str(&function_name)
159        .map_err(|e| CliError::custom(format!("Failed to parse function name: {e}")))?;
160
161    // Get all the dependencies in the package if it exists.
162    // Get the programs and optional manifests for all programs.
163    let programs = if let Some(package) = &package {
164        // Get the package directories.
165        let build_directory = package.build_directory();
166        let imports_directory = package.imports_directory();
167        let source_directory = package.source_directory();
168        // Get the program names and their bytecode.
169        package
170            .programs
171            .iter()
172            .clone()
173            .map(|program| {
174                let program_id = ProgramID::<A::Network>::from_str(&format!("{}.aleo", program.name))
175                    .map_err(|e| CliError::custom(format!("Failed to parse program ID: {e}")))?;
176                match &program.data {
177                    ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string())),
178                    ProgramData::SourcePath(path) => {
179                        // Get the path to the built bytecode.
180                        let bytecode_path = if path.as_path() == source_directory.join("main.leo") {
181                            build_directory.join("main.aleo")
182                        } else {
183                            imports_directory.join(format!("{}.aleo", program.name))
184                        };
185                        // Fetch the bytecode.
186                        let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| {
187                            CliError::custom(format!("Failed to read bytecode at {}: {e}", bytecode_path.display()))
188                        })?;
189                        // Return the bytecode and the manifest.
190                        Ok((program_id, bytecode))
191                    }
192                }
193            })
194            .collect::<Result<Vec<_>>>()?
195    } else {
196        Vec::new()
197    };
198
199    // Parse the program strings into AVM programs.
200    let mut programs = programs
201        .into_iter()
202        .map(|(name, bytecode)| {
203            // Parse the program.
204            let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
205                .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
206            // Return the program and its name.
207            Ok((name, program))
208        })
209        .collect::<Result<Vec<_>>>()?;
210
211    // Determine whether the program is local or remote.
212    let is_local = programs.iter().any(|(name, _)| name == &program_id);
213
214    // If the program is local, then check that the function exists.
215    if is_local {
216        let program =
217            &programs.iter().find(|(name, _)| name == &program_id).expect("Program should exist since it is local").1;
218        if !program.contains_function(&function_id) {
219            return Err(CliError::custom(format!(
220                "Function `{function_name}` does not exist in program `{program_name}`."
221            ))
222            .into());
223        }
224    }
225
226    let inputs = command
227        .inputs
228        .into_iter()
229        .map(|input| {
230            Value::from_str(&input).map_err(|e| CliError::custom(format!("Failed to parse input: {e}")).into())
231        })
232        .collect::<Result<Vec<_>>>()?;
233
234    // Get the first fee option.
235    let (_, priority_fee, record) =
236        parse_fee_options(&private_key, &command.fee_options, 1)?.into_iter().next().unwrap_or((None, None, None));
237
238    // Get the consensus version.
239    let consensus_version =
240        get_consensus_version::<A::Network>(&command.extra.consensus_version, &endpoint, network, &context)?;
241
242    // Print the execution plan.
243    print_execution_plan::<A::Network>(
244        &private_key,
245        &address,
246        &endpoint,
247        &network,
248        &program_name,
249        &function_name,
250        is_local,
251        priority_fee.unwrap_or(0),
252        record.is_some(),
253        &command.action,
254        consensus_version,
255    );
256
257    // Prompt the user to confirm the plan.
258    if !confirm("Do you want to proceed with execution?", command.extra.yes)? {
259        println!("❌ Execution aborted.");
260        return Ok(());
261    }
262
263    // Initialize an RNG.
264    let rng = &mut rand::thread_rng();
265
266    // Initialize a new VM.
267    let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
268
269    // Specify the query
270    let query = SnarkVMQuery::from(&endpoint);
271
272    // If the program is not local, then download it and its dependencies for the network.
273    // Note: The dependencies are downloaded in "post-order" (child before parent).
274    if !is_local {
275        println!("⬇️ Downloading {program_name} and its dependencies from {endpoint}...");
276        programs = load_programs_from_network(&context, program_id, network, &endpoint)?;
277    };
278
279    // Add the programs to the VM.
280    println!("Adding programs to the VM ...");
281    for (_, program) in programs {
282        vm.process().write().add_program(&program)?;
283    }
284
285    // Execute the program and produce a transaction.
286    let transaction = vm.execute(
287        &private_key,
288        (&program_name, &function_name),
289        inputs.iter(),
290        record,
291        priority_fee.unwrap_or(0),
292        Some(query),
293        rng,
294    )?;
295
296    // Print the execution stats.
297    print_execution_stats::<A::Network>(
298        &vm,
299        &program_name,
300        transaction.execution().expect("Expected execution"),
301        priority_fee,
302        consensus_version,
303    )?;
304
305    // Print the transaction.
306    // If the `print` option is set, print the execution transaction to the console.
307    // The transaction is printed in JSON format.
308    if command.action.print {
309        let transaction_json = serde_json::to_string_pretty(&transaction)
310            .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
311        println!("πŸ–¨οΈ Printing execution for {program_name}\n{transaction_json}");
312    }
313
314    // If the `save` option is set, save the execution transaction to a file in the specified directory.
315    // The file format is `program_name.execution.json`.
316    // The directory is created if it doesn't exist.
317    if let Some(path) = &command.action.save {
318        // Create the directory if it doesn't exist.
319        std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
320        // Save the transaction to a file.
321        let file_path = PathBuf::from(path).join(format!("{program_name}.execution.json"));
322        println!("πŸ’Ύ Saving execution for {program_name} at {}", file_path.display());
323        let transaction_json = serde_json::to_string_pretty(&transaction)
324            .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
325        std::fs::write(file_path, transaction_json)
326            .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
327    }
328
329    // If the `broadcast` option is set, broadcast each deployment transaction to the network.
330    if command.action.broadcast {
331        println!("πŸ“‘ Broadcasting execution for {program_name}...");
332        // Get and confirm the fee with the user.
333        let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
334        if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
335            println!("❌ Execution aborted.");
336            return Ok(());
337        }
338        let fee_id = fee.id().to_string();
339        let id = transaction.id().to_string();
340        let height_before = check_transaction::current_height(&endpoint, network)?;
341        // Broadcast the transaction to the network.
342        let response =
343            handle_broadcast(&format!("{}/{}/transaction/broadcast", endpoint, network), &transaction, &program_name)?;
344
345        let fail = |msg| {
346            println!("❌ Failed to broadcast execution: {}.", msg);
347            Ok(())
348        };
349
350        match response.status() {
351            200..=299 => {
352                let status = check_transaction::check_transaction_with_message(
353                    &id,
354                    Some(&fee_id),
355                    &endpoint,
356                    network,
357                    height_before + 1,
358                    command.extra.max_wait,
359                    command.extra.blocks_to_check,
360                )?;
361                if status == Some(TransactionStatus::Accepted) {
362                    println!("βœ… Execution confirmed!");
363                }
364            }
365            _ => {
366                let error_message =
367                    response.into_string().map_err(|e| CliError::custom(format!("Failed to read response: {e}")))?;
368                return fail(&error_message);
369            }
370        }
371    }
372
373    Ok(())
374}
375
376/// Pretty-print the execution plan in a readable format.
377#[allow(clippy::too_many_arguments)]
378fn print_execution_plan<N: Network>(
379    private_key: &PrivateKey<N>,
380    address: &Address<N>,
381    endpoint: &str,
382    network: &NetworkName,
383    program_name: &str,
384    function_name: &str,
385    is_local: bool,
386    priority_fee: u64,
387    fee_record: bool,
388    action: &TransactionAction,
389    consensus_version: ConsensusVersion,
390) {
391    println!("\n{}", "πŸš€ Execution Plan Summary".bold().underline());
392    println!("{}", "──────────────────────────────────────────────".dimmed());
393
394    println!("{}", "πŸ”§ Configuration:".bold());
395    println!("  {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
396    println!("  {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
397    println!("  {:20}{}", "Endpoint:", endpoint.yellow());
398    println!("  {:20}{}", "Network:", network.to_string().yellow());
399    println!("  {:20}{}", "Consensus Version:", (consensus_version as u8).to_string().yellow());
400
401    println!("\n{}", "🎯 Execution Target:".bold());
402    println!("  {:16}{}", "Program:", program_name.cyan());
403    println!("  {:16}{}", "Function:", function_name.cyan());
404    println!("  {:16}{}", "Source:", if is_local { "local" } else { "remote" });
405
406    println!("\n{}", "πŸ’Έ Fee Info:".bold());
407    println!("  {:16}{}", "Priority Fee:", format!("{} ΞΌcredits", priority_fee).green());
408    println!("  {:16}{}", "Fee Record:", if fee_record { "yes" } else { "no (public fee)" });
409
410    println!("\n{}", "βš™οΈ Actions:".bold());
411    if !is_local {
412        println!("  - Program and its dependencies will be downloaded from the network.");
413    }
414    if action.print {
415        println!("  - Transaction will be printed to the console.");
416    } else {
417        println!("  - Transaction will NOT be printed to the console.");
418    }
419    if let Some(path) = &action.save {
420        println!("  - Transaction will be saved to {}", path.bold());
421    } else {
422        println!("  - Transaction will NOT be saved to a file.");
423    }
424    if action.broadcast {
425        println!("  - Transaction will be broadcast to {}", endpoint.bold());
426    } else {
427        println!("  - Transaction will NOT be broadcast to the network.");
428    }
429    println!("{}", "──────────────────────────────────────────────\n".dimmed());
430}
431
432/// Pretty‑print execution statistics without a table, using the same UI
433/// conventions as `print_deployment_plan`.
434fn print_execution_stats<N: Network>(
435    vm: &VM<N, ConsensusMemory<N>>,
436    program_name: &str,
437    execution: &Execution<N>,
438    priority_fee: Option<u64>,
439    consensus_version: ConsensusVersion,
440) -> Result<()> {
441    use colored::*;
442
443    // ── Gather cost components ────────────────────────────────────────────
444    let (base_fee, (storage_cost, execution_cost)) = if consensus_version == ConsensusVersion::V1 {
445        execution_cost_v1(&vm.process().read(), execution)?
446    } else {
447        execution_cost_v2(&vm.process().read(), execution)?
448    };
449
450    let base_cr = base_fee as f64 / 1_000_000.0;
451    let prio_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0;
452    let total_cr = base_cr + prio_cr;
453
454    // ── Header ────────────────────────────────────────────────────────────
455    println!("\n{} {}", "πŸ“Š Execution Summary for".bold(), program_name.bold());
456    println!("{}", "──────────────────────────────────────────────".dimmed());
457
458    // ── Cost breakdown ────────────────────────────────────────────────────
459    println!("{}", "πŸ’° Cost Breakdown (credits)".bold());
460    println!("  {:22}{}{:.6}", "Transaction Storage:".cyan(), "".yellow(), storage_cost as f64 / 1_000_000.0);
461    println!("  {:22}{}{:.6}", "On‑chain Execution:".cyan(), "".yellow(), execution_cost as f64 / 1_000_000.0);
462    println!("  {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_cr);
463    println!("  {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_cr);
464
465    // ── Footer rule ───────────────────────────────────────────────────────
466    println!("{}", "──────────────────────────────────────────────".dimmed());
467    Ok(())
468}