Skip to main content

leo_lang/cli/commands/common/
util.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 snarkvm::prelude::{ConsensusVersion, Network, Program};
18
19/// Threshold percentage for program size warnings.
20const PROGRAM_SIZE_WARNING_THRESHOLD: usize = 90;
21
22/// Formats program size as KB and returns a warning message if approaching the limit.
23///
24/// Both `size` and `max_size` are expected in bytes.
25/// Returns `(size_kb, max_kb, warning)` where `warning` is `Some` if size exceeds 90% of max.
26pub fn format_program_size(size: usize, max_size: usize) -> (f64, f64, Option<String>) {
27    let size_kb = size as f64 / 1024.0;
28    let max_kb = max_size as f64 / 1024.0;
29    let percentage = (size as f64 / max_size as f64) * 100.0;
30
31    let warning = if size > max_size * PROGRAM_SIZE_WARNING_THRESHOLD / 100 {
32        Some(format!("approaching the size limit ({percentage:.1}% of {max_kb:.2} KB)"))
33    } else {
34        None
35    };
36
37    (size_kb, max_kb, warning)
38}
39
40/// Default edition for local programs during local operations (run, execute, synthesize).
41///
42/// Local programs don't have an on-chain edition yet. We default to edition 1 to avoid
43/// snarkVM's V8+ check that rejects edition 0 programs without constructors. That check
44/// is only relevant for deployed programs, not local development.
45pub const LOCAL_PROGRAM_DEFAULT_EDITION: leo_package::Edition = 1;
46
47/// Prints a program's ID and source (local or network edition).
48pub fn print_program_source(id: &str, edition: Option<leo_package::Edition>) {
49    match (id, edition) {
50        ("credits.aleo", _) => println!("  - {id} (already included)"),
51        (_, Some(e)) => println!("  - {id} (edition: {e})"),
52        (_, None) => println!("  - {id} (local)"),
53    }
54}
55
56/// Checks if any programs violate edition/constructor requirements.
57///
58/// Programs at edition 0 without a constructor cannot be executed after ConsensusVersion::V8.
59/// This check should be performed before attempting execution to provide a clear error message.
60///
61/// # Arguments
62/// * `programs` - Slice of (program, edition) tuples to check
63/// * `consensus_version` - The current consensus version
64/// * `action` - Description of the action being attempted (e.g., "deploy", "execute", "upgrade")
65///
66/// # Returns
67/// `Ok(())` if all programs pass the check, or an error with a descriptive message if not.
68pub fn check_edition_constructor_requirements<N: Network>(
69    programs: &[(Program<N>, leo_package::Edition)],
70    consensus_version: ConsensusVersion,
71    action: &str,
72) -> Result<(), leo_errors::Backtraced> {
73    // Only check for V8+ consensus versions.
74    if consensus_version < ConsensusVersion::V8 {
75        return Ok(());
76    }
77
78    for (program, edition) in programs {
79        // Programs at edition 0 without a constructor cannot be executed after V8.
80        if *edition == 0 && !program.contains_constructor() {
81            let id = program.id();
82            // Skip credits.aleo as it's a special case.
83            if id.to_string() != "credits.aleo" {
84                return Err(crate::errors::custom(format!(
85                    "Cannot {action} with dependency '{id}' (edition 0)\n\n\
86                    Programs at edition 0 without a constructor cannot be executed under \
87                    consensus version V8 or later (current: V{}).\n\n\
88                    The program '{id}' must be upgraded on-chain before it can be used.",
89                    consensus_version as u8
90                )));
91            }
92        }
93    }
94
95    Ok(())
96}
97
98/// Load additional programs specified by `--with` and add them to the VM.
99///
100/// Each entry is either a local `.aleo` file path (if it exists on disk)
101/// or a remote program name fetched from the network endpoint with transitive dependencies.
102pub fn load_extra_programs_into_vm<N: Network>(
103    entries: &[String],
104    vm: &snarkvm::prelude::VM<N, snarkvm::prelude::store::helpers::memory::ConsensusMemory<N>>,
105    context: &crate::cli::context::Context,
106    network: leo_ast::NetworkName,
107    endpoint: Option<&str>,
108    network_retries: u32,
109) -> leo_errors::Result<()> {
110    use snarkvm::prelude::ProgramID;
111    use std::{path::Path, str::FromStr};
112
113    let mut extras: Vec<(Program<N>, leo_package::Edition)> = Vec::new();
114
115    for entry in entries {
116        let path = Path::new(entry);
117        if path.is_file() {
118            println!("📂 Loading local program from {entry}...");
119            let bytecode = std::fs::read_to_string(path)
120                .map_err(|e| crate::errors::custom(format!("Failed to read program file '{entry}': {e}")))?;
121            let program = Program::<N>::from_str(&bytecode)
122                .map_err(|e| crate::errors::custom(format!("Failed to parse program from '{entry}': {e}")))?;
123            extras.push((program, LOCAL_PROGRAM_DEFAULT_EDITION));
124        } else if path.exists() {
125            return Err(crate::errors::custom(format!("'{entry}' exists but is not a file.")).into());
126        } else {
127            let endpoint = endpoint.ok_or_else(|| {
128                crate::errors::custom(format!(
129                    "'{entry}' is not a local file; fetching from the network requires --endpoint to be set."
130                ))
131            })?;
132            let name = if entry.ends_with(".aleo") { entry.clone() } else { format!("{entry}.aleo") };
133            println!("⬇️  Fetching remote program {name} and its dependencies from {endpoint}...");
134            let program_id = ProgramID::<N>::from_str(&name)
135                .map_err(|e| crate::errors::custom(format!("Failed to parse program ID '{name}': {e}")))?;
136            let fetched = super::query::load_latest_programs_from_network(
137                context,
138                program_id,
139                network,
140                endpoint,
141                network_retries,
142            )?;
143            extras.extend(fetched.into_iter().map(|(p, ed)| (p, ed.unwrap_or(LOCAL_PROGRAM_DEFAULT_EDITION))));
144        }
145    }
146
147    vm.process().lock().add_programs_with_editions(&extras)?;
148
149    Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use snarkvm::prelude::TestnetV0;
156    use std::str::FromStr;
157
158    #[test]
159    fn test_edition_constructor_error_message() {
160        // A program without a constructor at edition 0 should fail under V8+
161        let program = Program::<TestnetV0>::from_str(
162            "program old_program.aleo;\nfunction main:\n    input r0 as u32.public;\n    output r0 as u32.public;\n",
163        )
164        .unwrap();
165
166        let result = check_edition_constructor_requirements(&[(program, 0)], ConsensusVersion::V9, "deploy");
167        assert!(result.is_err());
168    }
169}