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
30#[derive(Parser, Clone, Debug)]
33pub struct BuildOptions {
34 #[clap(long, help = "Enables offline mode.")]
35 pub offline: bool,
36 #[clap(long, help = "Enable spans in AST snapshots.")]
37 pub enable_ast_spans: bool,
38 #[clap(long, help = "Enables dead code elimination in the compiler.", default_value = "true")]
39 pub enable_dce: bool,
40 #[clap(long, help = "Max depth to type check nested conditionals.", default_value = "10")]
41 pub conditional_block_max_depth: usize,
42 #[clap(long, help = "Disable type checking of nested conditional branches in finalize scope.")]
43 pub disable_conditional_branch_type_checking: bool,
44 #[clap(long, help = "Write an AST snapshot immediately after parsing.")]
45 pub enable_initial_ast_snapshot: bool,
46 #[clap(long, help = "Writes all AST snapshots for the different compiler phases.")]
47 pub enable_all_ast_snapshots: bool,
48 #[clap(long, help = "Comma separated list of passes whose AST snapshots to capture.", value_delimiter = ',', num_args = 1..)]
49 pub ast_snapshots: Vec<String>,
50 #[clap(long, help = "Build tests along with the main program and dependencies.")]
51 pub build_tests: bool,
52 #[clap(long, help = "Don't use the dependency cache.")]
53 pub no_cache: bool,
54 #[clap(long, help = "Don't use the local source code.")]
55 pub no_local: bool,
56}
57
58impl Default for BuildOptions {
59 fn default() -> Self {
60 Self {
61 offline: false,
62 enable_ast_spans: false,
63 enable_dce: true,
64 conditional_block_max_depth: 10,
65 disable_conditional_branch_type_checking: false,
66 enable_initial_ast_snapshot: false,
67 enable_all_ast_snapshots: false,
68 ast_snapshots: Vec::new(),
69 build_tests: false,
70 no_cache: false,
71 no_local: false,
72 }
73 }
74}
75
76#[derive(Parser, Clone, Debug, Default)]
78pub struct EnvOptions {
79 #[clap(
80 long,
81 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."
82 )]
83 pub(crate) private_key: Option<String>,
84 #[clap(
85 long,
86 help = "The network type to use. e.g `mainnet`, `testnet, and `canary`. Overrides the `NETWORK` environment variable in your shell or `.env` file."
87 )]
88 pub(crate) network: Option<NetworkName>,
89 #[clap(
90 long,
91 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."
92 )]
93 pub(crate) endpoint: Option<String>,
94 #[clap(
95 long,
96 help = "Whether the network is a devnet. If not set, defaults to the `DEVNET` environment variable in your shell or `.env` file."
97 )]
98 pub(crate) devnet: bool,
99 #[clap(
100 long,
101 help = "Optional consensus heights to use. This should only be set if you are using a custom devnet.",
102 value_delimiter = ','
103 )]
104 pub(crate) consensus_heights: Option<Vec<u32>>,
105}
106
107#[derive(Parser, Clone, Debug, Default)]
109pub struct FeeOptions {
110 #[clap(
111 long,
112 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.",
113 hide = true,
114 value_delimiter = '|',
115 value_parser = parse_amount
116 )]
117 pub(crate) base_fees: Vec<Option<u64>>,
118 #[clap(
119 long,
120 help = "Priority fee in microcredits, delimited by `|`, and used in order. The fees must either be valid `u64` or `default`. Defaults to 0.",
121 value_delimiter = '|',
122 value_parser = parse_amount
123 )]
124 pub(crate) priority_fees: Vec<Option<u64>>,
125 #[clap(
126 short,
127 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.",
128 long,
129 value_delimiter = '|',
130 value_parser = parse_record_string,
131 )]
132 fee_records: Vec<Option<String>>,
133}
134
135fn parse_amount(s: &str) -> Result<Option<u64>, String> {
137 let trimmed = s.trim();
138 if trimmed == "default" { Ok(None) } else { trimmed.parse::<u64>().map_err(|e| e.to_string()).map(Some) }
139}
140
141fn parse_record_string(s: &str) -> Result<Option<String>, String> {
143 let trimmed = s.trim();
144 if trimmed == "default" { Ok(None) } else { Ok(Some(trimmed.to_string())) }
145}
146
147fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
149 match record.starts_with("record1") {
150 true => {
151 let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
153 let view_key = ViewKey::try_from(private_key)?;
155 Ok(ciphertext.decrypt(&view_key)?)
157 }
158 false => Ok(Record::<N, Plaintext<N>>::from_str(record)?),
159 }
160}
161
162#[allow(clippy::type_complexity)]
164pub fn parse_fee_options<N: Network>(
165 private_key: &PrivateKey<N>,
166 fee_options: &FeeOptions,
167 k: usize,
168) -> Result<Vec<(Option<u64>, Option<u64>, Option<Record<N, Plaintext<N>>>)>> {
169 let base_fees = fee_options.base_fees.clone();
171 let priority_fees = fee_options.priority_fees.clone();
173 let parse_record = |record: &Option<String>| record.as_ref().map(|r| parse_record::<N>(private_key, r)).transpose();
175 let fee_records = fee_options.fee_records.iter().map(parse_record).collect::<Result<Vec<_>>>()?;
176
177 let base_fees = base_fees.into_iter().chain(iter::repeat(None)).take(k);
179 let priority_fees = priority_fees.into_iter().chain(iter::repeat(None)).take(k);
180 let fee_records = fee_records.into_iter().chain(iter::repeat(None)).take(k);
181
182 Ok(base_fees.zip(priority_fees).zip(fee_records).map(|((x, y), z)| (x, y, z)).collect())
183}
184
185#[derive(Parser, Clone, Debug, Default)]
187pub struct ExtraOptions {
188 #[clap(
189 short,
190 long,
191 help = "Don't ask for confirmation. DO NOT SET THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING",
192 default_value = "false"
193 )]
194 pub(crate) yes: bool,
195 #[clap(
196 long,
197 help = "Consensus version to use. If one is not provided, the CLI will attempt to determine it from the latest block."
198 )]
199 pub(crate) consensus_version: Option<u8>,
200 #[clap(
201 long,
202 help = "Seconds to wait for a block to appear when searching for a transaction.",
203 default_value = "8"
204 )]
205 pub(crate) max_wait: usize,
206 #[clap(long, help = "Number of blocks to look at when searching for a transaction.", default_value = "12")]
207 pub(crate) blocks_to_check: usize,
208}
209
210pub fn get_consensus_version(
213 consensus_version: &Option<u8>,
214 endpoint: &str,
215 network: NetworkName,
216 heights: &[u32],
217 context: &Context,
218) -> Result<ConsensusVersion> {
219 let result = match consensus_version {
221 Some(1) => Ok(ConsensusVersion::V1),
222 Some(2) => Ok(ConsensusVersion::V2),
223 Some(3) => Ok(ConsensusVersion::V3),
224 Some(4) => Ok(ConsensusVersion::V4),
225 Some(5) => Ok(ConsensusVersion::V5),
226 Some(6) => Ok(ConsensusVersion::V6),
227 Some(7) => Ok(ConsensusVersion::V7),
228 Some(8) => Ok(ConsensusVersion::V8),
229 Some(9) => Ok(ConsensusVersion::V9),
230 Some(10) => Ok(ConsensusVersion::V10),
231 None => {
233 println!("Attempting to determine the consensus version from the latest block height at {endpoint}...");
234 get_latest_block_height(endpoint, network, context)
236 .and_then(|current_block_height| get_consensus_version_from_height(current_block_height, heights))
237 .map_err(|_| {
238 CliError::custom(
239 "Failed to get consensus version. Ensure that your endpoint is valid or provide an explicit version to use via `--consensus-version`",
240 )
241 .into()
242 })
243 }
244 Some(version) => Err(CliError::custom(format!("Invalid consensus version: {version}")).into()),
245 };
246
247 if let Ok(consensus_version) = result {
250 if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
251 println!("⚠️ Warning: {e}");
252 }
253 }
254
255 result
256}
257
258pub fn check_consensus_version_mismatch(
260 consensus_version: ConsensusVersion,
261 endpoint: &str,
262 network: NetworkName,
263) -> anyhow::Result<()> {
264 if let Ok(response) = fetch_from_network(&format!("{endpoint}/{network}/consensus_version")) {
266 if let Ok(response) = response.parse::<u8>() {
267 let consensus_version = consensus_version as u8;
268 if response != consensus_version {
269 bail!("Expected consensus version {consensus_version} but found {response} at {endpoint}",);
270 }
271 }
272 }
273 Ok(())
274}
275
276pub fn get_consensus_version_from_height(seek_height: u32, heights: &[u32]) -> Result<ConsensusVersion> {
279 let index = match heights.binary_search_by(|height| height.cmp(&seek_height)) {
281 Ok(index) => index,
283 Err(index) => {
285 if index == 0 {
286 return Err(CliError::custom("Expected consensus version 1 to exist at height 0.").into());
287 } else {
288 index - 1
290 }
291 }
292 };
293 Ok(number_to_consensus_version(index + 1))
295}
296
297pub fn number_to_consensus_version(index: usize) -> ConsensusVersion {
299 match index {
300 1 => ConsensusVersion::V1,
301 2 => ConsensusVersion::V2,
302 3 => ConsensusVersion::V3,
303 4 => ConsensusVersion::V4,
304 5 => ConsensusVersion::V5,
305 6 => ConsensusVersion::V6,
306 7 => ConsensusVersion::V7,
307 8 => ConsensusVersion::V8,
308 9 => ConsensusVersion::V9,
309 10 => ConsensusVersion::V10,
310 _ => panic!("Invalid consensus version: {index}"),
311 }
312}
313
314pub fn get_consensus_heights(network_name: NetworkName, is_devnet: bool) -> Vec<u32> {
319 if let Ok(heights) = std::env::var("CONSENSUS_VERSION_HEIGHTS") {
321 if let Ok(heights) = heights.split(',').map(|s| s.trim().parse::<u32>()).collect::<Result<Vec<_>, _>>() {
322 return heights;
323 } else {
324 println!(
325 "⚠️ Warning: Failed to parse `CONSENSUS_VERSION_HEIGHTS` environment variable. Falling back to default heights."
326 );
327 }
328 }
329 if is_devnet {
332 TEST_CONSENSUS_VERSION_HEIGHTS.into_iter().map(|(_, v)| v).collect_vec()
333 } else {
334 match network_name {
335 NetworkName::CanaryV0 => CANARY_V0_CONSENSUS_VERSION_HEIGHTS,
336 NetworkName::MainnetV0 => MAINNET_V0_CONSENSUS_VERSION_HEIGHTS,
337 NetworkName::TestnetV0 => TESTNET_V0_CONSENSUS_VERSION_HEIGHTS,
338 }
339 .into_iter()
340 .map(|(_, v)| v)
341 .collect_vec()
342 }
343}
344
345pub fn validate_consensus_heights(heights: &[u32]) -> anyhow::Result<()> {
347 ensure!(heights[0] == 0, "Genesis height must be 0.");
349 for window in heights.windows(2) {
351 if window[0] >= window[1] {
352 bail!("Heights must be strictly increasing, but found: {window:?}");
353 }
354 }
355 Ok(())
356}
357
358#[derive(Args, Clone, Debug)]
360pub struct TransactionAction {
361 #[arg(long, help = "Print the transaction to stdout.")]
362 pub print: bool,
363 #[arg(long, help = "Broadcast the transaction to the network.")]
364 pub broadcast: bool,
365 #[arg(long, help = "Save the transaction to the provided directory.")]
366 pub save: Option<String>,
367}
368
369pub fn get_endpoint(endpoint: &Option<String>) -> Result<String> {
372 match endpoint {
373 Some(endpoint) => Ok(endpoint.clone()),
374 None => {
375 std::env::var("ENDPOINT").map_err(|_| {
377 CliError::custom("Please provide the `--endpoint` or set the `ENDPOINT` environment variable.").into()
378 })
379 }
380 }
381}
382
383pub fn get_network(network: &Option<NetworkName>) -> Result<NetworkName> {
386 match network {
387 Some(network) => Ok(*network),
388 None => {
389 let network = std::env::var("NETWORK").map_err(|_| {
391 CliError::custom("Please provide the `--network` or set the `NETWORK` environment variable.")
392 })?;
393 Ok(NetworkName::from_str(&network)?)
395 }
396 }
397}
398
399pub fn get_private_key<N: Network>(private_key: &Option<String>) -> Result<PrivateKey<N>> {
402 match private_key {
403 Some(private_key) => Ok(PrivateKey::<N>::from_str(private_key)?),
404 None => {
405 let private_key = std::env::var("PRIVATE_KEY")
407 .map_err(|e| CliError::custom(format!("Failed to load `PRIVATE_KEY` from the environment: {e}")))?;
408 Ok(PrivateKey::<N>::from_str(&private_key)?)
410 }
411 }
412}
413
414pub fn get_is_devnet(devnet: bool) -> bool {
417 if devnet { true } else { std::env::var("DEVNET").is_ok() }
418}
419
420#[cfg(test)]
421mod test {
422 use snarkvm::prelude::ConsensusVersion;
423
424 #[test]
425 fn test_latest_consensus_version() {
426 assert_eq!(ConsensusVersion::latest(), ConsensusVersion::V10); }
428}