Skip to main content

leo_lang/cli/commands/common/
options.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::*;
18use anyhow::{bail, ensure};
19use itertools::Itertools;
20use leo_ast::NetworkName;
21use leo_package::fetch_from_network;
22use snarkvm::prelude::{
23    CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
24    ConsensusVersion,
25    MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
26    TEST_CONSENSUS_VERSION_HEIGHTS,
27    TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
28};
29
30pub const DEFAULT_ENDPOINT: &str = "https://api.explorer.provable.com/v1";
31
32/// Compiler Options wrapper for Build command. Also used by other commands which
33/// require Build command output as their input.
34#[derive(Parser, Clone, Debug, Default)]
35pub struct BuildOptions {
36    #[clap(long, help = "Build tests along with the main program and dependencies.")]
37    pub build_tests: bool,
38    #[clap(long, help = "Don't use the dependency cache.")]
39    pub no_cache: bool,
40    #[clap(long, help = "Don't use the local source code.")]
41    pub no_local: bool,
42    #[clap(long, help = "Resolve git dependencies from the lock file and local cache only; don't fetch from remotes.")]
43    pub offline: bool,
44    #[clap(
45        long,
46        help = "Print the program checksum and the checksum of each entry and view function (the `std::prog::function_checksum` targets)."
47    )]
48    pub checksums: bool,
49    #[clap(skip)]
50    pub no_std: bool,
51}
52
53/// Network connection overrides for the `.env` file. Flattened by every command that talks to a
54/// network endpoint.
55#[derive(Parser, Clone, Debug)]
56pub struct EnvOptions {
57    #[clap(
58        long,
59        help = "The network type to use. e.g `mainnet`, `testnet, and `canary`. Overrides the `NETWORK` environment variable in your shell or `.env` file.",
60        global = true
61    )]
62    pub(crate) network: Option<NetworkName>,
63    #[clap(
64        long,
65        help = "The endpoint to deploy to. Overrides the `ENDPOINT` environment variable. We recommend using `https://api.explorer.provable.com/v1` for live networks and `http://localhost:3030` for local devnets.",
66        global = true
67    )]
68    pub(crate) endpoint: Option<String>,
69    #[clap(
70        long,
71        env = "NETWORK_RETRIES",
72        help = "Number of times to retry a failed network request before giving up.",
73        default_value = "2"
74    )]
75    pub(crate) network_retries: u32,
76}
77
78impl Default for EnvOptions {
79    fn default() -> Self {
80        Self { network: None, endpoint: None, network_retries: 2 }
81    }
82}
83
84/// Private-key override, flattened by commands that sign or identify an account
85/// (`run`, `devnode`, `deploy`, `execute`, `upgrade`).
86#[derive(Parser, Clone, Debug, Default)]
87pub struct PrivateKeyOptions {
88    #[clap(
89        long,
90        help = "The private key to use for the deployment. Overrides the `PRIVATE_KEY` environment variable in your shell or `.env` file. We recommend using `APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH` for local devnets. This key should NEVER be used in production.",
91        global = true
92    )]
93    pub(crate) private_key: Option<String>,
94}
95
96/// Consensus overrides, flattened only by transaction-producing commands (`deploy`, `execute`,
97/// `upgrade`).
98#[derive(Parser, Clone, Debug, Default)]
99pub struct ConsensusOptions {
100    #[clap(
101        long,
102        help = "Whether the network is a devnet. If not set, defaults to the `DEVNET` environment variable in your shell."
103    )]
104    pub(crate) devnet: bool,
105    #[clap(
106        long,
107        help = "Optional consensus heights to use. This should only be set if you are using a custom devnet.",
108        value_delimiter = ','
109    )]
110    pub(crate) consensus_heights: Option<Vec<u32>>,
111}
112
113/// The fee options for the transactions.
114#[derive(Parser, Clone, Debug, Default)]
115pub struct FeeOptions {
116    #[clap(
117        long,
118        help = "Priority fee in microcredits, delimited by `|`, and used in order. The fees must either be valid `u64` or `default`. Defaults to 0.",
119        value_delimiter = '|',
120        value_parser = parse_amount
121    )]
122    pub(crate) priority_fees: Vec<Option<u64>>,
123    #[clap(
124        short,
125        help = "Records to pay for fees privately, delimited by '|', and used in order. The fees must either be valid plaintext, ciphertext, or `default`. Defaults to public fees.",
126        long,
127        value_delimiter = '|',
128        value_parser = parse_record_string,
129    )]
130    fee_records: Vec<Option<String>>,
131}
132
133// A helper function to parse amounts, which can either be a `u64` or `default`.
134fn parse_amount(s: &str) -> Result<Option<u64>, String> {
135    let trimmed = s.trim();
136    if trimmed == "default" { Ok(None) } else { trimmed.parse::<u64>().map_err(|e| e.to_string()).map(Some) }
137}
138
139// A helper function to parse record strings, which can either be a string or `default`.
140fn parse_record_string(s: &str) -> Result<Option<String>, String> {
141    let trimmed = s.trim();
142    if trimmed == "default" { Ok(None) } else { Ok(Some(trimmed.to_string())) }
143}
144
145/// Parses the record string. If the string is a ciphertext, then attempt to decrypt it. Lifted from snarkOS.
146fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
147    match record.starts_with("record1") {
148        true => {
149            // Parse the ciphertext.
150            let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
151            // Derive the view key.
152            let view_key = ViewKey::try_from(private_key)?;
153            // Decrypt the ciphertext.
154            Ok(ciphertext.decrypt(&view_key)?)
155        }
156        false => Ok(Record::<N, Plaintext<N>>::from_str(record)?),
157    }
158}
159
160// A helper function to construct fee options for `k` transactions.
161#[allow(clippy::type_complexity)]
162pub fn parse_fee_options<N: Network>(
163    private_key: &PrivateKey<N>,
164    fee_options: &FeeOptions,
165    k: usize,
166) -> Result<Vec<(Option<u64>, Option<Record<N, Plaintext<N>>>)>> {
167    // Parse the priority fees.
168    let priority_fees = fee_options.priority_fees.clone();
169    // Parse the fee records.
170    let parse_record = |record: &Option<String>| record.as_ref().map(|r| parse_record::<N>(private_key, r)).transpose();
171    let fee_records = fee_options.fee_records.iter().map(parse_record).collect::<Result<Vec<_>>>()?;
172
173    // Pad the vectors to length `k`.
174    let priority_fees = priority_fees.into_iter().chain(iter::repeat(None)).take(k);
175    let fee_records = fee_records.into_iter().chain(iter::repeat(None)).take(k);
176
177    Ok(priority_fees.zip(fee_records).collect())
178}
179
180/// Additional options that are common across a number of commands.
181#[derive(Parser, Clone, Debug, Default)]
182pub struct ExtraOptions {
183    #[clap(
184        short,
185        long,
186        help = "Don't ask for confirmation. DO NOT SET THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING",
187        default_value = "false"
188    )]
189    pub(crate) yes: bool,
190    #[clap(
191        long,
192        help = "Consensus version to use. If one is not provided, the CLI will attempt to determine it from the latest block."
193    )]
194    pub(crate) consensus_version: Option<u8>,
195    #[clap(
196        long,
197        help = "Seconds to wait for a block to appear when searching for a transaction.",
198        default_value = "8"
199    )]
200    pub(crate) max_wait: usize,
201    #[clap(long, help = "Number of blocks to look at when searching for a transaction.", default_value = "12")]
202    pub(crate) blocks_to_check: usize,
203}
204
205// A helper function to get the consensus version from the fee options.
206// If a consensus version is not provided, then attempt to query the current block height and use it to determine the version.
207pub fn get_consensus_version(
208    consensus_version: &Option<u8>,
209    endpoint: &str,
210    network: NetworkName,
211    heights: &[u32],
212    context: &Context,
213    network_retries: u32,
214) -> Result<ConsensusVersion> {
215    // Get the consensus version.
216    let result = match consensus_version {
217        Some(1) => Ok(ConsensusVersion::V1),
218        Some(2) => Ok(ConsensusVersion::V2),
219        Some(3) => Ok(ConsensusVersion::V3),
220        Some(4) => Ok(ConsensusVersion::V4),
221        Some(5) => Ok(ConsensusVersion::V5),
222        Some(6) => Ok(ConsensusVersion::V6),
223        Some(7) => Ok(ConsensusVersion::V7),
224        Some(8) => Ok(ConsensusVersion::V8),
225        Some(9) => Ok(ConsensusVersion::V9),
226        Some(10) => Ok(ConsensusVersion::V10),
227        Some(11) => Ok(ConsensusVersion::V11),
228        Some(12) => Ok(ConsensusVersion::V12),
229        Some(13) => Ok(ConsensusVersion::V13),
230        Some(14) => Ok(ConsensusVersion::V14),
231        Some(15) => Ok(ConsensusVersion::V15),
232        Some(16) => Ok(ConsensusVersion::V16),
233        Some(17) => Ok(ConsensusVersion::V17),
234        Some(18) => Ok(ConsensusVersion::V18),
235        Some(19) => Ok(ConsensusVersion::V19),
236        Some(20) => Ok(ConsensusVersion::V20),
237        Some(21) => Ok(ConsensusVersion::V21),
238        Some(22) => Ok(ConsensusVersion::V22),
239        // If none is provided, then attempt to query the current block height and use it to determine the version.
240        None => {
241            println!("Attempting to determine the consensus version from the latest block height at {endpoint}...");
242            // Get the consensus heights for the current network.
243            get_latest_block_height(endpoint, network, context, network_retries)
244                .and_then(|current_block_height| get_consensus_version_from_height(current_block_height, heights))
245                .map_err(|_| {
246                    crate::errors::custom(
247                        "Failed to get consensus version. Ensure that your endpoint is valid or provide an explicit version to use via `--consensus-version`",
248                    )
249                        .into()
250                })
251        }
252        Some(version) => Err(crate::errors::custom(format!("Invalid consensus version: {version}")).into()),
253    };
254
255    // Check `{endpoint}/{network}/consensus_version` endpoint for the consensus version.
256    // If it returns a result and does not match the given version, print a warning.
257    if let Ok(consensus_version) = result
258        && let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network, network_retries)
259    {
260        println!("⚠️ Warning: {e}");
261    }
262
263    result
264}
265
266/// A helper function to check for a consensus version mismatch against the network.
267pub fn check_consensus_version_mismatch(
268    consensus_version: ConsensusVersion,
269    endpoint: &str,
270    network: NetworkName,
271    network_retries: u32,
272) -> anyhow::Result<()> {
273    // Check the `{endpoint}/{network}/consensus_version` endpoint for the consensus version.
274    if let Ok(response) = fetch_from_network(&format!("{endpoint}/{network}/consensus_version"), network_retries)
275        && let Ok(response) = response.parse::<u8>()
276    {
277        let consensus_version = consensus_version as u8;
278        if response != consensus_version {
279            bail!("Expected consensus version {consensus_version} but found {response} at {endpoint}",);
280        }
281    }
282    Ok(())
283}
284
285// A helper function to get the consensus version based on the block height.
286// Note. This custom implementation is necessary because we use `snarkVM` with the `test_heights` feature enabled, which does not reflect the actual consensus version heights.
287pub fn get_consensus_version_from_height(seek_height: u32, heights: &[u32]) -> Result<ConsensusVersion> {
288    // Find the consensus version based on the block height.
289    let index = match heights.binary_search_by(|height| height.cmp(&seek_height)) {
290        // If a consensus version was found at this height, return it.
291        Ok(index) => index,
292        // If the specified height was not found, determine whether to return an appropriate version.
293        Err(index) => {
294            if index == 0 {
295                return Err(crate::errors::custom("Expected consensus version 1 to exist at height 0.").into());
296            } else {
297                // Return the appropriate version belonging to the height *lower* than the sought height.
298                index - 1
299            }
300        }
301    };
302    // Convert the index to a consensus version.
303    number_to_consensus_version(index + 1)
304}
305
306// A helper to convert an index to a consensus version.
307pub fn number_to_consensus_version(index: usize) -> Result<ConsensusVersion> {
308    match index {
309        1 => Ok(ConsensusVersion::V1),
310        2 => Ok(ConsensusVersion::V2),
311        3 => Ok(ConsensusVersion::V3),
312        4 => Ok(ConsensusVersion::V4),
313        5 => Ok(ConsensusVersion::V5),
314        6 => Ok(ConsensusVersion::V6),
315        7 => Ok(ConsensusVersion::V7),
316        8 => Ok(ConsensusVersion::V8),
317        9 => Ok(ConsensusVersion::V9),
318        10 => Ok(ConsensusVersion::V10),
319        11 => Ok(ConsensusVersion::V11),
320        12 => Ok(ConsensusVersion::V12),
321        13 => Ok(ConsensusVersion::V13),
322        14 => Ok(ConsensusVersion::V14),
323        15 => Ok(ConsensusVersion::V15),
324        16 => Ok(ConsensusVersion::V16),
325        17 => Ok(ConsensusVersion::V17),
326        18 => Ok(ConsensusVersion::V18),
327        19 => Ok(ConsensusVersion::V19),
328        20 => Ok(ConsensusVersion::V20),
329        21 => Ok(ConsensusVersion::V21),
330        22 => Ok(ConsensusVersion::V22),
331        _ => Err(crate::errors::custom(format!(
332            "Invalid consensus version: {index}. You may need to update Leo to support this version."
333        ))
334        .into()),
335    }
336}
337
338/// Get the consensus heights for the current network.
339/// First check the `CONSENSUS_VERSION_HEIGHTS` environment variable.
340/// Otherwise, if `is_devnet` is true, then return the test consensus heights.
341/// Otherwise, return the consensus heights for the given network.
342pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
343    // Check the `CONSENSUS_VERSION_HEIGHTS` environment variable.
344    if let Ok(heights) = std::env::var("CONSENSUS_VERSION_HEIGHTS") {
345        if let Ok(heights) = heights.split(',').map(|s| s.trim().parse::<u32>()).collect::<Result<Vec<_>, _>>() {
346            return heights;
347        } else {
348            println!(
349                "⚠️ Warning: Failed to parse `CONSENSUS_VERSION_HEIGHTS` environment variable. Falling back to default heights."
350            );
351        }
352    }
353    // If `is_devnet` is true, then return the test consensus heights.
354    // Otherwise, return the consensus heights for the given network.
355    if is_devnet {
356        TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().map(|(_, v)| v).collect_vec()
357    } else {
358        match network_name {
359            NetworkName::CanaryV0 => CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
360            NetworkName::MainnetV0 => MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
361            NetworkName::TestnetV0 => TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
362        }
363        .into_iter()
364        .map(|(_, v)| v)
365        .collect_vec()
366    }
367}
368
369/// Validates a vector of heights as consensus heights.
370pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
371    // There must be exactly one height per consensus version.
372    let expected = ConsensusVersion::latest() as usize;
373    ensure!(
374        heights.len() == expected,
375        "expected exactly {expected} consensus heights (one per consensus version), but found {}",
376        heights.len()
377    );
378    // Assert that the genesis height is 0.
379    ensure!(heights[0] == 0, "Genesis height must be 0.");
380    // Activation heights must increase. Inactive versions can share `u32::MAX`.
381    for window in heights.windows(2) {
382        if window[0] > window[1] || (window[0] == window[1] && window[0] != u32::MAX) {
383            bail!("Heights must increase, except for inactive versions at u32::MAX, but found: {window:?}");
384        }
385    }
386    Ok(())
387}
388
389/// What to do with a transaction produced by the CLI.
390#[derive(Args, Clone, Debug)]
391pub struct TransactionAction {
392    #[arg(long, help = "Print the transaction to stdout.")]
393    pub print: bool,
394    #[arg(long, help = "Broadcast the transaction to the network.")]
395    pub broadcast: bool,
396    #[arg(long, help = "Save the transaction to the provided directory.")]
397    pub save: Option<String>,
398}
399
400/// Returns the endpoint to interact with the network.
401/// If the `--endpoint` options is not provided, it will default to the environment variable.
402pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
403    match endpoint {
404        Some(endpoint) => Ok(endpoint.clone()),
405        None => {
406            // Load the endpoint from the environment.
407            std::env::var("ENDPOINT").map_err(|_| {
408                crate::errors::custom("Please provide the `--endpoint` or set the `ENDPOINT` environment variable.")
409                    .into()
410            })
411        }
412    }
413}
414
415/// Returns the network name.
416/// If the `--network` options is not provided, it will default to the environment variable.
417pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
418    match network {
419        Some(network) => Ok(*network),
420        None => {
421            // Load the network from the environment.
422            let network = std::env::var("NETWORK").map_err(|_| {
423                crate::errors::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
424            })?;
425            // Parse the network.
426            Ok(NetworkName::from_str(&network)?)
427        }
428    }
429}
430
431/// Returns the private key.
432/// If the `--private-key` options is not provided, it will default to the environment variable.
433pub fn get_private_key<N: Network>(private_key: &Option<String>) -> Result<PrivateKey<N>> {
434    match private_key {
435        Some(private_key) => Ok(PrivateKey::<N>::from_str(private_key)?),
436        None => {
437            // Load the private key from the environment.
438            let private_key = std::env::var("PRIVATE_KEY").map_err(|e| {
439                crate::errors::custom(format!("Failed to load `PRIVATE_KEY` from the environment: {e}"))
440            })?;
441            // Parse the private key.
442            Ok(PrivateKey::<N>::from_str(&private_key)?)
443        }
444    }
445}
446
447/// Returns whether the devnet flag is set.
448/// If the `--devnet` flag is not set, check if the environment variable is set, otherwise default to `false`.
449pub fn get_is_devnet(devnet: bool) -> bool {
450    if devnet { true } else { std::env::var("DEVNET").is_ok() }
451}
452
453#[cfg(test)]
454mod test {
455    use snarkvm::prelude::ConsensusVersion;
456
457    #[test]
458    fn test_latest_consensus_version() {
459        assert_eq!(ConsensusVersion::latest(), ConsensusVersion::V22); // If this fails, update the test and any code that matches on `ConsensusVersion`.
460        let context = super::Context::new(None, None, false, None).expect("Test context must be valid");
461        for version in [ConsensusVersion::V19, ConsensusVersion::V20, ConsensusVersion::V21, ConsensusVersion::V22] {
462            assert_eq!(super::number_to_consensus_version(version as usize).expect("Valid version"), version);
463            assert_eq!(
464                super::get_consensus_version(
465                    &Some(version as u8),
466                    "",
467                    leo_ast::NetworkName::TestnetV0,
468                    &[],
469                    &context,
470                    0,
471                )
472                .expect("Explicit consensus versions do not require an endpoint"),
473                version
474            );
475        }
476        assert!(super::number_to_consensus_version(23).is_err());
477    }
478
479    #[test]
480    fn test_consensus_activation_heights() {
481        let heights = super::TEST_CONSENSUS_VERSION_HEIGHTS.map(|(_, height)| height);
482        for (version, height) in super::TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().skip(19) {
483            assert_eq!(super::get_consensus_version_from_height(height, &heights).expect("Activation height"), version);
484            assert_eq!(
485                super::get_consensus_version_from_height(height - 1, &heights).expect("Previous height") as u8,
486                version as u8 - 1
487            );
488        }
489    }
490
491    #[test]
492    fn test_validate_consensus_heights() {
493        let n = ConsensusVersion::latest() as u32;
494        // Exactly one height per version, genesis 0, strictly increasing: ok.
495        let valid: Vec<u32> = (0..n).collect();
496        assert!(super::validate_consensus_heights(&valid).is_ok());
497        // Wrong count is a graceful error, not a panic.
498        assert!(super::validate_consensus_heights(&(0..n - 1).collect::<Vec<_>>()).is_err());
499        // Empty input is a graceful error, not an index-out-of-bounds panic.
500        assert!(super::validate_consensus_heights(&[]).is_err());
501        for schedule in [
502            super::MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
503            super::TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
504            super::CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
505        ] {
506            assert!(super::validate_consensus_heights(&schedule.map(|(_, height)| height)).is_ok());
507        }
508        let mut duplicate = valid.clone();
509        duplicate[1] = duplicate[0];
510        assert!(super::validate_consensus_heights(&duplicate).is_err());
511        let mut decreasing = valid;
512        decreasing[1] = u32::MAX;
513        assert!(super::validate_consensus_heights(&decreasing).is_err());
514    }
515}