Skip to main content

leo_lang/cli/commands/
run.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 leo_ast::{NetworkName, TEST_PRIVATE_KEY};
21use leo_package::{Package, ProgramData};
22
23use aleo_std::StorageMode;
24
25use clap::Parser;
26use std::path::PathBuf;
27
28#[cfg(not(feature = "only_testnet"))]
29use snarkvm::circuit::{AleoCanaryV0, AleoV0};
30use snarkvm::{
31    circuit::{Aleo, AleoTestnetV0},
32    prelude::{
33        Identifier,
34        ProgramID,
35        VM,
36        store::{ConsensusStore, helpers::memory::ConsensusMemory},
37    },
38};
39
40/// Run the Leo program with the given inputs, without generating a proof.
41#[derive(Parser, Debug)]
42pub struct LeoRun {
43    #[clap(
44        name = "NAME",
45        help = "The name of the function to execute, e.g `helloworld.aleo::main` or `main`.",
46        default_value = "main"
47    )]
48    pub(crate) name: String,
49    #[clap(
50        name = "INPUTS",
51        help = "The program inputs e.g. `1u32`, `record1...` (record ciphertext), or `{ owner: ...}` "
52    )]
53    pub(crate) inputs: Vec<String>,
54    #[clap(flatten)]
55    pub(crate) env_override: EnvOptions,
56    #[clap(flatten)]
57    pub(crate) key_override: PrivateKeyOptions,
58    #[clap(flatten)]
59    pub(crate) build_options: BuildOptions,
60    #[clap(
61        long,
62        value_name = "DIR",
63        help = "Directory containing local .aleo imports when --path points to an Aleo bytecode file; other imports are fetched from the network"
64    )]
65    pub(crate) imports_dir: Option<PathBuf>,
66    #[clap(
67        long = "with",
68        help = "Additional programs to load into the VM (comma-separated). \
69            If a path exists locally, it is read as an .aleo bytecode file; \
70            otherwise it is fetched from the network endpoint.",
71        value_delimiter = ','
72    )]
73    pub(crate) with: Vec<String>,
74}
75
76impl Command for LeoRun {
77    type Input = Option<Package>;
78    type Output = RunOutput;
79
80    fn log_span(&self) -> Span {
81        tracing::span!(tracing::Level::INFO, "Leo")
82    }
83
84    fn prelude(&self, context: Context) -> Result<Self::Input> {
85        // Get the path to the current directory.
86        let path = context.dir()?;
87        // Get the path to the home directory.
88        let home_path = context.home()?;
89        if path.extension().and_then(|extension| extension.to_str()) == Some("aleo") {
90            if context.package_filter.is_some() {
91                return Err(crate::errors::custom("`--package` cannot be used with an Aleo bytecode file.").into());
92            }
93            let network = match get_network(&self.env_override.network) {
94                Ok(network) => network,
95                Err(_) => {
96                    println!("⚠️ No network specified, defaulting to 'testnet'.");
97                    NetworkName::TestnetV0
98                }
99            };
100            let endpoint = match get_endpoint(&self.env_override.endpoint) {
101                Ok(endpoint) => endpoint,
102                Err(_) => {
103                    println!("⚠️ No endpoint specified, defaulting to '{DEFAULT_ENDPOINT}'.");
104                    DEFAULT_ENDPOINT.to_string()
105                }
106            };
107            return Package::from_aleo_file(
108                path,
109                home_path,
110                self.imports_dir.as_deref(),
111                self.build_options.no_cache,
112                self.build_options.no_local,
113                Some(network),
114                Some(&endpoint),
115                self.env_override.network_retries,
116            )
117            .map(Some);
118        }
119        if self.imports_dir.is_some() {
120            return Err(
121                crate::errors::custom("`--imports-dir` requires `--path` to point to an Aleo bytecode file.").into()
122            );
123        }
124        // If the current directory is a valid Leo package, then build it.
125        if Package::from_directory_no_graph(
126            path,
127            home_path,
128            self.env_override.network,
129            self.env_override.endpoint.as_deref(),
130            self.env_override.network_retries,
131        )
132        .is_ok()
133        {
134            let package =
135                LeoBuild { env_override: self.env_override.clone(), options: self.build_options.clone(), rename: None }
136                    .execute(context)?;
137            // Return the package.
138            Ok(Some(package))
139        } else {
140            Ok(None)
141        }
142    }
143
144    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
145        // Libraries cannot be run.
146        if let Some(package) = &input
147            && package.compilation_units.last().is_some_and(|p| p.kind.is_library())
148        {
149            return Err(crate::errors::custom("Cannot run a library package. Only programs can be run.").into());
150        }
151
152        // Get the network, defaulting to `TestnetV0` if none is specified.
153        let network = match get_network(&self.env_override.network) {
154            Ok(network) => network,
155            Err(_) => {
156                println!("⚠️ No network specified, defaulting to 'testnet'.");
157                NetworkName::TestnetV0
158            }
159        };
160
161        // Handle each network with the appropriate parameterization.
162        match network {
163            NetworkName::TestnetV0 => handle_run::<AleoTestnetV0>(self, context, network, input),
164            NetworkName::MainnetV0 => {
165                #[cfg(feature = "only_testnet")]
166                panic!("Mainnet chosen with only_testnet feature");
167                #[cfg(not(feature = "only_testnet"))]
168                handle_run::<AleoV0>(self, context, network, input)
169            }
170            NetworkName::CanaryV0 => {
171                #[cfg(feature = "only_testnet")]
172                panic!("Canary chosen with only_testnet feature");
173                #[cfg(not(feature = "only_testnet"))]
174                handle_run::<AleoCanaryV0>(self, context, network, input)
175            }
176        }
177    }
178}
179
180// A helper function to handle the `run` command.
181fn handle_run<A: Aleo>(
182    command: LeoRun,
183    context: Context,
184    network: NetworkName,
185    package: Option<Package>,
186) -> Result<<LeoRun as Command>::Output> {
187    // Get the private key, defaulting to a test key.
188    let private_key = match get_private_key::<A::Network>(&command.key_override.private_key) {
189        Ok(private_key) => private_key,
190        Err(_) => {
191            println!("⚠️ No valid private key specified, defaulting to '{TEST_PRIVATE_KEY}'.");
192            PrivateKey::<A::Network>::from_str(TEST_PRIVATE_KEY).expect("Failed to parse the test private key")
193        }
194    };
195
196    // Parse the <NAME> into an optional program name and a function name.
197    // If only a function name is provided, then use the program name from the package.
198    let (program_name, function_name) = match command.name.split_once('/').or_else(|| command.name.split_once("::")) {
199        Some((program_name, function_name)) => (program_name.to_string(), function_name.to_string()),
200        None => match &package {
201            Some(package) => (
202                package
203                    .compilation_units
204                    .last()
205                    .expect("There must be at least one program in a Leo package")
206                    .name
207                    .to_string(),
208                command.name,
209            ),
210            None => {
211                return Err(crate::errors::custom(format!(
212                    "Running `leo execute {} ...`, without an explicit program name requires that your current working directory is a valid Leo project.",
213                    command.name
214                )).into());
215            }
216        },
217    };
218
219    // Parse the program name as a `ProgramID`.
220    let program_id = ProgramID::<A::Network>::from_str(&program_name)
221        .map_err(|e| crate::errors::custom(format!("Failed to parse program name: {e}")))?;
222    // Parse the function name as an `Identifier`.
223    let function_id = Identifier::<A::Network>::from_str(&function_name)
224        .map_err(|e| crate::errors::custom(format!("Failed to parse function name: {e}")))?;
225
226    // Get all the dependencies in the package if it exists.
227    // Get the programs and optional manifests for all programs.
228    let programs = if let Some(package) = &package {
229        // Get the program names and their bytecode.
230        package
231            .compilation_units
232            .iter()
233            .clone()
234            .filter(|unit| !unit.kind.is_library())
235            .map(|unit| {
236                let program_id = ProgramID::<A::Network>::from_str(&format!("{}", unit.name))
237                    .map_err(|e| crate::errors::custom(format!("Failed to parse program ID: {e}")))?;
238                match &unit.data {
239                    ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string(), unit.edition)),
240                    ProgramData::SourcePath { .. } => {
241                        // Get the path to the built bytecode.
242                        let bytecode_path = package.unit_bytecode_path(&unit.name.to_string());
243                        // Fetch the bytecode.
244                        let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| {
245                            crate::errors::custom(format!(
246                                "Failed to read bytecode at {}: {e}",
247                                bytecode_path.display()
248                            ))
249                        })?;
250                        // Return the bytecode and the manifest.
251                        Ok((program_id, bytecode, unit.edition))
252                    }
253                }
254            })
255            .collect::<Result<Vec<_>>>()?
256    } else {
257        Vec::new()
258    };
259
260    // Parse the program strings into AVM programs.
261    let mut programs = programs
262        .into_iter()
263        .map(|(_, bytecode, edition)| {
264            // Parse the program.
265            let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
266                .map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
267            // Return the program and its name.
268            Ok((program, edition))
269        })
270        .collect::<Result<Vec<_>>>()?;
271
272    // Determine whether the program is local or remote.
273    let is_local = programs.iter().any(|(program, _)| program.id() == &program_id);
274
275    // If the program is local, then check that the function exists.
276    if is_local {
277        let program = &programs
278            .iter()
279            .find(|(program, _)| program.id() == &program_id)
280            .expect("Program should exist since it is local")
281            .0;
282        // `view fn`s are read-only finalize-store reads; they have no transition semantics, so
283        // `leo run` (an in-memory simulation against an empty finalize store) can't produce a
284        // meaningful result. Detect this up front instead of falling through to the
285        // "function does not exist" branch below, which would mislead the user.
286        if program.contains_view(&function_id) {
287            return Err(crate::errors::custom(format!(
288                "`{function_name}` is a `view fn`; views are read-only and cannot be simulated by `leo run` \
289                 (which evaluates against an empty in-memory finalize store)."
290            ))
291            .into());
292        }
293        if !program.contains_function(&function_id) {
294            return Err(crate::errors::custom(format!(
295                "Function `{function_name}` does not exist in program `{program_name}`."
296            ))
297            .into());
298        }
299    }
300
301    let inputs =
302        command.inputs.into_iter().map(|string| parse_input(&string, &private_key)).collect::<Result<Vec<_>>>()?;
303
304    // Initialize an RNG.
305    let rng = &mut rand::rng();
306
307    // Initialize a new VM.
308    let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
309
310    // If the program is not local, then download it and its dependencies for the network.
311    // Note: The dependencies are downloaded in "post-order" (child before parent).
312    if !is_local {
313        // Get the endpoint, accounting for overrides.
314        let endpoint = get_endpoint(&command.env_override.endpoint)?;
315        println!("⬇️ Downloading {program_name} and its dependencies from {endpoint}...");
316        // Load the programs from the network.
317        programs = load_latest_programs_from_network(
318            &context,
319            program_id,
320            network,
321            &endpoint,
322            command.env_override.network_retries,
323        )?;
324    };
325
326    // Add the programs to the VM.
327    println!("\n➕Adding programs to the VM in the following order:");
328    let programs_and_editions = programs
329        .into_iter()
330        .map(|(program, edition)| {
331            print_program_source(&program.id().to_string(), edition);
332            let edition = edition.unwrap_or(LOCAL_PROGRAM_DEFAULT_EDITION);
333            (program, edition)
334        })
335        .collect::<Vec<_>>();
336    vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
337
338    // Load any extra programs specified via `--with`.
339    if !command.with.is_empty() {
340        let endpoint = get_endpoint(&command.env_override.endpoint).ok();
341        load_extra_programs_into_vm::<A::Network>(
342            &command.with,
343            &vm,
344            &context,
345            network,
346            endpoint.as_deref(),
347            command.env_override.network_retries,
348        )?;
349    }
350
351    // Evaluate the program and get a response.
352    let authorization = vm
353        .authorize(&private_key, program_id, function_id, inputs.iter(), rng)
354        .map_err(|e| crate::errors::custom(format!("Failed to authorize execution: {e}")))?;
355    let response = vm
356        .process()
357        .evaluate::<A>(authorization)
358        .map_err(|e| crate::errors::custom(format!("Failed to evaluate program: {e}")))?;
359
360    // Collect outputs.
361    let outputs: Vec<String> = response.outputs().iter().map(|o| o.to_string()).collect();
362
363    // Print the response.
364    match outputs.len() {
365        0 => (),
366        1 => println!("\n➡️  Output\n"),
367        _ => println!("\n➡️  Outputs\n"),
368    };
369    for output in &outputs {
370        println!(" • {output}");
371    }
372
373    Ok(RunOutput { program: program_id.to_string(), function: function_id.to_string(), outputs })
374}