Skip to main content

leo_lang/cli/commands/
mod.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
17mod abi;
18pub use abi::LeoAbi;
19
20mod add;
21pub use add::{DependencySource, GitRef, LeoAdd};
22
23mod account;
24pub use account::Account;
25
26mod build;
27pub use build::{LeoBuild, build_output};
28
29mod clean;
30pub use clean::LeoClean;
31
32mod common;
33pub use common::*;
34
35mod deploy;
36pub use deploy::LeoDeploy;
37use deploy::{
38    Task,
39    compute_deployment_stats,
40    max_program_size_for_consensus_version,
41    print_deployment_plan,
42    print_deployment_summary,
43    warn_if_transaction_oversized,
44};
45
46mod devnet;
47pub use devnet::LeoDevnet;
48
49mod devnode;
50pub use devnode::LeoDevnode;
51
52mod execute;
53pub use execute::LeoExecute;
54
55pub mod query;
56pub use query::LeoQuery;
57
58mod new;
59pub use new::LeoNew;
60
61mod remove;
62pub use remove::LeoRemove;
63
64mod run;
65pub use run::LeoRun;
66
67mod synthesize;
68pub use synthesize::LeoSynthesize;
69
70mod test;
71pub use test::LeoTest;
72
73mod update;
74pub use update::LeoUpdate;
75
76pub mod upgrade;
77pub use upgrade::LeoUpgrade;
78
79use super::*;
80use crate::cli::{helpers::context::*, query::QueryCommands};
81
82use leo_errors::{Handler, Result};
83use snarkvm::{
84    console::network::Network,
85    prelude::{Address, Ciphertext, Plaintext, PrivateKey, Record, Value, ViewKey, block::Transaction},
86};
87
88use clap::{Args, Parser};
89use colored::Colorize;
90use dialoguer::{Confirm, theme::ColorfulTheme};
91use std::{iter, str::FromStr};
92use tracing::span::Span;
93use ureq::http::Uri;
94
95/// Base trait for the Leo CLI, see methods and their documentation for details.
96pub trait Command {
97    /// If the current command requires running another command beforehand
98    /// and needs its output result, this is where the result type is defined.
99    /// Example: type Input: <CommandA as Command>::Out
100    type Input;
101
102    /// Defines the output of this command, which may be used as `Input` for another
103    /// command. If this command is not used as a prelude for another command,
104    /// this field may be left empty.
105    type Output;
106
107    /// Adds a span to the logger via `tracing::span`.
108    /// Because of the specifics of the macro implementation, it is not possible
109    /// to set the span name with a non-literal i.e. a dynamic variable even if this
110    /// variable is a &'static str.
111    fn log_span(&self) -> Span {
112        tracing::span!(tracing::Level::INFO, "Leo")
113    }
114
115    /// Runs the prelude and returns the Input of the current command.
116    fn prelude(&self, context: Context) -> Result<Self::Input>
117    where
118        Self: std::marker::Sized;
119
120    /// Runs the main operation of this command. This function is run within
121    /// context of 'execute' function, which sets logging and timers.
122    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output>
123    where
124        Self: std::marker::Sized;
125
126    /// A wrapper around the `apply` method.
127    /// This function sets up tracing, timing, and the context.
128    fn execute(self, context: Context) -> Result<Self::Output>
129    where
130        Self: std::marker::Sized,
131    {
132        let input = self.prelude(context.clone())?;
133
134        // Create the span for this command.
135        let span = self.log_span();
136        let span = span.enter();
137
138        // Calculate the execution time for this command.
139        let out = self.apply(context, input);
140
141        drop(span);
142
143        out
144    }
145
146    /// Executes command but empty the result. Comes in handy where there's a
147    /// need to make match arms compatible while keeping implementation-specific
148    /// output possible. Errors however are all of the type Error
149    fn try_execute(self, context: Context) -> Result<()>
150    where
151        Self: std::marker::Sized,
152    {
153        self.execute(context).map(|_| Ok(()))?
154    }
155}
156
157/// A helper function to parse an input string into a `Value`, handling record ciphertexts as well.
158pub fn parse_input<N: Network>(input: &str, private_key: &PrivateKey<N>) -> Result<Value<N>> {
159    // Trim whitespace from the input.
160    let input = input.trim();
161    // Check if the input is a record ciphertext.
162    if input.starts_with("record1") {
163        // Get the view key from the private key.
164        let view_key = ViewKey::<N>::try_from(private_key)
165            .map_err(|e| crate::errors::custom(format!("Failed to view key from the private key: {e}")))?;
166        // Parse the input as a record.
167        Record::<N, Ciphertext<N>>::from_str(input)
168            .and_then(|ciphertext| ciphertext.decrypt(&view_key))
169            .map(Value::Record)
170            .map_err(|e| crate::errors::custom(format!("Failed to parse input as record: {e}")).into())
171    } else {
172        // Pre-validate numeric literals to reject malformed inputs that snarkvm would silently coerce to zero.
173        validate_cli_literal(input)?;
174        Value::from_str(input).map_err(|e| crate::errors::custom(format!("Failed to parse input: {e}")).into())
175    }
176}
177
178/// Pre-validates a CLI input literal to reject malformed numeric literals
179/// that snarkvm would silently coerce to zero (e.g. `truefield`, `""field`).
180fn validate_cli_literal(input: &str) -> Result<()> {
181    // Skip pre-validation for Aleo bech32 values; they are validated downstream.
182    const ALEO_BECH32_PREFIXES: &[&str] = &["aleo1", "sign1", "APrivateKey1", "AViewKey1"];
183    if ALEO_BECH32_PREFIXES.iter().any(|prefix| input.starts_with(prefix)) {
184        return Ok(());
185    }
186
187    // Suffixes ordered longest-first to avoid ambiguous matches (e.g. u128 before u8).
188    const UNSIGNED_SUFFIXES: &[&str] = &["u128", "u64", "u32", "u16", "u8"];
189    const SIGNED_SUFFIXES: &[&str] = &["i128", "i64", "i32", "i16", "i8"];
190    const FIELD_LIKE_SUFFIXES: &[&str] = &["field", "scalar", "group"];
191
192    for suffix in UNSIGNED_SUFFIXES {
193        if let Some(prefix) = input.strip_suffix(suffix) {
194            return validate_numeric_prefix(prefix, suffix, false, false);
195        }
196    }
197    for suffix in SIGNED_SUFFIXES {
198        if let Some(prefix) = input.strip_suffix(suffix) {
199            return validate_numeric_prefix(prefix, suffix, true, false);
200        }
201    }
202    for suffix in FIELD_LIKE_SUFFIXES {
203        if let Some(prefix) = input.strip_suffix(suffix) {
204            // Group supports coordinate pair syntax like (x, y)group — defer to snarkvm.
205            if *suffix == "group" && prefix.starts_with('(') {
206                return Ok(());
207            }
208            return validate_numeric_prefix(prefix, suffix, true, true);
209        }
210    }
211
212    // For all other inputs (bool, address, struct, record, etc.), skip pre-validation.
213    Ok(())
214}
215
216/// Validates that `prefix` is a well-formed numeric string for the given type `suffix`.
217fn validate_numeric_prefix(prefix: &str, suffix: &str, allow_negative: bool, decimal_only: bool) -> Result<()> {
218    if prefix.is_empty() {
219        return Err(crate::errors::custom(format!(
220            "Invalid {suffix} literal: missing numeric value before '{suffix}'"
221        ))
222        .into());
223    }
224    let valid = if decimal_only {
225        is_valid_decimal(prefix, allow_negative)
226    } else {
227        is_valid_decimal(prefix, allow_negative) || is_valid_radix_prefixed(prefix, allow_negative)
228    };
229    if !valid {
230        return Err(crate::errors::custom(format!(
231            "Invalid {suffix} literal: '{prefix}' is not a valid numeric value"
232        ))
233        .into());
234    }
235    Ok(())
236}
237
238/// Checks if `s` is a valid decimal integer string (e.g. `123`, `-42`, `1_000`).
239fn is_valid_decimal(s: &str, allow_negative: bool) -> bool {
240    let s = if allow_negative { s.strip_prefix('-').unwrap_or(s) } else { s };
241    if s.is_empty() {
242        return false;
243    }
244    let mut chars = s.chars();
245    // Must start with a digit.
246    if !chars.next().unwrap().is_ascii_digit() {
247        return false;
248    }
249    chars.all(|c| c.is_ascii_digit() || c == '_')
250}
251
252/// Checks if `s` is a valid radix-prefixed integer string (e.g. `0xFF`, `0b1010`, `0o77`).
253fn is_valid_radix_prefixed(s: &str, allow_negative: bool) -> bool {
254    let s = if allow_negative { s.strip_prefix('-').unwrap_or(s) } else { s };
255    if s.len() < 3 || !s.starts_with('0') {
256        return false;
257    }
258    let radix_char = s.as_bytes()[1];
259    let rest = &s[2..];
260    if rest.is_empty() || rest.starts_with('_') {
261        return false;
262    }
263    match radix_char {
264        b'x' | b'X' => rest.chars().all(|c| c.is_ascii_hexdigit() || c == '_'),
265        b'o' => rest.chars().all(|c| matches!(c, '0'..='7' | '_')),
266        b'b' => rest.chars().all(|c| matches!(c, '0' | '1' | '_')),
267        _ => false,
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_valid_literals() {
277        let valid = [
278            "42field",
279            "-7field",
280            "0field",
281            "1_000_000field",
282            "100u64",
283            "0x1Fu8",
284            "0b1010u32",
285            "0o77u16",
286            "-128i8",
287            "-0x80i16",
288            "0scalar",
289            "42group",
290            "(1, 2)group",
291            // Non-numeric types: no suffix match, so validation is skipped.
292            "true",
293            "false",
294            "aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9",
295            // Addresses whose bech32 tail collides with a Leo numeric type suffix
296            // (regression coverage for issue #29372).
297            "aleo1vnx8f43f7yjvs2ehlqsl78j2qh4409s8e4g7gx40u3v884gqgqxqscutu8",
298            "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu32",
299            "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu64",
300            "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqscalar",
301            // Signatures: same bech32 alphabet collisions as addresses.
302            "sign195m229jvzr0wmnshj6f8gwplhkrkhjumgjmad553r997u7pjfgpfz4j2w0c9lp53mcqqdsmut2g3a2zuvgst85w38hv273mwjec3sqjsv9w6uglcy58gjh7x3l55z68zsf24kx7a73ctp8x8klhuw7l2p4s3aq8um5jp304js7qcnwdqj56q5r5088tyvxsgektun0rnmvtsuxpe6sj",
303            "sign1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu8",
304            "sign1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu32",
305            "sign1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu64",
306            "sign1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqscalar",
307            // Private keys and view keys: not valid program inputs, but skipping
308            // pre-validation lets snarkvm produce the correct parse error.
309            "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH",
310            "APrivateKey1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu8",
311            "AViewKey1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu32",
312        ];
313        for input in &valid {
314            assert!(validate_cli_literal(input).is_ok(), "expected '{input}' to be valid");
315        }
316    }
317
318    #[test]
319    fn test_invalid_literals() {
320        let invalid = [
321            "truefield",
322            "falsefield",
323            "field",
324            "scalar",
325            "u8",
326            "abcu64",
327            "-u8",
328            "hello_worldscalar",
329            "truegroup",
330            "xxxi128",
331        ];
332        for input in &invalid {
333            assert!(validate_cli_literal(input).is_err(), "expected '{input}' to be invalid");
334        }
335    }
336
337    // Smoke test for `parse_input` across the value types accepted as Leo program inputs.
338    #[test]
339    fn test_parse_input_value_types() {
340        use snarkvm::prelude::TestnetV0;
341        let private_key =
342            PrivateKey::<TestnetV0>::from_str("APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH").unwrap();
343        let inputs = [
344            // Integers across signed and unsigned widths.
345            "42u8",
346            "0u8",
347            "255u8",
348            "-7i32",
349            "-128i8",
350            "100u64",
351            "1_000_000u128",
352            // Field and scalar.
353            "1_000_000field",
354            "-7field",
355            "0scalar",
356            // Booleans.
357            "true",
358            "false",
359            // Addresses (regular tail, and bech32 tails matching numeric suffixes).
360            "aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9",
361            "aleo1vnx8f43f7yjvs2ehlqsl78j2qh4409s8e4g7gx40u3v884gqgqxqscutu8",
362            // Signature.
363            "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq",
364            // Composite plaintexts.
365            "[1u8, 2u8, 3u8]",
366            "{a: 1u8, b: 2u8}",
367            "{nested: {x: 1field, y: 2field}, count: 3u32}",
368            // Whitespace is trimmed.
369            "  42u8  ",
370        ];
371        for input in &inputs {
372            assert!(parse_input::<TestnetV0>(input, &private_key).is_ok(), "expected '{input}' to parse");
373        }
374    }
375}