1use 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#[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#[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#[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
142fn 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
148fn 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
154fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
156 match record.starts_with("record1") {
157 true => {
158 let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
160 let view_key = ViewKey::try_from(private_key)?;
162 Ok(ciphertext.decrypt(&view_key)?)
164 }
165 false => Ok(Record::<N, Plaintext<N>>::from_str(record)?),
166 }
167}
168
169#[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 let base_fees = fee_options.base_fees.clone();
178 let priority_fees = fee_options.priority_fees.clone();
180 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 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#[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
217pub 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 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 None => {
241 println!("Attempting to determine the consensus version from the latest block height at {endpoint}...");
242 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 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
266pub fn check_consensus_version_mismatch(
268 consensus_version: ConsensusVersion,
269 endpoint: &str,
270 network: NetworkName,
271) -> anyhow::Result<()> {
272 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
284pub fn get_consensus_version_from_height(seek_height: u32, heights: &[u32]) -> Result<ConsensusVersion> {
287 let index = match heights.binary_search_by(|height| height.cmp(&seek_height)) {
289 Ok(index) => index,
291 Err(index) => {
293 if index == 0 {
294 return Err(CliError::custom("Expected consensus version 1 to exist at height 0.").into());
295 } else {
296 index - 1
298 }
299 }
300 };
301 Ok(number_to_consensus_version(index + 1))
303}
304
305pub 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
323pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
328 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 {
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
354pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
356 ensure!(heights[0] == 0, "Genesis height must be 0.");
358 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#[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
378pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
381 match endpoint {
382 Some(endpoint) => Ok(endpoint.clone()),
383 None => {
384 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
392pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
395 match network {
396 Some(network) => Ok(*network),
397 None => {
398 let network = std::env::var("NETWORK").map_err(|_| {
400 CliError::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
401 })?;
402 Ok(NetworkName::from_str(&network)?)
404 }
405 }
406}
407
408pub 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 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 Ok(PrivateKey::<N>::from_str(&private_key)?)
419 }
420 }
421}
422
423pub 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); }
437}