leo_lang/cli/commands/
run.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 leo_ast::NetworkName;
20use leo_package::{Package, ProgramData, TEST_PRIVATE_KEY};
21
22use aleo_std::StorageMode;
23
24use clap::Parser;
25
26#[cfg(not(feature = "only_testnet"))]
27use snarkvm::circuit::{AleoCanaryV0, AleoV0};
28use snarkvm::{
29    circuit::{Aleo, AleoTestnetV0},
30    prelude::{
31        Identifier,
32        ProgramID,
33        VM,
34        store::{ConsensusStore, helpers::memory::ConsensusMemory},
35    },
36};
37
38/// Run the Leo program with the given inputs, without generating a proof.
39#[derive(Parser, Debug)]
40pub struct LeoRun {
41    #[clap(
42        name = "NAME",
43        help = "The name of the function to execute, e.g `helloworld.aleo/main` or `main`.",
44        default_value = "main"
45    )]
46    pub(crate) name: String,
47    #[clap(
48        name = "INPUTS",
49        help = "The program inputs e.g. `1u32`, `record1...` (record ciphertext), or `{ owner: ...}` "
50    )]
51    pub(crate) inputs: Vec<String>,
52    #[clap(flatten)]
53    pub(crate) env_override: EnvOptions,
54    #[clap(flatten)]
55    pub(crate) build_options: BuildOptions,
56}
57
58impl Command for LeoRun {
59    type Input = Option<Package>;
60    type Output = ();
61
62    fn log_span(&self) -> Span {
63        tracing::span!(tracing::Level::INFO, "Leo")
64    }
65
66    fn prelude(&self, context: Context) -> Result<Self::Input> {
67        // Get the path to the current directory.
68        let path = context.dir()?;
69        // Get the path to the home directory.
70        let home_path = context.home()?;
71        // If the current directory is a valid Leo package, then build it.
72        if Package::from_directory_no_graph(
73            path,
74            home_path,
75            self.env_override.network,
76            self.env_override.endpoint.as_deref(),
77        )
78        .is_ok()
79        {
80            let package = LeoBuild { env_override: self.env_override.clone(), options: self.build_options.clone() }
81                .execute(context)?;
82            // Return the package.
83            Ok(Some(package))
84        } else {
85            Ok(None)
86        }
87    }
88
89    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
90        // Get the network, defaulting to `TestnetV0` if none is specified.
91        let network = match get_network(&self.env_override.network) {
92            Ok(network) => network,
93            Err(_) => {
94                println!("⚠️ No network specified, defaulting to 'testnet'.");
95                NetworkName::TestnetV0
96            }
97        };
98
99        // Handle each network with the appropriate parameterization.
100        match network {
101            NetworkName::TestnetV0 => handle_run::<AleoTestnetV0>(self, context, network, input),
102            NetworkName::MainnetV0 => {
103                #[cfg(feature = "only_testnet")]
104                panic!("Mainnet chosen with only_testnet feature");
105                #[cfg(not(feature = "only_testnet"))]
106                handle_run::<AleoV0>(self, context, network, input)
107            }
108            NetworkName::CanaryV0 => {
109                #[cfg(feature = "only_testnet")]
110                panic!("Canary chosen with only_testnet feature");
111                #[cfg(not(feature = "only_testnet"))]
112                handle_run::<AleoCanaryV0>(self, context, network, input)
113            }
114        }
115    }
116}
117
118// A helper function to handle the `run` command.
119fn handle_run<A: Aleo>(
120    command: LeoRun,
121    context: Context,
122    network: NetworkName,
123    package: Option<Package>,
124) -> Result<<LeoRun as Command>::Output> {
125    // Get the private key, defaulting to a test key.
126    let private_key = match get_private_key::<A::Network>(&command.env_override.private_key) {
127        Ok(private_key) => private_key,
128        Err(_) => {
129            println!("⚠️ No valid private key specified, defaulting to '{TEST_PRIVATE_KEY}'.");
130            PrivateKey::<A::Network>::from_str(TEST_PRIVATE_KEY).expect("Failed to parse the test private key")
131        }
132    };
133
134    // Parse the <NAME> into an optional program name and a function name.
135    // If only a function name is provided, then use the program name from the package.
136    let (program_name, function_name) = match command.name.split_once('/') {
137        Some((program_name, function_name)) => (program_name.to_string(), function_name.to_string()),
138        None => match &package {
139            Some(package) => (
140                format!(
141                    "{}.aleo",
142                    package.programs.last().expect("There must be at least one program in a Leo package").name
143                ),
144                command.name,
145            ),
146            None => {
147                return Err(CliError::custom(format!(
148                    "Running `leo execute {} ...`, without an explicit program name requires that your current working directory is a valid Leo project.",
149                    command.name
150                )).into());
151            }
152        },
153    };
154
155    // Parse the program name as a `ProgramID`.
156    let program_id = ProgramID::<A::Network>::from_str(&program_name)
157        .map_err(|e| CliError::custom(format!("Failed to parse program name: {e}")))?;
158    // Parse the function name as an `Identifier`.
159    let function_id = Identifier::<A::Network>::from_str(&function_name)
160        .map_err(|e| CliError::custom(format!("Failed to parse function name: {e}")))?;
161
162    // Get all the dependencies in the package if it exists.
163    // Get the programs and optional manifests for all programs.
164    let programs = if let Some(package) = &package {
165        // Get the package directories.
166        let build_directory = package.build_directory();
167        let imports_directory = package.imports_directory();
168        let source_directory = package.source_directory();
169        // Get the program names and their bytecode.
170        package
171            .programs
172            .iter()
173            .clone()
174            .map(|program| {
175                let program_id = ProgramID::<A::Network>::from_str(&format!("{}.aleo", program.name))
176                    .map_err(|e| CliError::custom(format!("Failed to parse program ID: {e}")))?;
177                match &program.data {
178                    ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string(), program.edition)),
179                    ProgramData::SourcePath { source, .. } => {
180                        // Get the path to the built bytecode.
181                        let bytecode_path = if source.as_path() == source_directory.join("main.leo") {
182                            build_directory.join("main.aleo")
183                        } else {
184                            imports_directory.join(format!("{}.aleo", program.name))
185                        };
186                        // Fetch the bytecode.
187                        let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| {
188                            CliError::custom(format!("Failed to read bytecode at {}: {e}", bytecode_path.display()))
189                        })?;
190                        // Return the bytecode and the manifest.
191                        Ok((program_id, bytecode, program.edition))
192                    }
193                }
194            })
195            .collect::<Result<Vec<_>>>()?
196    } else {
197        Vec::new()
198    };
199
200    // Parse the program strings into AVM programs.
201    let mut programs = programs
202        .into_iter()
203        .map(|(_, bytecode, edition)| {
204            // Parse the program.
205            let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
206                .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
207            // Return the program and its name.
208            Ok((program, edition))
209        })
210        .collect::<Result<Vec<_>>>()?;
211
212    // Determine whether the program is local or remote.
213    let is_local = programs.iter().any(|(program, _)| program.id() == &program_id);
214
215    // If the program is local, then check that the function exists.
216    if is_local {
217        let program = &programs
218            .iter()
219            .find(|(program, _)| program.id() == &program_id)
220            .expect("Program should exist since it is local")
221            .0;
222        if !program.contains_function(&function_id) {
223            return Err(CliError::custom(format!(
224                "Function `{function_name}` does not exist in program `{program_name}`."
225            ))
226            .into());
227        }
228    }
229
230    let inputs =
231        command.inputs.into_iter().map(|string| parse_input(&string, &private_key)).collect::<Result<Vec<_>>>()?;
232
233    // Initialize an RNG.
234    let rng = &mut rand::thread_rng();
235
236    // Initialize a new VM.
237    let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
238
239    // If the program is not local, then download it and its dependencies for the network.
240    // Note: The dependencies are downloaded in "post-order" (child before parent).
241    if !is_local {
242        // Get the endpoint, accounting for overrides.
243        let endpoint = get_endpoint(&command.env_override.endpoint)?;
244        println!("⬇️ Downloading {program_name} and its dependencies from {endpoint}...");
245        // Load the programs from the network.
246        programs = load_latest_programs_from_network(&context, program_id, network, &endpoint)?;
247    };
248
249    // Add the programs to the VM.
250    println!("\n➕Adding programs to the VM in the following order:");
251    let programs_and_editions = programs
252        .into_iter()
253        .map(|(program, edition)| {
254            // 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.
255            let edition = edition.unwrap_or(1);
256            // Get the program ID.
257            let id = program.id().to_string();
258            // Print the program ID and edition.
259            match id == "credits.aleo" {
260                true => println!("  - {id} (already included)"),
261                false => println!("  - {id} (edition: {edition})"),
262            }
263            (program, edition)
264        })
265        .collect::<Vec<_>>();
266    vm.process().write().add_programs_with_editions(&programs_and_editions)?;
267
268    // Evaluate the program and get a response.
269    let authorization = vm.authorize(&private_key, program_id, function_id, inputs.iter(), rng)?;
270    let response = vm.process().read().evaluate::<A>(authorization)?;
271
272    // Print the response.
273    match response.outputs().len() {
274        0 => (),
275        1 => println!("\n➡️  Output\n"),
276        _ => println!("\n➡️  Outputs\n"),
277    };
278    for output in response.outputs() {
279        println!(" • {output}");
280    }
281
282    Ok(())
283}