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, Default)]
35pub struct BuildOptions {
36 #[clap(long, help = "Build tests along with the main program and dependencies.")]
37 pub build_tests: bool,
38 #[clap(long, help = "Don't use the dependency cache.")]
39 pub no_cache: bool,
40 #[clap(long, help = "Don't use the local source code.")]
41 pub no_local: bool,
42 #[clap(long, help = "Resolve git dependencies from the lock file and local cache only; don't fetch from remotes.")]
43 pub offline: bool,
44 #[clap(
45 long,
46 help = "Print the program checksum and the checksum of each entry and view function (the `std::prog::function_checksum` targets)."
47 )]
48 pub checksums: bool,
49 #[clap(skip)]
50 pub no_std: bool,
51}
52
53#[derive(Parser, Clone, Debug)]
56pub struct EnvOptions {
57 #[clap(
58 long,
59 help = "The network type to use. e.g `mainnet`, `testnet, and `canary`. Overrides the `NETWORK` environment variable in your shell or `.env` file.",
60 global = true
61 )]
62 pub(crate) network: Option<NetworkName>,
63 #[clap(
64 long,
65 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.",
66 global = true
67 )]
68 pub(crate) endpoint: Option<String>,
69 #[clap(
70 long,
71 env = "NETWORK_RETRIES",
72 help = "Number of times to retry a failed network request before giving up.",
73 default_value = "2"
74 )]
75 pub(crate) network_retries: u32,
76}
77
78impl Default for EnvOptions {
79 fn default() -> Self {
80 Self { network: None, endpoint: None, network_retries: 2 }
81 }
82}
83
84#[derive(Parser, Clone, Debug, Default)]
87pub struct PrivateKeyOptions {
88 #[clap(
89 long,
90 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.",
91 global = true
92 )]
93 pub(crate) private_key: Option<String>,
94}
95
96#[derive(Parser, Clone, Debug, Default)]
99pub struct ConsensusOptions {
100 #[clap(
101 long,
102 help = "Whether the network is a devnet. If not set, defaults to the `DEVNET` environment variable in your shell."
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 )]
110 pub(crate) consensus_heights: Option<Vec<u32>>,
111}
112
113#[derive(Parser, Clone, Debug, Default)]
115pub struct FeeOptions {
116 #[clap(
117 long,
118 help = "Priority fee in microcredits, delimited by `|`, and used in order. The fees must either be valid `u64` or `default`. Defaults to 0.",
119 value_delimiter = '|',
120 value_parser = parse_amount
121 )]
122 pub(crate) priority_fees: Vec<Option<u64>>,
123 #[clap(
124 short,
125 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.",
126 long,
127 value_delimiter = '|',
128 value_parser = parse_record_string,
129 )]
130 fee_records: Vec<Option<String>>,
131}
132
133fn parse_amount(s: &str) -> Result<Option<u64>, String> {
135 let trimmed = s.trim();
136 if trimmed == "default" { Ok(None) } else { trimmed.parse::<u64>().map_err(|e| e.to_string()).map(Some) }
137}
138
139fn parse_record_string(s: &str) -> Result<Option<String>, String> {
141 let trimmed = s.trim();
142 if trimmed == "default" { Ok(None) } else { Ok(Some(trimmed.to_string())) }
143}
144
145fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
147 match record.starts_with("record1") {
148 true => {
149 let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
151 let view_key = ViewKey::try_from(private_key)?;
153 Ok(ciphertext.decrypt(&view_key)?)
155 }
156 false => Ok(Record::<N, Plaintext<N>>::from_str(record)?),
157 }
158}
159
160#[allow(clippy::type_complexity)]
162pub fn parse_fee_options<N: Network>(
163 private_key: &PrivateKey<N>,
164 fee_options: &FeeOptions,
165 k: usize,
166) -> Result<Vec<(Option<u64>, Option<Record<N, Plaintext<N>>>)>> {
167 let priority_fees = fee_options.priority_fees.clone();
169 let parse_record = |record: &Option<String>| record.as_ref().map(|r| parse_record::<N>(private_key, r)).transpose();
171 let fee_records = fee_options.fee_records.iter().map(parse_record).collect::<Result<Vec<_>>>()?;
172
173 let priority_fees = priority_fees.into_iter().chain(iter::repeat(None)).take(k);
175 let fee_records = fee_records.into_iter().chain(iter::repeat(None)).take(k);
176
177 Ok(priority_fees.zip(fee_records).collect())
178}
179
180#[derive(Parser, Clone, Debug, Default)]
182pub struct ExtraOptions {
183 #[clap(
184 short,
185 long,
186 help = "Don't ask for confirmation. DO NOT SET THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING",
187 default_value = "false"
188 )]
189 pub(crate) yes: bool,
190 #[clap(
191 long,
192 help = "Consensus version to use. If one is not provided, the CLI will attempt to determine it from the latest block."
193 )]
194 pub(crate) consensus_version: Option<u8>,
195 #[clap(
196 long,
197 help = "Seconds to wait for a block to appear when searching for a transaction.",
198 default_value = "8"
199 )]
200 pub(crate) max_wait: usize,
201 #[clap(long, help = "Number of blocks to look at when searching for a transaction.", default_value = "12")]
202 pub(crate) blocks_to_check: usize,
203}
204
205pub fn get_consensus_version(
208 consensus_version: &Option<u8>,
209 endpoint: &str,
210 network: NetworkName,
211 heights: &[u32],
212 context: &Context,
213 network_retries: u32,
214) -> Result<ConsensusVersion> {
215 let result = match consensus_version {
217 Some(1) => Ok(ConsensusVersion::V1),
218 Some(2) => Ok(ConsensusVersion::V2),
219 Some(3) => Ok(ConsensusVersion::V3),
220 Some(4) => Ok(ConsensusVersion::V4),
221 Some(5) => Ok(ConsensusVersion::V5),
222 Some(6) => Ok(ConsensusVersion::V6),
223 Some(7) => Ok(ConsensusVersion::V7),
224 Some(8) => Ok(ConsensusVersion::V8),
225 Some(9) => Ok(ConsensusVersion::V9),
226 Some(10) => Ok(ConsensusVersion::V10),
227 Some(11) => Ok(ConsensusVersion::V11),
228 Some(12) => Ok(ConsensusVersion::V12),
229 Some(13) => Ok(ConsensusVersion::V13),
230 Some(14) => Ok(ConsensusVersion::V14),
231 Some(15) => Ok(ConsensusVersion::V15),
232 Some(16) => Ok(ConsensusVersion::V16),
233 Some(17) => Ok(ConsensusVersion::V17),
234 Some(18) => Ok(ConsensusVersion::V18),
235 Some(19) => Ok(ConsensusVersion::V19),
236 Some(20) => Ok(ConsensusVersion::V20),
237 Some(21) => Ok(ConsensusVersion::V21),
238 Some(22) => Ok(ConsensusVersion::V22),
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, network_retries)
244 .and_then(|current_block_height| get_consensus_version_from_height(current_block_height, heights))
245 .map_err(|_| {
246 crate::errors::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(crate::errors::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, network_retries)
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 network_retries: u32,
272) -> anyhow::Result<()> {
273 if let Ok(response) = fetch_from_network(&format!("{endpoint}/{network}/consensus_version"), network_retries)
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(crate::errors::custom("Expected consensus version 1 to exist at height 0.").into());
296 } else {
297 index - 1
299 }
300 }
301 };
302 number_to_consensus_version(index + 1)
304}
305
306pub fn number_to_consensus_version(index: usize) -> Result<ConsensusVersion> {
308 match index {
309 1 => Ok(ConsensusVersion::V1),
310 2 => Ok(ConsensusVersion::V2),
311 3 => Ok(ConsensusVersion::V3),
312 4 => Ok(ConsensusVersion::V4),
313 5 => Ok(ConsensusVersion::V5),
314 6 => Ok(ConsensusVersion::V6),
315 7 => Ok(ConsensusVersion::V7),
316 8 => Ok(ConsensusVersion::V8),
317 9 => Ok(ConsensusVersion::V9),
318 10 => Ok(ConsensusVersion::V10),
319 11 => Ok(ConsensusVersion::V11),
320 12 => Ok(ConsensusVersion::V12),
321 13 => Ok(ConsensusVersion::V13),
322 14 => Ok(ConsensusVersion::V14),
323 15 => Ok(ConsensusVersion::V15),
324 16 => Ok(ConsensusVersion::V16),
325 17 => Ok(ConsensusVersion::V17),
326 18 => Ok(ConsensusVersion::V18),
327 19 => Ok(ConsensusVersion::V19),
328 20 => Ok(ConsensusVersion::V20),
329 21 => Ok(ConsensusVersion::V21),
330 22 => Ok(ConsensusVersion::V22),
331 _ => Err(crate::errors::custom(format!(
332 "Invalid consensus version: {index}. You may need to update Leo to support this version."
333 ))
334 .into()),
335 }
336}
337
338pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
343 if let Ok(heights) = std::env::var("CONSENSUS_VERSION_HEIGHTS") {
345 if let Ok(heights) = heights.split(',').map(|s| s.trim().parse::<u32>()).collect::<Result<Vec<_>, _>>() {
346 return heights;
347 } else {
348 println!(
349 "⚠️ Warning: Failed to parse `CONSENSUS_VERSION_HEIGHTS` environment variable. Falling back to default heights."
350 );
351 }
352 }
353 if is_devnet {
356 TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().map(|(_, v)| v).collect_vec()
357 } else {
358 match network_name {
359 NetworkName::CanaryV0 => CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
360 NetworkName::MainnetV0 => MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
361 NetworkName::TestnetV0 => TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
362 }
363 .into_iter()
364 .map(|(_, v)| v)
365 .collect_vec()
366 }
367}
368
369pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
371 let expected = ConsensusVersion::latest() as usize;
373 ensure!(
374 heights.len() == expected,
375 "expected exactly {expected} consensus heights (one per consensus version), but found {}",
376 heights.len()
377 );
378 ensure!(heights[0] == 0, "Genesis height must be 0.");
380 for window in heights.windows(2) {
382 if window[0] > window[1] || (window[0] == window[1] && window[0] != u32::MAX) {
383 bail!("Heights must increase, except for inactive versions at u32::MAX, but found: {window:?}");
384 }
385 }
386 Ok(())
387}
388
389#[derive(Args, Clone, Debug)]
391pub struct TransactionAction {
392 #[arg(long, help = "Print the transaction to stdout.")]
393 pub print: bool,
394 #[arg(long, help = "Broadcast the transaction to the network.")]
395 pub broadcast: bool,
396 #[arg(long, help = "Save the transaction to the provided directory.")]
397 pub save: Option<String>,
398}
399
400pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
403 match endpoint {
404 Some(endpoint) => Ok(endpoint.clone()),
405 None => {
406 std::env::var("ENDPOINT").map_err(|_| {
408 crate::errors::custom("Please provide the `--endpoint` or set the `ENDPOINT` environment variable.")
409 .into()
410 })
411 }
412 }
413}
414
415pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
418 match network {
419 Some(network) => Ok(*network),
420 None => {
421 let network = std::env::var("NETWORK").map_err(|_| {
423 crate::errors::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
424 })?;
425 Ok(NetworkName::from_str(&network)?)
427 }
428 }
429}
430
431pub fn get_private_key<N: Network>(private_key: &Option<String>) -> Result<PrivateKey<N>> {
434 match private_key {
435 Some(private_key) => Ok(PrivateKey::<N>::from_str(private_key)?),
436 None => {
437 let private_key = std::env::var("PRIVATE_KEY").map_err(|e| {
439 crate::errors::custom(format!("Failed to load `PRIVATE_KEY` from the environment: {e}"))
440 })?;
441 Ok(PrivateKey::<N>::from_str(&private_key)?)
443 }
444 }
445}
446
447pub fn get_is_devnet(devnet: bool) -> bool {
450 if devnet { true } else { std::env::var("DEVNET").is_ok() }
451}
452
453#[cfg(test)]
454mod test {
455 use snarkvm::prelude::ConsensusVersion;
456
457 #[test]
458 fn test_latest_consensus_version() {
459 assert_eq!(ConsensusVersion::latest(), ConsensusVersion::V22); let context = super::Context::new(None, None, false, None).expect("Test context must be valid");
461 for version in [ConsensusVersion::V19, ConsensusVersion::V20, ConsensusVersion::V21, ConsensusVersion::V22] {
462 assert_eq!(super::number_to_consensus_version(version as usize).expect("Valid version"), version);
463 assert_eq!(
464 super::get_consensus_version(
465 &Some(version as u8),
466 "",
467 leo_ast::NetworkName::TestnetV0,
468 &[],
469 &context,
470 0,
471 )
472 .expect("Explicit consensus versions do not require an endpoint"),
473 version
474 );
475 }
476 assert!(super::number_to_consensus_version(23).is_err());
477 }
478
479 #[test]
480 fn test_consensus_activation_heights() {
481 let heights = super::TEST_CONSENSUS_VERSION_HEIGHTS.map(|(_, height)| height);
482 for (version, height) in super::TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().skip(19) {
483 assert_eq!(super::get_consensus_version_from_height(height, &heights).expect("Activation height"), version);
484 assert_eq!(
485 super::get_consensus_version_from_height(height - 1, &heights).expect("Previous height") as u8,
486 version as u8 - 1
487 );
488 }
489 }
490
491 #[test]
492 fn test_validate_consensus_heights() {
493 let n = ConsensusVersion::latest() as u32;
494 let valid: Vec<u32> = (0..n).collect();
496 assert!(super::validate_consensus_heights(&valid).is_ok());
497 assert!(super::validate_consensus_heights(&(0..n - 1).collect::<Vec<_>>()).is_err());
499 assert!(super::validate_consensus_heights(&[]).is_err());
501 for schedule in [
502 super::MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
503 super::TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
504 super::CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
505 ] {
506 assert!(super::validate_consensus_heights(&schedule.map(|(_, height)| height)).is_ok());
507 }
508 let mut duplicate = valid.clone();
509 duplicate[1] = duplicate[0];
510 assert!(super::validate_consensus_heights(&duplicate).is_err());
511 let mut decreasing = valid;
512 decreasing[1] = u32::MAX;
513 assert!(super::validate_consensus_heights(&decreasing).is_err());
514 }
515}