leo_lang/cli/commands/common/
options.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::*;
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
30/// Compiler Options wrapper for Build command. Also used by other commands which
31/// require Build command output as their input.
32#[derive(Parser, Clone, Debug)]
33pub struct BuildOptions {
34    #[clap(long, help = "Enables offline mode.")]
35    pub offline: bool,
36    #[clap(long, help = "Enable spans in AST snapshots.")]
37    pub enable_ast_spans: bool,
38    #[clap(long, help = "Enables dead code elimination in the compiler.", default_value = "true")]
39    pub enable_dce: bool,
40    #[clap(long, help = "Max depth to type check nested conditionals.", default_value = "10")]
41    pub conditional_block_max_depth: usize,
42    #[clap(long, help = "Disable type checking of nested conditional branches in finalize scope.")]
43    pub disable_conditional_branch_type_checking: bool,
44    #[clap(long, help = "Write an AST snapshot immediately after parsing.")]
45    pub enable_initial_ast_snapshot: bool,
46    #[clap(long, help = "Writes all AST snapshots for the different compiler phases.")]
47    pub enable_all_ast_snapshots: bool,
48    #[clap(long, help = "Comma separated list of passes whose AST snapshots to capture.", value_delimiter = ',', num_args = 1..)]
49    pub ast_snapshots: Vec<String>,
50    #[clap(long, help = "Build tests along with the main program and dependencies.")]
51    pub build_tests: bool,
52    #[clap(long, help = "Don't use the dependency cache.")]
53    pub no_cache: bool,
54    #[clap(long, help = "Don't use the local source code.")]
55    pub no_local: bool,
56}
57
58impl Default for BuildOptions {
59    fn default() -> Self {
60        Self {
61            offline: false,
62            enable_ast_spans: false,
63            enable_dce: true,
64            conditional_block_max_depth: 10,
65            disable_conditional_branch_type_checking: false,
66            enable_initial_ast_snapshot: false,
67            enable_all_ast_snapshots: false,
68            ast_snapshots: Vec::new(),
69            build_tests: false,
70            no_cache: false,
71            no_local: false,
72        }
73    }
74}
75
76/// Overrides for the `.env` file.
77#[derive(Parser, Clone, Debug, Default)]
78pub struct EnvOptions {
79    #[clap(
80        long,
81        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."
82    )]
83    pub(crate) private_key: Option<String>,
84    #[clap(
85        long,
86        help = "The network type to use. e.g `mainnet`, `testnet, and `canary`. Overrides the `NETWORK` environment variable in your shell or `.env` file."
87    )]
88    pub(crate) network: Option<NetworkName>,
89    #[clap(
90        long,
91        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."
92    )]
93    pub(crate) endpoint: Option<String>,
94    #[clap(
95        long,
96        help = "Whether the network is a devnet. If not set, defaults to the `DEVNET` environment variable in your shell or `.env` file."
97    )]
98    pub(crate) devnet: bool,
99    #[clap(
100        long,
101        help = "Optional consensus heights to use. This should only be set if you are using a custom devnet.",
102        value_delimiter = ','
103    )]
104    pub(crate) consensus_heights: Option<Vec<u32>>,
105}
106
107/// The fee options for the transactions.
108#[derive(Parser, Clone, Debug, Default)]
109pub struct FeeOptions {
110    #[clap(
111        long,
112        help = "[UNUSED] Base fees in microcredits, delimited by `|`, and used in order. The fees must either be valid `u64` or `default`. Defaults to automatic calculation.",
113        hide = true,
114        value_delimiter = '|',
115        value_parser = parse_amount
116    )]
117    pub(crate) base_fees: Vec<Option<u64>>,
118    #[clap(
119        long,
120        help = "Priority fee in microcredits, delimited by `|`, and used in order. The fees must either be valid `u64` or `default`. Defaults to 0.",
121        value_delimiter = '|',
122        value_parser = parse_amount
123    )]
124    pub(crate) priority_fees: Vec<Option<u64>>,
125    #[clap(
126        short,
127        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.",
128        long,
129        value_delimiter = '|',
130        value_parser = parse_record_string,
131    )]
132    fee_records: Vec<Option<String>>,
133}
134
135// A helper function to parse amounts, which can either be a `u64` or `default`.
136fn parse_amount(s: &str) -> Result<Option<u64>, String> {
137    let trimmed = s.trim();
138    if trimmed == "default" { Ok(None) } else { trimmed.parse::<u64>().map_err(|e| e.to_string()).map(Some) }
139}
140
141// A helper function to parse record strings, which can either be a string or `default`.
142fn parse_record_string(s: &str) -> Result<Option<String>, String> {
143    let trimmed = s.trim();
144    if trimmed == "default" { Ok(None) } else { Ok(Some(trimmed.to_string())) }
145}
146
147/// Parses the record string. If the string is a ciphertext, then attempt to decrypt it. Lifted from snarkOS.
148fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
149    match record.starts_with("record1") {
150        true => {
151            // Parse the ciphertext.
152            let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
153            // Derive the view key.
154            let view_key = ViewKey::try_from(private_key)?;
155            // Decrypt the ciphertext.
156            Ok(ciphertext.decrypt(&view_key)?)
157        }
158        false => Ok(Record::<N, Plaintext<N>>::from_str(record)?),
159    }
160}
161
162// A helper function to construct fee options for `k` transactions.
163#[allow(clippy::type_complexity)]
164pub fn parse_fee_options<N: Network>(
165    private_key: &PrivateKey<N>,
166    fee_options: &FeeOptions,
167    k: usize,
168) -> Result<Vec<(Option<u64>, Option<u64>, Option<Record<N, Plaintext<N>>>)>> {
169    // Parse the base fees.
170    let base_fees = fee_options.base_fees.clone();
171    // Parse the priority fees.
172    let priority_fees = fee_options.priority_fees.clone();
173    // Parse the fee records.
174    let parse_record = |record: &Option<String>| record.as_ref().map(|r| parse_record::<N>(private_key, r)).transpose();
175    let fee_records = fee_options.fee_records.iter().map(parse_record).collect::<Result<Vec<_>>>()?;
176
177    // Pad the vectors to length `k`.
178    let base_fees = base_fees.into_iter().chain(iter::repeat(None)).take(k);
179    let priority_fees = priority_fees.into_iter().chain(iter::repeat(None)).take(k);
180    let fee_records = fee_records.into_iter().chain(iter::repeat(None)).take(k);
181
182    Ok(base_fees.zip(priority_fees).zip(fee_records).map(|((x, y), z)| (x, y, z)).collect())
183}
184
185/// Additional options that are common across a number of commands.
186#[derive(Parser, Clone, Debug, Default)]
187pub struct ExtraOptions {
188    #[clap(
189        short,
190        long,
191        help = "Don't ask for confirmation. DO NOT SET THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING",
192        default_value = "false"
193    )]
194    pub(crate) yes: bool,
195    #[clap(
196        long,
197        help = "Consensus version to use. If one is not provided, the CLI will attempt to determine it from the latest block."
198    )]
199    pub(crate) consensus_version: Option<u8>,
200    #[clap(
201        long,
202        help = "Seconds to wait for a block to appear when searching for a transaction.",
203        default_value = "8"
204    )]
205    pub(crate) max_wait: usize,
206    #[clap(long, help = "Number of blocks to look at when searching for a transaction.", default_value = "12")]
207    pub(crate) blocks_to_check: usize,
208}
209
210// A helper function to get the consensus version from the fee options.
211// If a consensus version is not provided, then attempt to query the current block height and use it to determine the version.
212pub fn get_consensus_version(
213    consensus_version: &Option<u8>,
214    endpoint: &str,
215    network: NetworkName,
216    heights: &[u32],
217    context: &Context,
218) -> Result<ConsensusVersion> {
219    // Get the consensus version.
220    let result = match consensus_version {
221        Some(1) => Ok(ConsensusVersion::V1),
222        Some(2) => Ok(ConsensusVersion::V2),
223        Some(3) => Ok(ConsensusVersion::V3),
224        Some(4) => Ok(ConsensusVersion::V4),
225        Some(5) => Ok(ConsensusVersion::V5),
226        Some(6) => Ok(ConsensusVersion::V6),
227        Some(7) => Ok(ConsensusVersion::V7),
228        Some(8) => Ok(ConsensusVersion::V8),
229        Some(9) => Ok(ConsensusVersion::V9),
230        Some(10) => Ok(ConsensusVersion::V10),
231        // If none is provided, then attempt to query the current block height and use it to determine the version.
232        None => {
233            println!("Attempting to determine the consensus version from the latest block height at {endpoint}...");
234            // Get the consensus heights for the current network.
235            get_latest_block_height(endpoint, network, context)
236                .and_then(|current_block_height| get_consensus_version_from_height(current_block_height, heights))
237                .map_err(|_| {
238                    CliError::custom(
239                        "Failed to get consensus version. Ensure that your endpoint is valid or provide an explicit version to use via `--consensus-version`",
240                    )
241                        .into()
242                })
243        }
244        Some(version) => Err(CliError::custom(format!("Invalid consensus version: {version}")).into()),
245    };
246
247    // Check `{endpoint}/{network}/consensus_version` endpoint for the consensus version.
248    // If it returns a result and does not match the given version, print a warning.
249    if let Ok(consensus_version) = result {
250        if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
251            println!("⚠️ Warning: {e}");
252        }
253    }
254
255    result
256}
257
258/// A helper function to check for a consensus version mismatch against the network.
259pub fn check_consensus_version_mismatch(
260    consensus_version: ConsensusVersion,
261    endpoint: &str,
262    network: NetworkName,
263) -> anyhow::Result<()> {
264    // Check the `{endpoint}/{network}/consensus_version` endpoint for the consensus version.
265    if let Ok(response) = fetch_from_network(&format!("{endpoint}/{network}/consensus_version")) {
266        if let Ok(response) = response.parse::<u8>() {
267            let consensus_version = consensus_version as u8;
268            if response != consensus_version {
269                bail!("Expected consensus version {consensus_version} but found {response} at {endpoint}",);
270            }
271        }
272    }
273    Ok(())
274}
275
276// A helper function to get the consensus version based on the block height.
277// 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.
278pub fn get_consensus_version_from_height(seek_height: u32, heights: &[u32]) -> Result<ConsensusVersion> {
279    // Find the consensus version based on the block height.
280    let index = match heights.binary_search_by(|height| height.cmp(&seek_height)) {
281        // If a consensus version was found at this height, return it.
282        Ok(index) => index,
283        // If the specified height was not found, determine whether to return an appropriate version.
284        Err(index) => {
285            if index == 0 {
286                return Err(CliError::custom("Expected consensus version 1 to exist at height 0.").into());
287            } else {
288                // Return the appropriate version belonging to the height *lower* than the sought height.
289                index - 1
290            }
291        }
292    };
293    // Convert the index to a consensus version.
294    Ok(number_to_consensus_version(index + 1))
295}
296
297// A helper to convert an index to a consensus version.
298pub fn number_to_consensus_version(index: usize) -> ConsensusVersion {
299    match index {
300        1 => ConsensusVersion::V1,
301        2 => ConsensusVersion::V2,
302        3 => ConsensusVersion::V3,
303        4 => ConsensusVersion::V4,
304        5 => ConsensusVersion::V5,
305        6 => ConsensusVersion::V6,
306        7 => ConsensusVersion::V7,
307        8 => ConsensusVersion::V8,
308        9 => ConsensusVersion::V9,
309        10 => ConsensusVersion::V10,
310        _ => panic!("Invalid consensus version: {index}"),
311    }
312}
313
314/// Get the consensus heights for the current network.
315/// First check the `CONSENSUS_VERSION_HEIGHTS` environment variable.
316/// Otherwise, if `is_devnet` is true, then return the test consensus heights.
317/// Otherwise, return the consensus heights for the given network.
318pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
319    // Check the `CONSENSUS_VERSION_HEIGHTS` environment variable.
320    if let Ok(heights) = std::env::var("CONSENSUS_VERSION_HEIGHTS") {
321        if let Ok(heights) = heights.split(',').map(|s| s.trim().parse::<u32>()).collect::<Result<Vec<_>, _>>() {
322            return heights;
323        } else {
324            println!(
325                "⚠️ Warning: Failed to parse `CONSENSUS_VERSION_HEIGHTS` environment variable. Falling back to default heights."
326            );
327        }
328    }
329    // If `is_devnet` is true, then return the test consensus heights.
330    // Otherwise, return the consensus heights for the given network.
331    if is_devnet {
332        TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().map(|(_, v)| v).collect_vec()
333    } else {
334        match network_name {
335            NetworkName::CanaryV0 => CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
336            NetworkName::MainnetV0 => MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
337            NetworkName::TestnetV0 => TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
338        }
339        .into_iter()
340        .map(|(_, v)| v)
341        .collect_vec()
342    }
343}
344
345/// Validates a vector of heights as consensus heights.
346pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
347    // Assert that the genesis height is 0.
348    ensure!(heights[0] == 0, "Genesis height must be 0.");
349    // Assert that the consensus heights are strictly increasing.
350    for window in heights.windows(2) {
351        if window[0] >= window[1] {
352            bail!("Heights must be strictly increasing, but found: {window:?}");
353        }
354    }
355    Ok(())
356}
357
358/// What to do with a transaction produced by the CLI.
359#[derive(Args, Clone, Debug)]
360pub struct TransactionAction {
361    #[arg(long, help = "Print the transaction to stdout.")]
362    pub print: bool,
363    #[arg(long, help = "Broadcast the transaction to the network.")]
364    pub broadcast: bool,
365    #[arg(long, help = "Save the transaction to the provided directory.")]
366    pub save: Option<String>,
367}
368
369/// Returns the endpoint to interact with the network.
370/// If the `--endpoint` options is not provided, it will default to the environment variable.
371pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
372    match endpoint {
373        Some(endpoint) => Ok(endpoint.clone()),
374        None => {
375            // Load the endpoint from the environment.
376            std::env::var("ENDPOINT").map_err(|_| {
377                CliError::custom("Please provide the `--endpoint` or set the `ENDPOINT` environment variable.").into()
378            })
379        }
380    }
381}
382
383/// Returns the network name.
384/// If the `--network` options is not provided, it will default to the environment variable.
385pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
386    match network {
387        Some(network) => Ok(*network),
388        None => {
389            // Load the network from the environment.
390            let network = std::env::var("NETWORK").map_err(|_| {
391                CliError::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
392            })?;
393            // Parse the network.
394            Ok(NetworkName::from_str(&network)?)
395        }
396    }
397}
398
399/// Returns the private key.
400/// If the `--private-key` options is not provided, it will default to the environment variable.
401pub fn get_private_key<N: Network>(private_key: &Option<String>) -> Result<PrivateKey<N>> {
402    match private_key {
403        Some(private_key) => Ok(PrivateKey::<N>::from_str(private_key)?),
404        None => {
405            // Load the private key from the environment.
406            let private_key = std::env::var("PRIVATE_KEY")
407                .map_err(|e| CliError::custom(format!("Failed to load `PRIVATE_KEY` from the environment: {e}")))?;
408            // Parse the private key.
409            Ok(PrivateKey::<N>::from_str(&private_key)?)
410        }
411    }
412}
413
414/// Returns whether the devnet flag is set.
415/// If the `--devnet` flag is not set, check if the environment variable is set, otherwise default to `false`.
416pub fn get_is_devnet(devnet: bool) -> bool {
417    if devnet { true } else { std::env::var("DEVNET").is_ok() }
418}
419
420#[cfg(test)]
421mod test {
422    use snarkvm::prelude::ConsensusVersion;
423
424    #[test]
425    fn test_latest_consensus_version() {
426        assert_eq!(ConsensusVersion::latest(), ConsensusVersion::V10); // If this fails, update the test and any code that matches on `ConsensusVersion`.
427    }
428}