Skip to main content

leo_lang/cli/commands/
execute.rs

1// Copyright (C) 2019-2026 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 super::common::load_extra_programs_into_vm;
20use check_transaction::TransactionStatus;
21use leo_ast::NetworkName;
22use leo_package::{Package, ProgramData, fetch_program_from_network, retry_network_call};
23
24use aleo_std::StorageMode;
25use rand::{CryptoRng, Rng};
26use snarkvm::prelude::{
27    Authorization,
28    Execution,
29    Fee,
30    Field,
31    Itertools,
32    Network,
33    Program,
34    execution_cost,
35    execution_cost_for_authorization,
36};
37
38use clap::Parser;
39use colored::*;
40use std::{convert::TryFrom, path::PathBuf};
41
42#[cfg(not(feature = "only_testnet"))]
43use snarkvm::circuit::{AleoCanaryV0, AleoV0};
44use snarkvm::{
45    circuit::{Aleo, AleoTestnetV0},
46    prelude::{
47        ConsensusVersion,
48        Identifier,
49        ProgramID,
50        VM,
51        query::{Query as SnarkVMQuery, QueryTrait},
52        store::{
53            ConsensusStore,
54            helpers::memory::{BlockMemory, ConsensusMemory},
55        },
56    },
57};
58
59/// Build, Prove and Run Leo program with inputs
60#[derive(Parser, Debug)]
61pub struct LeoExecute {
62    #[clap(
63        name = "NAME",
64        help = "The name of the function to execute, e.g `helloworld.aleo::main` or `main`.",
65        default_value = "main"
66    )]
67    name: String,
68    #[clap(
69        name = "INPUTS",
70        help = "The program inputs e.g. `1u32`, `record1...` (record ciphertext), or `{ owner: ...}` "
71    )]
72    inputs: Vec<String>,
73    #[clap(long, help = "Skips proving.")]
74    pub(crate) skip_execute_proof: bool,
75    #[clap(flatten)]
76    pub(crate) fee_options: FeeOptions,
77    #[clap(flatten)]
78    pub(crate) action: TransactionAction,
79    #[clap(flatten)]
80    pub(crate) env_override: EnvOptions,
81    #[clap(flatten)]
82    pub(crate) key_override: PrivateKeyOptions,
83    #[clap(flatten)]
84    pub(crate) consensus_override: ConsensusOptions,
85    #[clap(flatten)]
86    pub(crate) extra: ExtraOptions,
87    #[clap(flatten)]
88    build_options: BuildOptions,
89    #[clap(
90        long,
91        value_name = "DIR",
92        help = "Directory containing local .aleo imports when --path points to an Aleo bytecode file; other imports are fetched from the network"
93    )]
94    pub(crate) imports_dir: Option<PathBuf>,
95    #[clap(
96        long = "with",
97        help = "Additional programs to load into the VM (comma-separated). \
98        If a path exists locally, it is read as an .aleo bytecode file; \
99        otherwise it is fetched from the network endpoint.",
100        value_delimiter = ','
101    )]
102    pub(crate) with: Vec<String>,
103}
104
105impl Command for LeoExecute {
106    type Input = Option<Package>;
107    type Output = ExecuteOutput;
108
109    fn log_span(&self) -> Span {
110        tracing::span!(tracing::Level::INFO, "Leo")
111    }
112
113    fn prelude(&self, context: Context) -> Result<Self::Input> {
114        // Get the path to the current directory.
115        let path = context.dir()?;
116        // Get the path to the home directory.
117        let home_path = context.home()?;
118        // Get the network, accounting for overrides.
119        let network = get_network(&self.env_override.network)?;
120        // Get the endpoint, accounting for overrides.
121        let endpoint = get_endpoint(&self.env_override.endpoint)?;
122        if path.extension().and_then(|extension| extension.to_str()) == Some("aleo") {
123            if context.package_filter.is_some() {
124                return Err(crate::errors::custom("`--package` cannot be used with an Aleo bytecode file.").into());
125            }
126            return Package::from_aleo_file(
127                path,
128                home_path,
129                self.imports_dir.as_deref(),
130                true,
131                self.build_options.no_local,
132                Some(network),
133                Some(&endpoint),
134                self.env_override.network_retries,
135            )
136            .map(Some);
137        }
138        if self.imports_dir.is_some() {
139            return Err(
140                crate::errors::custom("`--imports-dir` requires `--path` to point to an Aleo bytecode file.").into()
141            );
142        }
143        // If the current directory is a valid Leo package, then build it.
144        if Package::from_directory_no_graph(
145            path,
146            home_path,
147            Some(network),
148            Some(&endpoint),
149            self.env_override.network_retries,
150        )
151        .is_ok()
152        {
153            let package = LeoBuild {
154                env_override: self.env_override.clone(),
155                options: {
156                    let mut options = self.build_options.clone();
157                    options.no_cache = true;
158                    options
159                },
160                rename: None,
161            }
162            .execute(context)?;
163            // Return the package.
164            Ok(Some(package))
165        } else {
166            Ok(None)
167        }
168    }
169
170    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
171        // Libraries cannot be executed.
172        if let Some(package) = &input
173            && package.compilation_units.last().is_some_and(|p| p.kind.is_library())
174        {
175            return Err(
176                crate::errors::custom("Cannot execute a library package. Only programs can be executed.").into()
177            );
178        }
179
180        // Get the network, accounting for overrides.
181        let network = get_network(&self.env_override.network)?;
182        // Handle each network with the appropriate parameterization.
183        match network {
184            NetworkName::TestnetV0 => handle_execute::<AleoTestnetV0>(self, context, network, input),
185            NetworkName::MainnetV0 => {
186                #[cfg(feature = "only_testnet")]
187                panic!("Mainnet chosen with only_testnet feature");
188                #[cfg(not(feature = "only_testnet"))]
189                handle_execute::<AleoV0>(self, context, network, input)
190            }
191            NetworkName::CanaryV0 => {
192                #[cfg(feature = "only_testnet")]
193                panic!("Canary chosen with only_testnet feature");
194                #[cfg(not(feature = "only_testnet"))]
195                handle_execute::<AleoCanaryV0>(self, context, network, input)
196            }
197        }
198    }
199}
200
201// A helper function to handle the `execute` command.
202fn handle_execute<A: Aleo>(
203    command: LeoExecute,
204    context: Context,
205    network: NetworkName,
206    package: Option<Package>,
207) -> Result<<LeoExecute as Command>::Output> {
208    // Get the private key and associated address, accounting for overrides.
209    let private_key = get_private_key(&command.key_override.private_key)?;
210    let address = Address::<A::Network>::try_from(&private_key)
211        .map_err(|e| crate::errors::custom(format!("Failed to parse address: {e}")))?;
212
213    // Get the endpoint, accounting for overrides.
214    let endpoint = get_endpoint(&command.env_override.endpoint)?;
215
216    // Get whether the network is a devnet, accounting for overrides.
217    let is_devnet = get_is_devnet(command.consensus_override.devnet);
218
219    // If the consensus heights are provided, use them; otherwise, use the default heights for the network.
220    let consensus_heights = command
221        .consensus_override
222        .consensus_heights
223        .clone()
224        .unwrap_or_else(|| get_consensus_heights(network, is_devnet));
225    // Validate the provided consensus heights.
226    validate_consensus_heights(&consensus_heights)
227        .map_err(|e| crate::errors::custom(format!("Invalid consensus heights: {e}")))?;
228    // Print the consensus heights being used.
229    let consensus_heights_string = consensus_heights.iter().format(",").to_string();
230    println!(
231        "\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"
232    );
233
234    // Set the consensus heights in the environment.
235    #[allow(unsafe_code)]
236    unsafe {
237        // SAFETY:
238        //  - `CONSENSUS_VERSION_HEIGHTS` is only set once and is only read in `snarkvm::prelude::load_consensus_heights`.
239        //  - There are no concurrent threads running at this point in the execution.
240        // WHY:
241        //  - This is needed because there is no way to set the desired consensus heights for a particular `VM` instance
242        //    without using the environment variable `CONSENSUS_VERSION_HEIGHTS`. Which is itself read once, and stored in a `OnceLock`.
243        std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
244    }
245
246    // Parse the <NAME> into an optional program name and a function name.
247    // If only a function name is provided, then use the program name from the package.
248    let (program_name, function_name) = match command.name.split_once('/').or_else(|| command.name.split_once("::")) {
249        Some((program_name, function_name)) => (program_name.to_string(), function_name.to_string()),
250        None => match &package {
251            Some(package) => (
252                format!(
253                    "{}",
254                    package.compilation_units.last().expect("There must be at least one program in a Leo package").name
255                ),
256                command.name,
257            ),
258            None => {
259                return Err(crate::errors::custom(format!(
260                    "Running `leo execute {} ...`, without an explicit program name requires that your current working directory is a valid Leo project.",
261                    command.name
262                )).into());
263            }
264        },
265    };
266
267    // Parse the program name as a `ProgramID`.
268    let program_id = ProgramID::<A::Network>::from_str(&program_name)
269        .map_err(|e| crate::errors::custom(format!("Failed to parse program name: {e}")))?;
270    // Parse the function name as an `Identifier`.
271    let function_id = Identifier::<A::Network>::from_str(&function_name)
272        .map_err(|e| crate::errors::custom(format!("Failed to parse function name: {e}")))?;
273
274    // Get all the dependencies in the package if it exists.
275    // Get the programs and optional manifests for all programs.
276    let programs = if let Some(package) = &package {
277        // Get the program names and their bytecode.
278        package
279            .compilation_units
280            .iter()
281            .clone()
282            .filter(|unit| !unit.kind.is_library())
283            .map(|unit| {
284                let program_id = ProgramID::<A::Network>::from_str(&format!("{}", unit.name))
285                    .map_err(|e| crate::errors::custom(format!("Failed to parse program ID: {e}")))?;
286                match &unit.data {
287                    ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string(), unit.edition)),
288                    ProgramData::SourcePath { .. } => {
289                        // Get the path to the built bytecode.
290                        let bytecode_path = package.unit_bytecode_path(&unit.name.to_string());
291                        // Fetch the bytecode.
292                        let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| {
293                            crate::errors::custom(format!(
294                                "Failed to read bytecode at {}: {e}",
295                                bytecode_path.display()
296                            ))
297                        })?;
298                        // Return the bytecode and the manifest.
299                        Ok((program_id, bytecode, unit.edition))
300                    }
301                }
302            })
303            .collect::<Result<Vec<_>>>()?
304    } else {
305        Vec::new()
306    };
307
308    // Parse the program strings into AVM programs.
309    let mut programs = programs
310        .into_iter()
311        .map(|(_, bytecode, edition)| {
312            // Parse the program.
313            let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
314                .map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
315            // Return the program and its name.
316            Ok((program, edition))
317        })
318        .collect::<Result<Vec<_>>>()?;
319
320    // Determine whether the program is local or remote.
321    let is_local = programs.iter().any(|(program, _)| program.id() == &program_id);
322
323    // If the program is local, then check that the function exists.
324    if is_local {
325        let program = &programs
326            .iter()
327            .find(|(program, _)| program.id() == &program_id)
328            .expect("Program should exist since it is local")
329            .0;
330        // `view fn`s are read-only finalize-store reads; they produce no transition and no
331        // transaction, so `leo execute` (which broadcasts a fee-paid transaction) cannot target
332        // one. Detect this up front instead of falling through to the "function does not exist"
333        // branch below, which would mislead the user.
334        if program.contains_view(&function_id) {
335            return Err(crate::errors::custom(format!(
336                "`{function_name}` is a `view fn`; views are read-only and do not produce a transaction, \
337                 so they cannot be executed."
338            ))
339            .into());
340        }
341        if !program.contains_function(&function_id) {
342            return Err(crate::errors::custom(format!(
343                "Function `{function_name}` does not exist in program `{program_name}`."
344            ))
345            .into());
346        }
347    }
348
349    let inputs =
350        command.inputs.into_iter().map(|string| parse_input(&string, &private_key)).collect::<Result<Vec<_>>>()?;
351
352    // Get the first fee option.
353    let (priority_fee, record) =
354        parse_fee_options(&private_key, &command.fee_options, 1)?.into_iter().next().unwrap_or((None, None));
355
356    // Get the consensus version.
357    let consensus_version = get_consensus_version(
358        &command.extra.consensus_version,
359        &endpoint,
360        network,
361        &consensus_heights,
362        &context,
363        command.env_override.network_retries,
364    )?;
365
366    // Build the config for JSON output.
367    let config = Some(Config {
368        address: address.to_string(),
369        network: network.to_string(),
370        endpoint: Some(endpoint.clone()),
371        consensus_version: Some(consensus_version as u8),
372    });
373
374    // Initialize an RNG.
375    let rng = &mut rand::rng();
376
377    // Initialize a new VM.
378    let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
379
380    // Specify the query.
381    let query = SnarkVMQuery::<A::Network, BlockMemory<A::Network>>::from(
382        endpoint
383            .parse::<Uri>()
384            .map_err(|e| crate::errors::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
385    );
386
387    // If the program is not local, then download it and its dependencies for the network.
388    // Note: The dependencies are downloaded in "post-order" (child before parent).
389    if !is_local {
390        println!("⬇️ Downloading {program_name} and its dependencies from {endpoint}...");
391        programs = load_latest_programs_from_network(
392            &context,
393            program_id,
394            network,
395            &endpoint,
396            command.env_override.network_retries,
397        )?;
398    };
399
400    let warnings =
401        check_task_for_warnings(&endpoint, network, &programs, consensus_version, command.env_override.network_retries);
402
403    // Add the programs to the VM.
404    println!("\nβž•Adding programs to the VM in the following order:");
405    let programs_and_editions = programs
406        .into_iter()
407        .map(|(program, edition)| {
408            print_program_source(&program.id().to_string(), edition);
409            let edition = edition.unwrap_or(LOCAL_PROGRAM_DEFAULT_EDITION);
410            (program, edition)
411        })
412        .collect::<Vec<_>>();
413    vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
414
415    // Load any extra programs specified via `--with`.
416    if !command.with.is_empty() {
417        load_extra_programs_into_vm::<A::Network>(
418            &command.with,
419            &vm,
420            &context,
421            network,
422            Some(&endpoint),
423            command.env_override.network_retries,
424        )?;
425    }
426
427    println!("\nβš™οΈ Preparing calls for review: {program_name}/{function_name}...");
428    // Generate the authorization (the method differs based on skip_execute_proof).
429    let authorization = if command.skip_execute_proof {
430        vm.process()
431            .authorize::<A, _>(&private_key, &program_name, &function_name, inputs.iter(), rng)
432            .map_err(|e| anyhow::anyhow!("{e}"))?
433    } else {
434        vm.authorize(&private_key, &program_name, &function_name, inputs.iter(), rng)
435            .map_err(|e| anyhow::anyhow!("{e}"))?
436    };
437
438    print_execution_plan::<A::Network>(
439        &address,
440        &endpoint,
441        &network,
442        &program_name,
443        &function_name,
444        is_local,
445        priority_fee.unwrap_or(0),
446        record.is_some(),
447        &command.action,
448        consensus_version,
449        &warnings,
450        command.skip_execute_proof,
451        &authorization,
452    )?;
453
454    // Estimate and display execution cost.
455    let (estimated_cost, (est_storage, est_exec)) =
456        execution_cost_for_authorization(vm.process(), &authorization, consensus_version)?;
457    let stats = print_execution_cost_summary(&program_name, est_storage, est_exec, priority_fee);
458
459    // Keep this authorization local until the user approves its calls and cost.
460    if !confirm("Approve all calls and fees shown above?", command.extra.yes)? {
461        println!("❌ Execution aborted.");
462        return Ok(ExecuteOutput::default());
463    }
464
465    // Generate the transaction (the method differs based on skip_execute_proof).
466    let (output_name, output, response) = if command.skip_execute_proof {
467        // Get the state root, retrying on transient network failures.
468        let state_root = retry_network_call(command.env_override.network_retries, || query.current_state_root())?;
469
470        // Create an execution without the proof.
471        let execution = Execution::from(authorization.transitions().values().cloned(), state_root, None)?;
472
473        // Calculate the actual cost for fee authorization.
474        let (cost, _) = execution_cost(vm.process(), &execution, consensus_version)?;
475
476        // Generate the fee authorization.
477        let id = authorization.to_execution_id()?;
478        let fee_authorization =
479            authorize_fee::<A, _>(&vm, &private_key, record, cost, priority_fee.unwrap_or(0), id, rng)?;
480
481        // Create a fee transition without a proof.
482        let fee = Fee::from(fee_authorization.transitions().into_iter().next().unwrap().1, state_root, None)?;
483
484        // Create the transaction.
485        let transaction = Transaction::from_execution(execution, Some(fee))?;
486
487        // Evaluate the transaction to get the response.
488        let response = vm.process().evaluate::<A>(authorization).map_err(|e| anyhow::anyhow!("{e}"))?;
489
490        ("transaction", Box::new(transaction), response)
491    } else {
492        // Determine if a fee is required.
493        let is_fee_required = !(authorization.is_split() || authorization.is_upgrade());
494        let is_priority_fee_declared = priority_fee.unwrap_or(0) > 0;
495
496        // Build fee authorization using the estimated cost.
497        let fee_authorization = if is_fee_required || is_priority_fee_declared {
498            let execution_id = authorization.to_execution_id()?;
499            Some(authorize_fee::<A, _>(
500                &vm,
501                &private_key,
502                record,
503                estimated_cost,
504                priority_fee.unwrap_or(0),
505                execution_id,
506                rng,
507            )?)
508        } else {
509            None
510        };
511
512        // Execute with the existing authorization (no re-authorization).
513        let (transaction, response) =
514            vm.execute_authorization_with_response(authorization, fee_authorization, Some(&query), rng)?;
515        ("transaction", Box::new(transaction), response)
516    };
517
518    let transaction = output.clone();
519
520    // Print the transaction.
521    // If the `print` option is set, print the execution transaction to the console.
522    // The transaction is printed in JSON format.
523    if command.action.print {
524        let transaction_json = serde_json::to_string_pretty(&output)
525            .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
526        println!("πŸ–¨οΈ Printing execution for {output_name}\n{transaction_json}");
527    }
528
529    // If the `save` option is set, save the execution transaction to a file in the specified directory.
530    // The file format is `program_name.execution.json`.
531    // The directory is created if it doesn't exist.
532    if let Some(path) = &command.action.save {
533        // Create the directory if it doesn't exist.
534        std::fs::create_dir_all(path).map_err(|e| crate::errors::custom(format!("Failed to create directory: {e}")))?;
535        // Save the transaction to a file.
536        let file_path = PathBuf::from(path).join(format!("{output_name}.execution.json"));
537        println!("πŸ’Ύ Saving execution for {output_name} at {}", file_path.display());
538        let transaction_json = serde_json::to_string_pretty(&output)
539            .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
540        std::fs::write(file_path, transaction_json)
541            .map_err(|e| crate::errors::custom(format!("Failed to write transaction to file: {e}")))?;
542    }
543
544    let mut broadcast_stats = None;
545
546    // Collect outputs.
547    let outputs: Vec<String> = response.outputs().iter().map(|o| o.to_string()).collect();
548
549    match outputs.len() {
550        0 => (),
551        1 => println!("\n➑️  Output\n"),
552        _ => println!("\n➑️  Outputs\n"),
553    };
554    for o in &outputs {
555        println!(" β€’ {o}");
556    }
557    println!();
558
559    // If the `broadcast` option is set, broadcast each deployment transaction to the network.
560    if command.action.broadcast {
561        println!("πŸ“‘ Broadcasting execution for {program_name}...");
562        // Get and confirm the fee with the user.
563        let mut fee_id = None;
564        let mut fee_transaction_id = None;
565        if let Some(fee) = transaction.fee_transition() {
566            // Most transactions will have fees, but some, like credits.aleo/upgrade executions, may not.
567            if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
568                println!("❌ Execution aborted.");
569                return Ok(ExecuteOutput {
570                    config: config.clone(),
571                    program: program_name.clone(),
572                    function: function_name.clone(),
573                    outputs,
574                    transaction_id: transaction.id().to_string(),
575                    stats: Some(stats),
576                    broadcast: None,
577                });
578            }
579            fee_id = Some(fee.id().to_string());
580            fee_transaction_id = Some(Transaction::from_fee(fee.clone())?.id().to_string());
581        }
582        let id = transaction.id().to_string();
583        let height_before =
584            check_transaction::current_height(&endpoint, network, command.env_override.network_retries)?;
585        // Broadcast the transaction to the network.
586        let (message, status) =
587            handle_broadcast(&format!("{endpoint}/{network}/transaction/broadcast"), &transaction, &program_name)?;
588
589        match status {
590            200..=299 => {
591                let tx_status = check_transaction::check_transaction_with_message(
592                    &id,
593                    fee_id.as_deref(),
594                    &endpoint,
595                    network,
596                    height_before + 1,
597                    command.extra.max_wait,
598                    command.extra.blocks_to_check,
599                    command.env_override.network_retries,
600                )?;
601                let confirmed = tx_status == Some(TransactionStatus::Accepted);
602                if confirmed {
603                    println!("βœ… Execution confirmed!");
604                }
605                broadcast_stats = Some(BroadcastStats {
606                    fee_id: fee_id.unwrap_or_default(),
607                    fee_transaction_id: fee_transaction_id.unwrap_or_default(),
608                    confirmed,
609                });
610            }
611            _ => {
612                println!("❌ Failed to broadcast execution: {message}.");
613            }
614        }
615    }
616
617    Ok(ExecuteOutput {
618        config,
619        program: program_name.clone(),
620        function: function_name.clone(),
621        outputs,
622        transaction_id: transaction.id().to_string(),
623        stats: Some(stats),
624        broadcast: broadcast_stats,
625    })
626}
627
628/// Authorize a fee transition using either a private record or public credits.
629fn authorize_fee<A: Aleo, R: Rng + CryptoRng>(
630    vm: &VM<A::Network, ConsensusMemory<A::Network>>,
631    private_key: &PrivateKey<A::Network>,
632    record: Option<Record<A::Network, Plaintext<A::Network>>>,
633    base_fee: u64,
634    priority_fee: u64,
635    execution_id: Field<A::Network>,
636    rng: &mut R,
637) -> Result<Authorization<A::Network>> {
638    match record {
639        None => vm.authorize_fee_public(private_key, base_fee, priority_fee, execution_id, rng),
640        Some(record) => vm.authorize_fee_private(private_key, record, base_fee, priority_fee, execution_id, rng),
641    }
642    .map_err(Into::into)
643}
644
645/// Check the execution task for warnings.
646/// The following properties are checked:
647///   - The component programs exist on the network and match the local ones.
648fn check_task_for_warnings<N: Network>(
649    endpoint: &str,
650    network: NetworkName,
651    programs: &[(Program<N>, Option<u16>)],
652    consensus_version: ConsensusVersion,
653    network_retries: u32,
654) -> Vec<String> {
655    let mut warnings = Vec::new();
656    for (program, _) in programs {
657        // Check if the program exists on the network.
658        if let Ok(remote_program) =
659            fetch_program_from_network(&program.id().to_string(), endpoint, network, network_retries)
660        {
661            // Parse the program.
662            let remote_program = match Program::<N>::from_str(&remote_program) {
663                Ok(program) => program,
664                Err(e) => {
665                    warnings.push(format!("Could not parse '{}' from the network. Error: {e}", program.id()));
666                    continue;
667                }
668            };
669            // Check if the program matches the local one.
670            if remote_program != *program {
671                warnings.push(format!(
672                    "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.",
673                    program.id()
674                ));
675            }
676        } else {
677            warnings.push(format!(
678                "The program '{}' does not exist on the network. You may use `leo deploy --broadcast` to deploy it.",
679                program.id()
680            ));
681        }
682    }
683    // Check for a consensus version mismatch.
684    if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network, network_retries) {
685        warnings.push(format!("{e}. In some cases, the execution may fail"));
686    }
687    warnings
688}
689
690/// Pretty-print the execution plan in a readable format.
691#[allow(clippy::too_many_arguments)]
692fn print_execution_plan<N: Network>(
693    address: &Address<N>,
694    endpoint: &str,
695    network: &NetworkName,
696    program_name: &str,
697    function_name: &str,
698    is_local: bool,
699    priority_fee: u64,
700    fee_record: bool,
701    action: &TransactionAction,
702    consensus_version: ConsensusVersion,
703    warnings: &[String],
704    skip_execute_proof: bool,
705    authorization: &Authorization<N>,
706) -> Result<()> {
707    println!("\n{}", "πŸš€ Execution Plan Summary".bold().underline());
708    println!("{}", "──────────────────────────────────────────────".dimmed());
709
710    println!("{}", "πŸ”§ Configuration:".bold());
711    println!("  {:20}{}", "Address:".cyan(), address.to_string().yellow());
712    println!("  {:20}{}", "Endpoint:", endpoint.escape_debug().to_string().yellow());
713    println!("  {:20}{}", "Network:", network.to_string().yellow());
714    println!("  {:20}{}", "Consensus Version:", (consensus_version as u8).to_string().yellow());
715
716    println!("\n{}", "🎯 Execution Target:".bold());
717    println!("  {:16}{}", "Program:", program_name.cyan());
718    println!("  {:16}{}", "Function:", function_name.cyan());
719    println!("  {:16}{}", "Source:", if is_local { "local" } else { "remote" });
720
721    println!("\n{}", "Calls to approve:".bold());
722    let transitions = authorization.transitions();
723    for (index, request) in authorization.to_vec_deque().iter().enumerate() {
724        println!("  Call {}: {}/{}", index + 1, request.program_id(), request.function_name());
725        println!("    Signer: {}", request.signer());
726        for (index, (input_id, value)) in request.input_ids().iter().zip(request.inputs()).enumerate() {
727            match value {
728                snarkvm::prelude::Value::Record(_) | snarkvm::prelude::Value::DynamicRecord(_) => {
729                    println!("    Input {}: <private record>", index + 1)
730                }
731                _ if matches!(input_id, snarkvm::prelude::InputID::Private(_)) => {
732                    println!("    Input {}: <private input>", index + 1)
733                }
734                _ => println!("    Input {}: {}", index + 1, value.to_string().escape_debug()),
735            }
736        }
737        // Requests and transitions have different traversal orders.
738        let transition = transitions
739            .values()
740            .find(|transition| transition.tcm() == request.tcm())
741            .ok_or_else(|| crate::errors::custom("Cannot review an execution with a missing transition."))?;
742        for future in transition.outputs().iter().filter_map(|output| output.future()) {
743            if future.program_id().to_string() == "credits.aleo"
744                && matches!(
745                    future.function_name().to_string().as_str(),
746                    "transfer_public" | "transfer_public_as_signer"
747                )
748                && let [
749                    snarkvm::prelude::Argument::Plaintext(debit),
750                    snarkvm::prelude::Argument::Plaintext(recipient),
751                    snarkvm::prelude::Argument::Plaintext(amount),
752                ] = future.arguments()
753            {
754                println!("    Debit: {}", debit.to_string().escape_debug());
755                println!("    Recipient: {}", recipient.to_string().escape_debug());
756                println!("    Amount: {} microcredits", amount.to_string().escape_debug());
757            }
758            println!("    Finalize: {}", future.to_string().escape_debug());
759        }
760    }
761    println!("  Final balances depend on on-chain state.");
762
763    println!("\n{}", "πŸ’Έ Fee Info:".bold());
764    println!("  {:16}{}", "Priority Fee:", format!("{priority_fee} ΞΌcredits").green());
765    println!("  {:16}{}", "Fee Record:", if fee_record { "yes" } else { "no (public fee)" });
766
767    println!("\n{}", "βš™οΈ Actions:".bold());
768    if !is_local {
769        println!("  - Program and its dependencies were loaded from network bytecode.");
770    }
771    if skip_execute_proof {
772        println!("  - A transaction will be generated, WITHOUT a proof.");
773    }
774    if action.print {
775        println!("  - Transaction will be printed to the console.");
776    } else {
777        println!("  - Transaction will NOT be printed to the console.");
778    }
779    if let Some(path) = &action.save {
780        println!("  - Transaction will be saved to {}", path.escape_debug().to_string().bold());
781    } else {
782        println!("  - Transaction will NOT be saved to a file.");
783    }
784    if action.broadcast {
785        println!("  - Transaction will be broadcast to {}", endpoint.escape_debug().to_string().bold());
786    } else {
787        println!("  - Transaction will NOT be broadcast to the network.");
788    }
789
790    // ── Warnings ─────────────────────────────────────────────────────────
791    if !warnings.is_empty() {
792        println!("\n{}", "⚠️ Warnings:".bold().red());
793        for warning in warnings {
794            println!("  β€’ {}", warning.escape_debug().to_string().dimmed());
795        }
796    }
797
798    println!("{}", "──────────────────────────────────────────────\n".dimmed());
799    Ok(())
800}
801
802/// Print execution cost summary and return stats for JSON output.
803fn print_execution_cost_summary(
804    program_name: &str,
805    storage_cost: u64,
806    execution_cost: u64,
807    priority_fee: Option<u64>,
808) -> ExecutionStats {
809    use colored::*;
810
811    let priority = priority_fee.unwrap_or(0);
812    let total = storage_cost + execution_cost + priority;
813    let stats = ExecutionStats { storage_cost, execution_cost, priority_fee: priority, total_cost: total };
814
815    println!("\n{} {}", "πŸ“Š Execution Cost Summary for".bold(), program_name.bold());
816    println!("{}", "──────────────────────────────────────────────".dimmed());
817    print!("{stats}");
818    println!("{}", "──────────────────────────────────────────────".dimmed());
819
820    stats
821}