1use super::*;
18
19use check_transaction::TransactionStatus;
20use leo_ast::NetworkName;
21use leo_package::{Package, ProgramData, fetch_program_from_network};
22
23use aleo_std::StorageMode;
24#[cfg(not(feature = "only_testnet"))]
25use snarkvm::prelude::{CanaryV0, MainnetV0};
26use snarkvm::{
27 ledger::{query::Query as SnarkVMQuery, store::helpers::memory::BlockMemory},
28 prelude::{
29 ConsensusVersion,
30 Deployment,
31 Program,
32 ProgramID,
33 TestnetV0,
34 VM,
35 deployment_cost,
36 store::{ConsensusStore, helpers::memory::ConsensusMemory},
37 },
38};
39
40use colored::*;
41use itertools::Itertools;
42use std::{collections::HashSet, fs, path::PathBuf};
43
44#[derive(Parser, Debug)]
46pub struct LeoDeploy {
47 #[clap(flatten)]
48 pub(crate) fee_options: FeeOptions,
49 #[clap(flatten)]
50 pub(crate) action: TransactionAction,
51 #[clap(flatten)]
52 pub(crate) env_override: EnvOptions,
53 #[clap(flatten)]
54 pub(crate) extra: ExtraOptions,
55 #[clap(long, help = "Skips deployment of any program that contains one of the given substrings.", value_delimiter = ',', num_args = 1..)]
56 pub(crate) skip: Vec<String>,
57 #[clap(flatten)]
58 pub(crate) build_options: BuildOptions,
59}
60
61pub struct Task<N: Network> {
62 pub id: ProgramID<N>,
63 pub program: Program<N>,
64 pub edition: Option<u16>,
65 pub is_local: bool,
66 pub priority_fee: Option<u64>,
67 pub record: Option<Record<N, Plaintext<N>>>,
68}
69
70impl Command for LeoDeploy {
71 type Input = Package;
72 type Output = ();
73
74 fn log_span(&self) -> Span {
75 tracing::span!(tracing::Level::INFO, "Leo")
76 }
77
78 fn prelude(&self, context: Context) -> Result<Self::Input> {
79 LeoBuild {
80 env_override: self.env_override.clone(),
81 options: {
82 let mut options = self.build_options.clone();
83 options.no_cache = true;
84 options
85 },
86 }
87 .execute(context)
88 }
89
90 fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
91 let network = get_network(&self.env_override.network)?;
93 match network {
95 NetworkName::TestnetV0 => handle_deploy::<TestnetV0>(&self, context, network, input),
96 NetworkName::MainnetV0 => {
97 #[cfg(feature = "only_testnet")]
98 panic!("Mainnet chosen with only_testnet feature");
99 #[cfg(not(feature = "only_testnet"))]
100 handle_deploy::<MainnetV0>(&self, context, network, input)
101 }
102 NetworkName::CanaryV0 => {
103 #[cfg(feature = "only_testnet")]
104 panic!("Canary chosen with only_testnet feature");
105 #[cfg(not(feature = "only_testnet"))]
106 handle_deploy::<CanaryV0>(&self, context, network, input)
107 }
108 }
109 }
110}
111
112fn handle_deploy<N: Network>(
114 command: &LeoDeploy,
115 context: Context,
116 network: NetworkName,
117 package: Package,
118) -> Result<<LeoDeploy as Command>::Output> {
119 let private_key = get_private_key(&command.env_override.private_key)?;
121 let address =
122 Address::try_from(&private_key).map_err(|e| CliError::custom(format!("Failed to parse address: {e}")))?;
123
124 let endpoint = get_endpoint(&command.env_override.endpoint)?;
126
127 let is_devnet = get_is_devnet(command.env_override.devnet);
129
130 let consensus_heights =
132 command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet));
133 validate_consensus_heights(&consensus_heights)
135 .map_err(|e| CliError::custom(format!("β οΈ Invalid consensus heights: {e}")))?;
136 let consensus_heights_string = consensus_heights.iter().format(",").to_string();
138 println!(
139 "\nπ’ Using the following consensus heights: {consensus_heights_string}\n To override, pass in `--consensus-heights` or override the environment variable `CONSENSUS_VERSION_HEIGHTS`.\n"
140 );
141
142 #[allow(unsafe_code)]
144 unsafe {
145 std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
152 }
153
154 let programs = package.programs.iter().filter(|program| !program.is_test).cloned();
156
157 let programs_and_bytecode: Vec<(leo_package::Program, String)> = programs
158 .into_iter()
159 .map(|program| {
160 let bytecode = match &program.data {
161 ProgramData::Bytecode(s) => s.clone(),
162 ProgramData::SourcePath { .. } => {
163 let aleo_name = format!("{}.aleo", program.name);
165 let aleo_path = if package.manifest.program == aleo_name {
166 package.build_directory().join("main.aleo")
169 } else {
170 package.imports_directory().join(aleo_name)
172 };
173 fs::read_to_string(aleo_path.clone())
174 .map_err(|e| CliError::custom(format!("Failed to read file {}: {e}", aleo_path.display())))?
175 }
176 };
177
178 Ok((program, bytecode))
179 })
180 .collect::<Result<_>>()?;
181
182 let fee_options = parse_fee_options(&private_key, &command.fee_options, programs_and_bytecode.len())?;
184
185 let tasks: Vec<Task<N>> = programs_and_bytecode
186 .into_iter()
187 .zip(fee_options)
188 .map(|((program, bytecode), (_base_fee, priority_fee, record))| {
189 let id_str = format!("{}.aleo", program.name);
190 let id =
191 id_str.parse().map_err(|e| CliError::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
192 let bytecode = bytecode.parse().map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
193 Ok(Task {
194 id,
195 program: bytecode,
196 edition: program.edition,
197 is_local: program.is_local,
198 priority_fee,
199 record,
200 })
201 })
202 .collect::<Result<_>>()?;
203
204 let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
206
207 let skipped: HashSet<ProgramID<N>> = local
209 .iter()
210 .filter_map(|task| {
211 let id_string = task.id.to_string();
212 command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
213 })
214 .collect();
215
216 let consensus_version =
218 get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
219
220 print_deployment_plan(
222 &private_key,
223 &address,
224 &endpoint,
225 &network,
226 &local,
227 &skipped,
228 &remote,
229 &check_tasks_for_warnings(&endpoint, network, &local, consensus_version, command),
230 consensus_version,
231 command,
232 );
233
234 if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
236 println!("β Deployment aborted.");
237 return Ok(());
238 }
239
240 let rng = &mut rand::thread_rng();
242
243 let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
245
246 let programs_and_editions = remote
248 .into_iter()
249 .map(|task| {
250 (task.program, task.edition.unwrap_or(1))
252 })
253 .collect::<Vec<_>>();
254 vm.process().write().add_programs_with_editions(&programs_and_editions)?;
255
256 let re = regex::Regex::new(r"v\d+$").unwrap();
258 let query_endpoint = re.replace(&endpoint, "").to_string();
259
260 let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
262 query_endpoint
263 .parse::<Uri>()
264 .map_err(|e| CliError::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
265 );
266
267 let mut transactions = Vec::new();
269 for Task { id, program, priority_fee, record, .. } in local {
270 if !skipped.contains(&id) {
272 if let Some(constructor) = program.constructor() {
274 println!(
275 r"
276π§ Your program '{}' has the following constructor.
277ββββββββββββββββββββββββββββββββββββββββββββββ
278{constructor}
279ββββββββββββββββββββββββββββββββββββββββββββββ
280Once it is deployed, it CANNOT be changed.
281",
282 id.to_string().bold()
283 );
284 if !confirm("Would you like to proceed?", command.extra.yes)? {
285 println!("β Deployment aborted.");
286 return Ok(());
287 }
288 }
289 println!("π¦ Creating deployment transaction for '{}'...\n", id.to_string().bold());
290 let transaction =
292 vm.deploy(&private_key, &program, record, priority_fee.unwrap_or(0), Some(&query), rng)
293 .map_err(|e| CliError::custom(format!("Failed to generate deployment transaction: {e}")))?;
294 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
296 print_deployment_stats(&vm, &id.to_string(), deployment, priority_fee, consensus_version)?;
298 transactions.push((id, transaction));
300 }
301 vm.process().write().add_program(&program)?;
303 }
304
305 for (program_id, transaction) in transactions.iter() {
306 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
308 validate_deployment_limits(deployment, program_id, &network)?;
309 }
310
311 if command.action.print {
314 for (program_name, transaction) in transactions.iter() {
315 let transaction_json = serde_json::to_string_pretty(transaction)
317 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
318 println!("π¨οΈ Printing deployment for {program_name}\n{transaction_json}")
319 }
320 }
321
322 if let Some(path) = &command.action.save {
326 std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
328 for (program_name, transaction) in transactions.iter() {
329 let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
331 println!("πΎ Saving deployment for {program_name} at {}", file_path.display());
332 let transaction_json = serde_json::to_string_pretty(transaction)
333 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
334 std::fs::write(file_path, transaction_json)
335 .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
336 }
337 }
338
339 if command.action.broadcast {
341 for (i, (program_id, transaction)) in transactions.iter().enumerate() {
342 println!("\nπ‘ Broadcasting deployment for {}...", program_id.to_string().bold());
343 let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
345 if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
346 println!("β© Deployment skipped.");
347 continue;
348 }
349 let fee_id = fee.id().to_string();
350 let id = transaction.id().to_string();
351 let height_before = check_transaction::current_height(&endpoint, network)?;
352 let (message, status) = handle_broadcast(
354 &format!("{endpoint}/{network}/transaction/broadcast"),
355 transaction,
356 &program_id.to_string(),
357 )?;
358
359 let fail_and_prompt = |msg| {
360 println!("β Failed to deploy program {program_id}: {msg}.");
361 let count = transactions.len() - i - 1;
362 if count > 0 {
364 confirm("Do you want to continue with the next deployment?", command.extra.yes)
365 } else {
366 Ok(false)
367 }
368 };
369
370 match status {
371 200..=299 => {
372 let status = check_transaction::check_transaction_with_message(
373 &id,
374 Some(&fee_id),
375 &endpoint,
376 network,
377 height_before + 1,
378 command.extra.max_wait,
379 command.extra.blocks_to_check,
380 )?;
381 if status == Some(TransactionStatus::Accepted) {
382 println!("β
Deployment confirmed!");
383 } else if fail_and_prompt("could not find the transaction on the network")? {
384 continue;
385 } else {
386 return Ok(());
387 }
388 }
389 _ => {
390 if fail_and_prompt(&message)? {
391 continue;
392 } else {
393 return Ok(());
394 }
395 }
396 }
397 }
398 }
399
400 Ok(())
401}
402
403fn check_tasks_for_warnings<N: Network>(
410 endpoint: &str,
411 network: NetworkName,
412 tasks: &[Task<N>],
413 consensus_version: ConsensusVersion,
414 command: &LeoDeploy,
415) -> Vec<String> {
416 let mut warnings = Vec::new();
417 for Task { id, is_local, program, .. } in tasks {
418 if !is_local || !command.action.broadcast {
419 continue;
420 }
421 if fetch_program_from_network(&id.to_string(), endpoint, network).is_ok() {
423 warnings
424 .push(format!("The program '{id}' already exists on the network. Please use `leo upgrade` instead.",));
425 }
426 if consensus_version >= ConsensusVersion::V7
428 && let Err(e) = program.check_program_naming_structure()
429 {
430 warnings.push(format!(
431 "The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
432 ));
433 }
434
435 if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
437 warnings.push(format!(
438 "The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
439 consensus_version as u8
440 ));
441 }
442 if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
444 warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The deployment will likely fail"));
445 }
446 if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
448 warnings
449 .push(format!("The program '{id}' does not contain a constructor. The deployment will likely fail",));
450 }
451 }
452 if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
454 warnings.push(format!("{e}. In some cases, the deployment may fail"));
455 }
456 warnings
457}
458
459pub(crate) fn validate_deployment_limits<N: Network>(
461 deployment: &Deployment<N>,
462 program_id: &ProgramID<N>,
463 network: &NetworkName,
464) -> Result<()> {
465 let combined_variables = deployment.num_combined_variables()?;
467 if combined_variables > N::MAX_DEPLOYMENT_VARIABLES {
468 return Err(CliError::variable_limit_exceeded(
469 program_id,
470 combined_variables,
471 N::MAX_DEPLOYMENT_VARIABLES,
472 network,
473 )
474 .into());
475 }
476
477 let constraints = deployment.num_combined_constraints()?;
479 if constraints > N::MAX_DEPLOYMENT_CONSTRAINTS {
480 return Err(CliError::constraint_limit_exceeded(
481 program_id,
482 constraints,
483 N::MAX_DEPLOYMENT_CONSTRAINTS,
484 network,
485 )
486 .into());
487 }
488
489 Ok(())
490}
491
492#[allow(clippy::too_many_arguments)]
494pub(crate) fn print_deployment_plan<N: Network>(
495 private_key: &PrivateKey<N>,
496 address: &Address<N>,
497 endpoint: &str,
498 network: &NetworkName,
499 local: &[Task<N>],
500 skipped: &HashSet<ProgramID<N>>,
501 remote: &[Task<N>],
502 warnings: &[String],
503 consensus_version: ConsensusVersion,
504 command: &LeoDeploy,
505) {
506 use colored::*;
507
508 println!("\n{}", "π οΈ Deployment Plan Summary".bold());
509 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
510
511 println!("{}", "π§ Configuration:".bold());
513 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
514 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
515 println!(" {:20}{}", "Endpoint:".cyan(), endpoint.yellow());
516 println!(" {:20}{}", "Network:".cyan(), network.to_string().yellow());
517 println!(" {:20}{}", "Consensus Version:".cyan(), (consensus_version as u8).to_string().yellow());
518
519 println!("\n{}", "π¦ Deployment Tasks:".bold());
521 if local.is_empty() {
522 println!(" (none)");
523 } else {
524 for Task { id, priority_fee, record, .. } in local.iter().filter(|task| !skipped.contains(&task.id)) {
525 let priority_fee_str = priority_fee.map_or("0".into(), |v| v.to_string());
526 let record_str = if record.is_some() { "yes" } else { "no (public fee)" };
527 println!(
528 " β’ {} β priority fee: {} β fee record: {}",
529 id.to_string().cyan(),
530 priority_fee_str,
531 record_str
532 );
533 }
534 }
535
536 if !skipped.is_empty() {
538 println!("\n{}", "π« Skipped Programs:".bold().red());
539 for symbol in skipped {
540 println!(" β’ {}", symbol.to_string().dimmed());
541 }
542 }
543
544 if !remote.is_empty() {
546 println!("\n{}", "π Remote Dependencies:".bold().red());
547 println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
548 for Task { id, .. } in remote {
549 println!(" β’ {}", id.to_string().dimmed());
550 }
551 }
552
553 println!("\n{}", "βοΈ Actions:".bold());
555 if command.action.print {
556 println!(" β’ Transaction(s) will be printed to the console.");
557 } else {
558 println!(" β’ Transaction(s) will NOT be printed to the console.");
559 }
560 if let Some(path) = &command.action.save {
561 println!(" β’ Transaction(s) will be saved to {}", path.bold());
562 } else {
563 println!(" β’ Transaction(s) will NOT be saved to a file.");
564 }
565 if command.action.broadcast {
566 println!(" β’ Transaction(s) will be broadcast to {}", endpoint.bold());
567 } else {
568 println!(" β’ Transaction(s) will NOT be broadcast to the network.");
569 }
570
571 if !warnings.is_empty() {
573 println!("\n{}", "β οΈ Warnings:".bold().red());
574 for warning in warnings {
575 println!(" β’ {}", warning.dimmed());
576 }
577 }
578
579 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ\n".dimmed());
580}
581
582pub(crate) fn print_deployment_stats<N: Network>(
585 vm: &VM<N, ConsensusMemory<N>>,
586 program_id: &str,
587 deployment: &Deployment<N>,
588 priority_fee: Option<u64>,
589 consensus_version: ConsensusVersion,
590) -> Result<()> {
591 use colored::*;
592 use num_format::{Locale, ToFormattedString};
593
594 let variables = deployment.num_combined_variables()?;
596 let constraints = deployment.num_combined_constraints()?;
597 let (base_fee, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
598 deployment_cost(&vm.process().read(), deployment, consensus_version)?;
599
600 let base_fee_cr = base_fee as f64 / 1_000_000.0;
601 let prio_fee_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0;
602 let total_fee_cr = base_fee_cr + prio_fee_cr;
603
604 println!("\n{} {}", "π Deployment Summary for".bold(), program_id.bold());
606 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
607
608 println!(" {:22}{}", "Total Variables:".cyan(), variables.to_formatted_string(&Locale::en).yellow());
610 println!(" {:22}{}", "Total Constraints:".cyan(), constraints.to_formatted_string(&Locale::en).yellow());
611 println!(
612 " {:22}{}",
613 "Max Variables:".cyan(),
614 N::MAX_DEPLOYMENT_VARIABLES.to_formatted_string(&Locale::en).green()
615 );
616 println!(
617 " {:22}{}",
618 "Max Constraints:".cyan(),
619 N::MAX_DEPLOYMENT_CONSTRAINTS.to_formatted_string(&Locale::en).green()
620 );
621
622 println!("\n{}", "π° Cost Breakdown (credits)".bold());
624 println!(
625 " {:22}{}{:.6}",
626 "Transaction Storage:".cyan(),
627 "".yellow(), storage_cost as f64 / 1_000_000.0
629 );
630 println!(" {:22}{}{:.6}", "Program Synthesis:".cyan(), "".yellow(), synthesis_cost as f64 / 1_000_000.0);
631 println!(" {:22}{}{:.6}", "Namespace:".cyan(), "".yellow(), namespace_cost as f64 / 1_000_000.0);
632 println!(" {:22}{}{:.6}", "Constructor:".cyan(), "".yellow(), constructor_cost as f64 / 1_000_000.0);
633 println!(" {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_fee_cr);
634 println!(" {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_fee_cr);
635
636 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
638 Ok(())
639}