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