leo_package/lib.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
17//! This crate deals with Leo packages on the file system and network.
18//!
19//! The main type is `Package`, which deals with Leo packages on the local filesystem.
20//! A Leo package directory is intended to have a structure like this:
21//! .
22//! ├── program.json
23//! ├── build
24//! │ ├── imports
25//! │ │ └── credits.aleo
26//! │ └── main.aleo
27//! ├── outputs
28//! │ ├── program.TypeChecking.ast
29//! │ └── program.TypeChecking.json
30//! ├── src
31//! │ └── main.leo
32//! └── tests
33//! └── test_something.leo
34//!
35//! The file `program.json` is a manifest containing the program name, version, description,
36//! and license, together with information about its dependencies.
37//!
38//! Such a directory structure, together with a `.gitignore` file, may be created
39//! on the file system using `Package::initialize`.
40//! ```no_run
41//! # use leo_ast::NetworkName;
42//! # use leo_package::{Package};
43//! let path = Package::initialize("my_package", "path/to/parent", false).unwrap();
44//! ```
45//!
46//! `tests` is where unit test files may be placed.
47//!
48//! Given an existing directory with such a structure, a `Package` may be created from it with
49//! `Package::from_directory`:
50//! ```no_run
51//! # use leo_ast::NetworkName;
52//! use leo_package::Package;
53//! let package = Package::from_directory("path/to/package", "/home/me/.aleo", false, false, Some(NetworkName::TestnetV0), Some("http://localhost:3030")).unwrap();
54//! ```
55//! This will read the manifest and keep their data in `package.manifest`.
56//! It will also process dependencies and store them in topological order in `package.compilation_units`. This processing
57//! will involve fetching bytecode from the network for network dependencies.
58//! If the `no_cache` option (3rd parameter) is set to `true`, the package will not use the dependency cache.
59//! The endpoint and network are optional and are only needed if the package has network dependencies.
60//!
61//! If you want to simply read the manifest file without processing dependencies, use
62//! `Package::from_directory_no_graph`.
63//!
64//! `CompilationUnit` generally doesn't need to be created directly, as `Package` will create `CompilationUnit`s
65//! for the main program and all dependencies. However, if you'd like to fetch bytecode for
66//! a program, you can use `CompilationUnit::fetch`.
67
68#![forbid(unsafe_code)]
69
70use leo_ast::NetworkName;
71use leo_errors::{PackageError, Result, UtilError};
72use leo_span::Symbol;
73
74use std::path::Path;
75
76mod dependency;
77pub use dependency::*;
78
79mod location;
80pub use location::*;
81
82mod manifest;
83pub use manifest::*;
84
85mod package;
86pub use package::*;
87
88mod compilation_unit;
89pub use compilation_unit::*;
90
91pub const SOURCE_DIRECTORY: &str = "src";
92
93pub const MAIN_FILENAME: &str = "main.leo";
94
95pub const LIB_FILENAME: &str = "lib.leo";
96
97pub const IMPORTS_DIRECTORY: &str = "build/imports";
98
99pub const OUTPUTS_DIRECTORY: &str = "outputs";
100
101pub const BUILD_DIRECTORY: &str = "build";
102
103pub const ABI_FILENAME: &str = "abi.json";
104
105pub const TESTS_DIRECTORY: &str = "tests";
106
107/// Maximum allowed program size in bytes.
108pub const MAX_PROGRAM_SIZE: usize =
109 <snarkvm::prelude::TestnetV0 as snarkvm::prelude::Network>::MAX_PROGRAM_SIZE.last().unwrap().1;
110
111/// The edition of a deployed program on the Aleo network.
112/// Edition 0 is the initial deployment, and increments with each upgrade.
113pub type Edition = u16;
114
115/// Converts a valid program or library name into a `Symbol`.
116///
117/// Names must either end with `.aleo` or contain no periods; otherwise an error is returned.
118fn symbol(name: &str) -> Result<Symbol> {
119 if name.ends_with(".aleo") || !name.contains('.') {
120 Ok(Symbol::intern(name))
121 } else {
122 Err(PackageError::invalid_network_name(name).into())
123 }
124}
125
126/// Checks whether a string is a valid Aleo program name.
127///
128/// A valid program name must end with `.aleo` and the base name (without the
129/// suffix) must satisfy Aleo package naming rules.
130pub fn is_valid_program_name(name: &str) -> bool {
131 let Some(rest) = name.strip_suffix(".aleo") else {
132 tracing::error!("Program names must end with `.aleo`.");
133 return false;
134 };
135
136 is_valid_package_name(rest)
137}
138
139/// Checks whether a string is a valid Aleo library name.
140///
141/// Library names must satisfy Aleo package naming rules but do not require
142/// a `.aleo` suffix.
143pub fn is_valid_library_name(name: &str) -> bool {
144 is_valid_package_name(name)
145}
146
147/// Checks whether a string satisfies general Aleo package naming rules.
148///
149/// Names must be nonempty, start with a letter, contain only ASCII alphanumeric
150/// characters or underscores, avoid reserved keywords, and not contain "aleo".
151fn is_valid_package_name(name: &str) -> bool {
152 // Check that the name is nonempty.
153 if name.is_empty() {
154 tracing::error!("Aleo names must be nonempty");
155 return false;
156 }
157
158 let first = name.chars().next().unwrap();
159
160 // Check that the first character is not an underscore.
161 if first == '_' {
162 tracing::error!("Aleo names cannot begin with an underscore");
163 return false;
164 }
165
166 // Check that the first character is not a number.
167 if first.is_numeric() {
168 tracing::error!("Aleo names cannot begin with a number");
169 return false;
170 }
171
172 // Check valid characters.
173 if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
174 tracing::error!("Aleo names can only contain ASCII alphanumeric characters and underscores.");
175 return false;
176 }
177
178 // Check reserved keywords.
179 if reserved_keywords().any(|kw| kw == name) {
180 tracing::error!(
181 "Aleo names cannot be a SnarkVM reserved keyword. Reserved keywords are: {}.",
182 reserved_keywords().collect::<Vec<_>>().join(", ")
183 );
184 return false;
185 }
186
187 // Disallow "aleo"
188 if name.contains("aleo") {
189 tracing::error!("Aleo names cannot contain the keyword `aleo`.");
190 return false;
191 }
192
193 true
194}
195
196/// Get the list of all reserved and restricted keywords from snarkVM.
197/// These keywords cannot be used as program names.
198/// See: https://github.com/ProvableHQ/snarkVM/blob/046a2964f75576b2c4afbab9aa9eabc43ceb6dc3/synthesizer/program/src/lib.rs#L192
199pub fn reserved_keywords() -> impl Iterator<Item = &'static str> {
200 use snarkvm::prelude::{Program, TestnetV0};
201
202 // Flatten RESTRICTED_KEYWORDS by ignoring ConsensusVersion
203 let restricted = Program::<TestnetV0>::RESTRICTED_KEYWORDS.iter().flat_map(|(_, kws)| kws.iter().copied());
204
205 Program::<TestnetV0>::KEYWORDS.iter().copied().chain(restricted)
206}
207
208/// Creates a configured ureq agent for Leo network requests.
209///
210/// Disables `http_status_as_error` so 4xx/5xx responses return `Ok(Response)`
211/// instead of `Err(StatusCode)`. This preserves response bodies which often
212/// contain useful error details from the server.
213pub fn create_http_agent() -> ureq::Agent {
214 ureq::Agent::config_builder().max_redirects(0).http_status_as_error(false).build().new_agent()
215}
216
217/// Retries a fallible network operation with exponential backoff.
218///
219/// Attempts the operation `retries + 1` times. Delays between attempts are
220/// 1 s, 2 s, 4 s, …, capped at 64 s. Returns the result of the last attempt.
221///
222/// Only use this for idempotent, read-only network calls (GET requests);
223/// never use it for state-mutating calls such as transaction broadcasts.
224pub fn retry_network_call<T, E: std::fmt::Display>(
225 network_retries: u32,
226 mut f: impl FnMut() -> std::result::Result<T, E>,
227) -> std::result::Result<T, E> {
228 let mut result = f();
229 for attempt in 1..=network_retries {
230 if result.is_ok() {
231 break;
232 }
233 let delay_secs = 2u64.pow(attempt - 1).min(64);
234 eprintln!("⚠️ Network request failed, retrying in {delay_secs}s (attempt {attempt}/{network_retries})...");
235 std::thread::sleep(std::time::Duration::from_secs(delay_secs));
236 result = f();
237 }
238 result
239}
240
241// Fetch the given endpoint url and return the sanitized response.
242pub fn fetch_from_network(url: &str, network_retries: u32) -> Result<String, UtilError> {
243 fetch_from_network_plain(url, network_retries).map(|s| s.replace("\\n", "\n").replace('\"', ""))
244}
245
246pub fn fetch_from_network_plain(url: &str, network_retries: u32) -> Result<String, UtilError> {
247 // Retry only on transport-level failures (connection errors, timeouts, etc.).
248 // HTTP 3xx/4xx/5xx responses are not retried since they reflect persistent conditions.
249 let agent = create_http_agent();
250 let mut response = retry_network_call(network_retries, || {
251 agent
252 .get(url)
253 .header("X-Leo-Version", env!("CARGO_PKG_VERSION"))
254 .call()
255 .map_err(|e| UtilError::failed_to_retrieve_from_endpoint(url, e))
256 })?;
257 match response.status().as_u16() {
258 200..=299 => Ok(response.body_mut().read_to_string().unwrap()),
259 301 => Err(UtilError::endpoint_moved_error(url)),
260 _ => Err(UtilError::network_error(url, response.status())),
261 }
262}
263
264/// Fetch the given program from the network and return the program as a string.
265// TODO (@d0cd) Unify with `leo_package::CompilationUnit::fetch`.
266pub fn fetch_program_from_network(
267 name: &str,
268 endpoint: &str,
269 network: NetworkName,
270 network_retries: u32,
271) -> Result<String, UtilError> {
272 let url = format!("{endpoint}/{network}/program/{name}");
273 let program = fetch_from_network(&url, network_retries)?;
274 Ok(program)
275}
276
277/// Fetch the latest edition of a program from the network.
278///
279/// Returns the actual latest edition number for the given program.
280/// This should be used instead of defaulting to arbitrary edition numbers.
281pub fn fetch_latest_edition(
282 name: &str,
283 endpoint: &str,
284 network: NetworkName,
285 network_retries: u32,
286) -> Result<Edition, UtilError> {
287 // Strip the .aleo suffix if present for the URL.
288 let name_without_suffix = name.strip_suffix(".aleo").unwrap_or(name);
289
290 let url = format!("{endpoint}/{network}/program/{name_without_suffix}.aleo/latest_edition");
291 let contents = fetch_from_network(&url, network_retries)?;
292 contents
293 .parse::<u16>()
294 .map_err(|e| UtilError::failed_to_retrieve_from_endpoint(url, format!("Failed to parse edition as u16: {e}")))
295}
296
297// Verify that a fetched program is valid aleo instructions.
298pub fn verify_valid_program(name: &str, program: &str) -> Result<(), UtilError> {
299 use snarkvm::prelude::{Program, TestnetV0};
300 use std::str::FromStr as _;
301
302 // Check if the program size exceeds the maximum allowed limit.
303 let program_size = program.len();
304
305 if program_size > MAX_PROGRAM_SIZE {
306 return Err(UtilError::program_size_limit_exceeded(name, program_size, MAX_PROGRAM_SIZE));
307 }
308
309 // Parse the program to verify it's valid Aleo instructions.
310 match Program::<TestnetV0>::from_str(program) {
311 Ok(_) => Ok(()),
312 Err(_) => Err(UtilError::snarkvm_parsing_error(name)),
313 }
314}
315
316pub fn filename_no_leo_extension(path: &Path) -> Option<&str> {
317 filename_no_extension(path, ".leo")
318}
319
320pub fn filename_no_aleo_extension(path: &Path) -> Option<&str> {
321 filename_no_extension(path, ".aleo")
322}
323
324fn filename_no_extension<'a>(path: &'a Path, extension: &'static str) -> Option<&'a str> {
325 path.file_name().and_then(|os_str| os_str.to_str()).and_then(|s| s.strip_suffix(extension))
326}