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 Some(12) => Ok(ConsensusVersion::V12),
240 None => {
242 println!("Attempting to determine the consensus version from the latest block height at {endpoint}...");
243 get_latest_block_height(endpoint, network, context)
245 .and_then(|current_block_height| get_consensus_version_from_height(current_block_height, heights))
246 .map_err(|_| {
247 CliError::custom(
248 "Failed to get consensus version. Ensure that your endpoint is valid or provide an explicit version to use via `--consensus-version`",
249 )
250 .into()
251 })
252 }
253 Some(version) => Err(CliError::custom(format!("Invalid consensus version: {version}")).into()),
254 };
255
256 if let Ok(consensus_version) = result
259 && let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network)
260 {
261 println!("⚠️ Warning: {e}");
262 }
263
264 result
265}
266
267pub fn check_consensus_version_mismatch(
269 consensus_version: ConsensusVersion,
270 endpoint: &str,
271 network: NetworkName,
272) -> anyhow::Result<()> {
273 if let Ok(response) = fetch_from_network(&format!("{endpoint}/{network}/consensus_version"))
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
285pub fn get_consensus_version_from_height(seek_height: u32, heights: &[u32]) -> Result<ConsensusVersion> {
288 let index = match heights.binary_search_by(|height| height.cmp(&seek_height)) {
290 Ok(index) => index,
292 Err(index) => {
294 if index == 0 {
295 return Err(CliError::custom("Expected consensus version 1 to exist at height 0.").into());
296 } else {
297 index - 1
299 }
300 }
301 };
302 Ok(number_to_consensus_version(index + 1))
304}
305
306pub fn number_to_consensus_version(index: usize) -> ConsensusVersion {
308 match index {
309 1 => ConsensusVersion::V1,
310 2 => ConsensusVersion::V2,
311 3 => ConsensusVersion::V3,
312 4 => ConsensusVersion::V4,
313 5 => ConsensusVersion::V5,
314 6 => ConsensusVersion::V6,
315 7 => ConsensusVersion::V7,
316 8 => ConsensusVersion::V8,
317 9 => ConsensusVersion::V9,
318 10 => ConsensusVersion::V10,
319 11 => ConsensusVersion::V11,
320 12 => ConsensusVersion::V12,
321 _ => panic!("Invalid consensus version: {index}"),
322 }
323}
324
325pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
330 if let Ok(heights) = std::env::var("CONSENSUS_VERSION_HEIGHTS") {
332 if let Ok(heights) = heights.split(',').map(|s| s.trim().parse::<u32>()).collect::<Result<Vec<_>, _>>() {
333 return heights;
334 } else {
335 println!(
336 "⚠️ Warning: Failed to parse `CONSENSUS_VERSION_HEIGHTS` environment variable. Falling back to default heights."
337 );
338 }
339 }
340 if is_devnet {
343 TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().map(|(_, v)| v).collect_vec()
344 } else {
345 match network_name {
346 NetworkName::CanaryV0 => CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
347 NetworkName::MainnetV0 => MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
348 NetworkName::TestnetV0 => TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
349 }
350 .into_iter()
351 .map(|(_, v)| v)
352 .collect_vec()
353 }
354}
355
356pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
358 ensure!(heights[0] == 0, "Genesis height must be 0.");
360 for window in heights.windows(2) {
362 if window[0] >= window[1] {
363 bail!("Heights must be strictly increasing, but found: {window:?}");
364 }
365 }
366 Ok(())
367}
368
369#[derive(Args, Clone, Debug)]
371pub struct TransactionAction {
372 #[arg(long, help = "Print the transaction to stdout.")]
373 pub print: bool,
374 #[arg(long, help = "Broadcast the transaction to the network.")]
375 pub broadcast: bool,
376 #[arg(long, help = "Save the transaction to the provided directory.")]
377 pub save: Option<String>,
378}
379
380pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
383 match endpoint {
384 Some(endpoint) => Ok(endpoint.clone()),
385 None => {
386 std::env::var("ENDPOINT").map_err(|_| {
388 CliError::custom("Please provide the `--endpoint` or set the `ENDPOINT` environment variable.").into()
389 })
390 }
391 }
392}
393
394pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
397 match network {
398 Some(network) => Ok(*network),
399 None => {
400 let network = std::env::var("NETWORK").map_err(|_| {
402 CliError::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
403 })?;
404 Ok(NetworkName::from_str(&network)?)
406 }
407 }
408}
409
410pub fn get_private_key<N: Network>(private_key: &Option<String>) -> Result<PrivateKey<N>> {
413 match private_key {
414 Some(private_key) => Ok(PrivateKey::<N>::from_str(private_key)?),
415 None => {
416 let private_key = std::env::var("PRIVATE_KEY")
418 .map_err(|e| CliError::custom(format!("Failed to load `PRIVATE_KEY` from the environment: {e}")))?;
419 Ok(PrivateKey::<N>::from_str(&private_key)?)
421 }
422 }
423}
424
425pub fn get_is_devnet(devnet: bool) -> bool {
428 if devnet { true } else { std::env::var("DEVNET").is_ok() }
429}
430
431#[cfg(test)]
432mod test {
433 use snarkvm::prelude::ConsensusVersion;
434
435 #[test]
436 fn test_latest_consensus_version() {
437 assert_eq!(ConsensusVersion::latest(), ConsensusVersion::V12); }
439}