use super::*;
use anyhow::{bail, ensure};
use itertools::Itertools;
use leo_ast::NetworkName;
use leo_package::fetch_from_network;
use snarkvm::prelude::{
CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
ConsensusVersion,
MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
TEST_CONSENSUS_VERSION_HEIGHTS,
TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
};
pub const DEFAULT_ENDPOINT: &str = "https://api.explorer.provable.com/v1";
#[derive(Parser, Clone, Debug, Default)]
pub struct BuildOptions {
#[clap(long, help = "Build tests along with the main program and dependencies.")]
pub build_tests: bool,
#[clap(long, help = "Don't use the dependency cache.")]
pub no_cache: bool,
#[clap(long, help = "Don't use the local source code.")]
pub no_local: bool,
#[clap(long, help = "Resolve git dependencies from the lock file and local cache only; don't fetch from remotes.")]
pub offline: bool,
#[clap(
long,
help = "Print the program checksum and the checksum of each entry and view function (the `std::prog::function_checksum` targets)."
)]
pub checksums: bool,
#[clap(skip)]
pub no_std: bool,
}
#[derive(Parser, Clone, Debug)]
pub struct EnvOptions {
#[clap(
long,
help = "The network type to use. e.g `mainnet`, `testnet, and `canary`. Overrides the `NETWORK` environment variable in your shell or `.env` file.",
global = true
)]
pub(crate) network: Option<NetworkName>,
#[clap(
long,
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.",
global = true
)]
pub(crate) endpoint: Option<String>,
#[clap(
long,
env = "NETWORK_RETRIES",
help = "Number of times to retry a failed network request before giving up.",
default_value = "2"
)]
pub(crate) network_retries: u32,
}
impl Default for EnvOptions {
fn default() -> Self {
Self { network: None, endpoint: None, network_retries: 2 }
}
}
#[derive(Parser, Clone, Debug, Default)]
pub struct PrivateKeyOptions {
#[clap(
long,
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.",
global = true
)]
pub(crate) private_key: Option<String>,
}
#[derive(Parser, Clone, Debug, Default)]
pub struct ConsensusOptions {
#[clap(
long,
help = "Whether the network is a devnet. If not set, defaults to the `DEVNET` environment variable in your shell."
)]
pub(crate) devnet: bool,
#[clap(
long,
help = "Optional consensus heights to use. This should only be set if you are using a custom devnet.",
value_delimiter = ','
)]
pub(crate) consensus_heights: Option<Vec<u32>>,
}
#[derive(Parser, Clone, Debug, Default)]
pub struct FeeOptions {
#[clap(
long,
help = "Priority fee in microcredits, delimited by `|`, and used in order. The fees must either be valid `u64` or `default`. Defaults to 0.",
value_delimiter = '|',
value_parser = parse_amount
)]
pub(crate) priority_fees: Vec<Option<u64>>,
#[clap(
short,
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.",
long,
value_delimiter = '|',
value_parser = parse_record_string,
)]
fee_records: Vec<Option<String>>,
}
fn parse_amount(s: &str) -> Result<Option<u64>, String> {
let trimmed = s.trim();
if trimmed == "default" { Ok(None) } else { trimmed.parse::<u64>().map_err(|e| e.to_string()).map(Some) }
}
fn parse_record_string(s: &str) -> Result<Option<String>, String> {
let trimmed = s.trim();
if trimmed == "default" { Ok(None) } else { Ok(Some(trimmed.to_string())) }
}
fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
match record.starts_with("record1") {
true => {
let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
let view_key = ViewKey::try_from(private_key)?;
Ok(ciphertext.decrypt(&view_key)?)
}
false => Ok(Record::<N, Plaintext<N>>::from_str(record)?),
}
}
#[allow(clippy::type_complexity)]
pub fn parse_fee_options<N: Network>(
private_key: &PrivateKey<N>,
fee_options: &FeeOptions,
k: usize,
) -> Result<Vec<(Option<u64>, Option<Record<N, Plaintext<N>>>)>> {
let priority_fees = fee_options.priority_fees.clone();
let parse_record = |record: &Option<String>| record.as_ref().map(|r| parse_record::<N>(private_key, r)).transpose();
let fee_records = fee_options.fee_records.iter().map(parse_record).collect::<Result<Vec<_>>>()?;
let priority_fees = priority_fees.into_iter().chain(iter::repeat(None)).take(k);
let fee_records = fee_records.into_iter().chain(iter::repeat(None)).take(k);
Ok(priority_fees.zip(fee_records).collect())
}
#[derive(Parser, Clone, Debug, Default)]
pub struct ExtraOptions {
#[clap(
short,
long,
help = "Don't ask for confirmation. DO NOT SET THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING",
default_value = "false"
)]
pub(crate) yes: bool,
#[clap(
long,
help = "Consensus version to use. If one is not provided, the CLI will attempt to determine it from the latest block."
)]
pub(crate) consensus_version: Option<u8>,
#[clap(
long,
help = "Seconds to wait for a block to appear when searching for a transaction.",
default_value = "8"
)]
pub(crate) max_wait: usize,
#[clap(long, help = "Number of blocks to look at when searching for a transaction.", default_value = "12")]
pub(crate) blocks_to_check: usize,
}
pub fn get_consensus_version(
consensus_version: &Option<u8>,
endpoint: &str,
network: NetworkName,
heights: &[u32],
context: &Context,
network_retries: u32,
) -> Result<ConsensusVersion> {
let result = match consensus_version {
Some(1) => Ok(ConsensusVersion::V1),
Some(2) => Ok(ConsensusVersion::V2),
Some(3) => Ok(ConsensusVersion::V3),
Some(4) => Ok(ConsensusVersion::V4),
Some(5) => Ok(ConsensusVersion::V5),
Some(6) => Ok(ConsensusVersion::V6),
Some(7) => Ok(ConsensusVersion::V7),
Some(8) => Ok(ConsensusVersion::V8),
Some(9) => Ok(ConsensusVersion::V9),
Some(10) => Ok(ConsensusVersion::V10),
Some(11) => Ok(ConsensusVersion::V11),
Some(12) => Ok(ConsensusVersion::V12),
Some(13) => Ok(ConsensusVersion::V13),
Some(14) => Ok(ConsensusVersion::V14),
Some(15) => Ok(ConsensusVersion::V15),
Some(16) => Ok(ConsensusVersion::V16),
Some(17) => Ok(ConsensusVersion::V17),
Some(18) => Ok(ConsensusVersion::V18),
None => {
println!("Attempting to determine the consensus version from the latest block height at {endpoint}...");
get_latest_block_height(endpoint, network, context, network_retries)
.and_then(|current_block_height| get_consensus_version_from_height(current_block_height, heights))
.map_err(|_| {
crate::errors::custom(
"Failed to get consensus version. Ensure that your endpoint is valid or provide an explicit version to use via `--consensus-version`",
)
.into()
})
}
Some(version) => Err(crate::errors::custom(format!("Invalid consensus version: {version}")).into()),
};
if let Ok(consensus_version) = result
&& let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network, network_retries)
{
println!("⚠️ Warning: {e}");
}
result
}
pub fn check_consensus_version_mismatch(
consensus_version: ConsensusVersion,
endpoint: &str,
network: NetworkName,
network_retries: u32,
) -> anyhow::Result<()> {
if let Ok(response) = fetch_from_network(&format!("{endpoint}/{network}/consensus_version"), network_retries)
&& let Ok(response) = response.parse::<u8>()
{
let consensus_version = consensus_version as u8;
if response != consensus_version {
bail!("Expected consensus version {consensus_version} but found {response} at {endpoint}",);
}
}
Ok(())
}
pub fn get_consensus_version_from_height(seek_height: u32, heights: &[u32]) -> Result<ConsensusVersion> {
let index = match heights.binary_search_by(|height| height.cmp(&seek_height)) {
Ok(index) => index,
Err(index) => {
if index == 0 {
return Err(crate::errors::custom("Expected consensus version 1 to exist at height 0.").into());
} else {
index - 1
}
}
};
number_to_consensus_version(index + 1)
}
pub fn number_to_consensus_version(index: usize) -> Result<ConsensusVersion> {
match index {
1 => Ok(ConsensusVersion::V1),
2 => Ok(ConsensusVersion::V2),
3 => Ok(ConsensusVersion::V3),
4 => Ok(ConsensusVersion::V4),
5 => Ok(ConsensusVersion::V5),
6 => Ok(ConsensusVersion::V6),
7 => Ok(ConsensusVersion::V7),
8 => Ok(ConsensusVersion::V8),
9 => Ok(ConsensusVersion::V9),
10 => Ok(ConsensusVersion::V10),
11 => Ok(ConsensusVersion::V11),
12 => Ok(ConsensusVersion::V12),
13 => Ok(ConsensusVersion::V13),
14 => Ok(ConsensusVersion::V14),
15 => Ok(ConsensusVersion::V15),
16 => Ok(ConsensusVersion::V16),
17 => Ok(ConsensusVersion::V17),
18 => Ok(ConsensusVersion::V18),
_ => Err(crate::errors::custom(format!(
"Invalid consensus version: {index}. You may need to update Leo to support this version."
))
.into()),
}
}
pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
if let Ok(heights) = std::env::var("CONSENSUS_VERSION_HEIGHTS") {
if let Ok(heights) = heights.split(',').map(|s| s.trim().parse::<u32>()).collect::<Result<Vec<_>, _>>() {
return heights;
} else {
println!(
"⚠️ Warning: Failed to parse `CONSENSUS_VERSION_HEIGHTS` environment variable. Falling back to default heights."
);
}
}
if is_devnet {
TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().map(|(_, v)| v).collect_vec()
} else {
match network_name {
NetworkName::CanaryV0 => CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
NetworkName::MainnetV0 => MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
NetworkName::TestnetV0 => TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
}
.into_iter()
.map(|(_, v)| v)
.collect_vec()
}
}
pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
let expected = ConsensusVersion::latest() as usize;
ensure!(
heights.len() == expected,
"expected exactly {expected} consensus heights (one per consensus version), but found {}",
heights.len()
);
ensure!(heights[0] == 0, "Genesis height must be 0.");
for window in heights.windows(2) {
if window[0] >= window[1] {
bail!("Heights must be strictly increasing, but found: {window:?}");
}
}
Ok(())
}
#[derive(Args, Clone, Debug)]
pub struct TransactionAction {
#[arg(long, help = "Print the transaction to stdout.")]
pub print: bool,
#[arg(long, help = "Broadcast the transaction to the network.")]
pub broadcast: bool,
#[arg(long, help = "Save the transaction to the provided directory.")]
pub save: Option<String>,
}
pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
match endpoint {
Some(endpoint) => Ok(endpoint.clone()),
None => {
std::env::var("ENDPOINT").map_err(|_| {
crate::errors::custom("Please provide the `--endpoint` or set the `ENDPOINT` environment variable.")
.into()
})
}
}
}
pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
match network {
Some(network) => Ok(*network),
None => {
let network = std::env::var("NETWORK").map_err(|_| {
crate::errors::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
})?;
Ok(NetworkName::from_str(&network)?)
}
}
}
pub fn get_private_key<N: Network>(private_key: &Option<String>) -> Result<PrivateKey<N>> {
match private_key {
Some(private_key) => Ok(PrivateKey::<N>::from_str(private_key)?),
None => {
let private_key = std::env::var("PRIVATE_KEY").map_err(|e| {
crate::errors::custom(format!("Failed to load `PRIVATE_KEY` from the environment: {e}"))
})?;
Ok(PrivateKey::<N>::from_str(&private_key)?)
}
}
}
pub fn get_is_devnet(devnet: bool) -> bool {
if devnet { true } else { std::env::var("DEVNET").is_ok() }
}
#[cfg(test)]
mod test {
use snarkvm::prelude::ConsensusVersion;
#[test]
fn test_latest_consensus_version() {
assert_eq!(ConsensusVersion::latest(), ConsensusVersion::V18); }
#[test]
fn test_validate_consensus_heights() {
let n = ConsensusVersion::latest() as u32;
let valid: Vec<u32> = (0..n).collect();
assert!(super::validate_consensus_heights(&valid).is_ok());
assert!(super::validate_consensus_heights(&(0..n - 1).collect::<Vec<_>>()).is_err());
assert!(super::validate_consensus_heights(&[]).is_err());
}
}