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 pub bytecode_size: usize,
69}
70
71impl Command for LeoDeploy {
72 type Input = Package;
73 type Output = ();
74
75 fn log_span(&self) -> Span {
76 tracing::span!(tracing::Level::INFO, "Leo")
77 }
78
79 fn prelude(&self, context: Context) -> Result<Self::Input> {
80 LeoBuild {
81 env_override: self.env_override.clone(),
82 options: {
83 let mut options = self.build_options.clone();
84 options.no_cache = true;
85 options
86 },
87 }
88 .execute(context)
89 }
90
91 fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
92 let network = get_network(&self.env_override.network)?;
94 match network {
96 NetworkName::TestnetV0 => handle_deploy::<TestnetV0>(&self, context, network, input),
97 NetworkName::MainnetV0 => {
98 #[cfg(feature = "only_testnet")]
99 panic!("Mainnet chosen with only_testnet feature");
100 #[cfg(not(feature = "only_testnet"))]
101 handle_deploy::<MainnetV0>(&self, context, network, input)
102 }
103 NetworkName::CanaryV0 => {
104 #[cfg(feature = "only_testnet")]
105 panic!("Canary chosen with only_testnet feature");
106 #[cfg(not(feature = "only_testnet"))]
107 handle_deploy::<CanaryV0>(&self, context, network, input)
108 }
109 }
110 }
111}
112
113fn handle_deploy<N: Network>(
115 command: &LeoDeploy,
116 context: Context,
117 network: NetworkName,
118 package: Package,
119) -> Result<<LeoDeploy as Command>::Output> {
120 let private_key = get_private_key(&command.env_override.private_key)?;
122 let address =
123 Address::try_from(&private_key).map_err(|e| CliError::custom(format!("Failed to parse address: {e}")))?;
124
125 let endpoint = get_endpoint(&command.env_override.endpoint)?;
127
128 let is_devnet = get_is_devnet(command.env_override.devnet);
130
131 let consensus_heights =
133 command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet));
134 validate_consensus_heights(&consensus_heights)
136 .map_err(|e| CliError::custom(format!("β οΈ Invalid consensus heights: {e}")))?;
137 let consensus_heights_string = consensus_heights.iter().format(",").to_string();
139 println!(
140 "\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"
141 );
142
143 #[allow(unsafe_code)]
145 unsafe {
146 std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
153 }
154
155 let programs = package.programs.iter().filter(|program| !program.is_test).cloned();
157
158 let programs_and_bytecode: Vec<(leo_package::Program, String)> = programs
159 .into_iter()
160 .map(|program| {
161 let bytecode = match &program.data {
162 ProgramData::Bytecode(s) => s.clone(),
163 ProgramData::SourcePath { .. } => {
164 let aleo_name = format!("{}.aleo", program.name);
166 let aleo_path = if package.manifest.program == aleo_name {
167 package.build_directory().join("main.aleo")
170 } else {
171 package.imports_directory().join(aleo_name)
173 };
174 fs::read_to_string(aleo_path.clone())
175 .map_err(|e| CliError::custom(format!("Failed to read file {}: {e}", aleo_path.display())))?
176 }
177 };
178
179 Ok((program, bytecode))
180 })
181 .collect::<Result<_>>()?;
182
183 let fee_options = parse_fee_options(&private_key, &command.fee_options, programs_and_bytecode.len())?;
185
186 let tasks: Vec<Task<N>> = programs_and_bytecode
187 .into_iter()
188 .zip(fee_options)
189 .map(|((program, bytecode), (_base_fee, priority_fee, record))| {
190 let id_str = format!("{}.aleo", program.name);
191 let id =
192 id_str.parse().map_err(|e| CliError::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
193 let bytecode_size = bytecode.len();
194 let parsed_program =
195 bytecode.parse().map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
196 Ok(Task {
197 id,
198 program: parsed_program,
199 edition: program.edition,
200 is_local: program.is_local,
201 priority_fee,
202 record,
203 bytecode_size,
204 })
205 })
206 .collect::<Result<_>>()?;
207
208 let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
210
211 let skipped: HashSet<ProgramID<N>> = local
213 .iter()
214 .filter_map(|task| {
215 let id_string = task.id.to_string();
216 command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
217 })
218 .collect();
219
220 let consensus_version =
222 get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
223
224 print_deployment_plan(
226 &private_key,
227 &address,
228 &endpoint,
229 &network,
230 &local,
231 &skipped,
232 &remote,
233 &check_tasks_for_warnings(&endpoint, network, &local, consensus_version, command),
234 consensus_version,
235 command,
236 );
237
238 if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
240 println!("β Deployment aborted.");
241 return Ok(());
242 }
243
244 let rng = &mut rand::thread_rng();
246
247 let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
249
250 let programs_and_editions = remote
252 .into_iter()
253 .map(|task| {
254 let edition = match task.edition {
256 Some(e) => e,
257 None => leo_package::fetch_latest_edition(&task.id.to_string(), &endpoint, network)?,
258 };
259 Ok((task.program, edition))
260 })
261 .collect::<Result<Vec<_>>>()?;
262
263 check_edition_constructor_requirements(&programs_and_editions, consensus_version, "deploy")?;
265
266 vm.process().write().add_programs_with_editions(&programs_and_editions)?;
267
268 let re = regex::Regex::new(r"v\d+$").unwrap();
270 let query_endpoint = re.replace(&endpoint, "").to_string();
271
272 let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
274 query_endpoint
275 .parse::<Uri>()
276 .map_err(|e| CliError::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
277 );
278
279 let mut transactions = Vec::new();
281 for Task { id, program, priority_fee, record, bytecode_size, .. } in local {
282 if !skipped.contains(&id) {
284 if let Some(constructor) = program.constructor() {
286 println!(
287 r"
288π§ Your program '{}' has the following constructor.
289ββββββββββββββββββββββββββββββββββββββββββββββ
290{constructor}
291ββββββββββββββββββββββββββββββββββββββββββββββ
292Once it is deployed, it CANNOT be changed.
293",
294 id.to_string().bold()
295 );
296 if !confirm("Would you like to proceed?", command.extra.yes)? {
297 println!("β Deployment aborted.");
298 return Ok(());
299 }
300 }
301 println!("π¦ Creating deployment transaction for '{}'...\n", id.to_string().bold());
302 let transaction =
304 vm.deploy(&private_key, &program, record, priority_fee.unwrap_or(0), Some(&query), rng)
305 .map_err(|e| CliError::custom(format!("Failed to generate deployment transaction: {e}")))?;
306 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
308 print_deployment_stats(&vm, &id.to_string(), deployment, priority_fee, consensus_version, bytecode_size)?;
310 transactions.push((id, transaction));
312 }
313 vm.process().write().add_program(&program)?;
315 }
316
317 for (program_id, transaction) in transactions.iter() {
318 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
320 validate_deployment_limits(deployment, program_id, &network)?;
321 }
322
323 if command.action.print {
326 for (program_name, transaction) in transactions.iter() {
327 let transaction_json = serde_json::to_string_pretty(transaction)
329 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
330 println!("π¨οΈ Printing deployment for {program_name}\n{transaction_json}")
331 }
332 }
333
334 if let Some(path) = &command.action.save {
338 std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
340 for (program_name, transaction) in transactions.iter() {
341 let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
343 println!("πΎ Saving deployment for {program_name} at {}", file_path.display());
344 let transaction_json = serde_json::to_string_pretty(transaction)
345 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
346 std::fs::write(file_path, transaction_json)
347 .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
348 }
349 }
350
351 if command.action.broadcast {
353 for (i, (program_id, transaction)) in transactions.iter().enumerate() {
354 println!("\nπ‘ Broadcasting deployment for {}...", program_id.to_string().bold());
355 let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
357 if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
358 println!("β© Deployment skipped.");
359 continue;
360 }
361 let fee_id = fee.id().to_string();
362 let id = transaction.id().to_string();
363 let height_before = check_transaction::current_height(&endpoint, network)?;
364 let (message, status) = handle_broadcast(
366 &format!("{endpoint}/{network}/transaction/broadcast"),
367 transaction,
368 &program_id.to_string(),
369 )?;
370
371 let fail_and_prompt = |msg| {
372 println!("β Failed to deploy program {program_id}: {msg}.");
373 let count = transactions.len() - i - 1;
374 if count > 0 {
376 confirm("Do you want to continue with the next deployment?", command.extra.yes)
377 } else {
378 Ok(false)
379 }
380 };
381
382 match status {
383 200..=299 => {
384 let status = check_transaction::check_transaction_with_message(
385 &id,
386 Some(&fee_id),
387 &endpoint,
388 network,
389 height_before + 1,
390 command.extra.max_wait,
391 command.extra.blocks_to_check,
392 )?;
393 if status == Some(TransactionStatus::Accepted) {
394 println!("β
Deployment confirmed!");
395 } else if fail_and_prompt("could not find the transaction on the network")? {
396 continue;
397 } else {
398 return Ok(());
399 }
400 }
401 _ => {
402 if fail_and_prompt(&message)? {
403 continue;
404 } else {
405 return Ok(());
406 }
407 }
408 }
409 }
410 }
411
412 Ok(())
413}
414
415fn check_tasks_for_warnings<N: Network>(
423 endpoint: &str,
424 network: NetworkName,
425 tasks: &[Task<N>],
426 consensus_version: ConsensusVersion,
427 command: &LeoDeploy,
428) -> Vec<String> {
429 let mut warnings = Vec::new();
430 for Task { id, is_local, program, bytecode_size, .. } in tasks {
431 if !is_local || !command.action.broadcast {
432 continue;
433 }
434 if fetch_program_from_network(&id.to_string(), endpoint, network).is_ok() {
436 warnings
437 .push(format!("The program '{id}' already exists on the network. Please use `leo upgrade` instead.",));
438 }
439 if consensus_version >= ConsensusVersion::V7
441 && let Err(e) = program.check_program_naming_structure()
442 {
443 warnings.push(format!(
444 "The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
445 ));
446 }
447
448 if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
450 warnings.push(format!(
451 "The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
452 consensus_version as u8
453 ));
454 }
455 if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
457 warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The deployment will likely fail"));
458 }
459 if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
461 warnings
462 .push(format!("The program '{id}' does not contain a constructor. The deployment will likely fail",));
463 }
464 if let (_, _, Some(msg)) = format_program_size(*bytecode_size, N::MAX_PROGRAM_SIZE) {
466 warnings.push(format!("The program '{id}' is {msg}."));
467 }
468 }
469 if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
471 warnings.push(format!("{e}. In some cases, the deployment may fail"));
472 }
473 warnings
474}
475
476pub(crate) fn validate_deployment_limits<N: Network>(
478 deployment: &Deployment<N>,
479 program_id: &ProgramID<N>,
480 network: &NetworkName,
481) -> Result<()> {
482 let combined_variables = deployment.num_combined_variables()?;
484 if combined_variables > N::MAX_DEPLOYMENT_VARIABLES {
485 return Err(CliError::variable_limit_exceeded(
486 program_id,
487 combined_variables,
488 N::MAX_DEPLOYMENT_VARIABLES,
489 network,
490 )
491 .into());
492 }
493
494 let constraints = deployment.num_combined_constraints()?;
496 if constraints > N::MAX_DEPLOYMENT_CONSTRAINTS {
497 return Err(CliError::constraint_limit_exceeded(
498 program_id,
499 constraints,
500 N::MAX_DEPLOYMENT_CONSTRAINTS,
501 network,
502 )
503 .into());
504 }
505
506 Ok(())
507}
508
509#[allow(clippy::too_many_arguments)]
511pub(crate) fn print_deployment_plan<N: Network>(
512 private_key: &PrivateKey<N>,
513 address: &Address<N>,
514 endpoint: &str,
515 network: &NetworkName,
516 local: &[Task<N>],
517 skipped: &HashSet<ProgramID<N>>,
518 remote: &[Task<N>],
519 warnings: &[String],
520 consensus_version: ConsensusVersion,
521 command: &LeoDeploy,
522) {
523 use colored::*;
524
525 println!("\n{}", "π οΈ Deployment Plan Summary".bold());
526 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
527
528 println!("{}", "π§ Configuration:".bold());
530 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
531 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
532 println!(" {:20}{}", "Endpoint:".cyan(), endpoint.yellow());
533 println!(" {:20}{}", "Network:".cyan(), network.to_string().yellow());
534 println!(" {:20}{}", "Consensus Version:".cyan(), (consensus_version as u8).to_string().yellow());
535
536 println!("\n{}", "π¦ Deployment Tasks:".bold());
538 if local.is_empty() {
539 println!(" (none)");
540 } else {
541 for Task { id, priority_fee, record, .. } in local.iter().filter(|task| !skipped.contains(&task.id)) {
542 let priority_fee_str = priority_fee.map_or("0".into(), |v| v.to_string());
543 let record_str = if record.is_some() { "yes" } else { "no (public fee)" };
544 println!(
545 " β’ {} β priority fee: {} β fee record: {}",
546 id.to_string().cyan(),
547 priority_fee_str,
548 record_str
549 );
550 }
551 }
552
553 if !skipped.is_empty() {
555 println!("\n{}", "π« Skipped Programs:".bold().red());
556 for symbol in skipped {
557 println!(" β’ {}", symbol.to_string().dimmed());
558 }
559 }
560
561 if !remote.is_empty() {
563 println!("\n{}", "π Remote Dependencies:".bold().red());
564 println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
565 for Task { id, .. } in remote {
566 println!(" β’ {}", id.to_string().dimmed());
567 }
568 }
569
570 println!("\n{}", "βοΈ Actions:".bold());
572 if command.action.print {
573 println!(" β’ Transaction(s) will be printed to the console.");
574 } else {
575 println!(" β’ Transaction(s) will NOT be printed to the console.");
576 }
577 if let Some(path) = &command.action.save {
578 println!(" β’ Transaction(s) will be saved to {}", path.bold());
579 } else {
580 println!(" β’ Transaction(s) will NOT be saved to a file.");
581 }
582 if command.action.broadcast {
583 println!(" β’ Transaction(s) will be broadcast to {}", endpoint.bold());
584 } else {
585 println!(" β’ Transaction(s) will NOT be broadcast to the network.");
586 }
587
588 if !warnings.is_empty() {
590 println!("\n{}", "β οΈ Warnings:".bold().red());
591 for warning in warnings {
592 println!(" β’ {}", warning.dimmed());
593 }
594 }
595
596 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ\n".dimmed());
597}
598
599pub(crate) fn print_deployment_stats<N: Network>(
602 vm: &VM<N, ConsensusMemory<N>>,
603 program_id: &str,
604 deployment: &Deployment<N>,
605 priority_fee: Option<u64>,
606 consensus_version: ConsensusVersion,
607 bytecode_size: usize,
608) -> Result<()> {
609 use colored::*;
610 use num_format::{Locale, ToFormattedString};
611
612 let variables = deployment.num_combined_variables()?;
614 let constraints = deployment.num_combined_constraints()?;
615 let (base_fee, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
616 deployment_cost(&vm.process().read(), deployment, consensus_version)?;
617
618 let base_fee_cr = base_fee as f64 / 1_000_000.0;
619 let prio_fee_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0;
620 let total_fee_cr = base_fee_cr + prio_fee_cr;
621
622 println!("\n{} {}", "π Deployment Summary for".bold(), program_id.bold());
624 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
625
626 let (size_kb, max_kb, warning) = format_program_size(bytecode_size, N::MAX_PROGRAM_SIZE);
628 println!(" {:22}{size_kb:.2} KB / {max_kb:.2} KB", "Program Size:".cyan());
629 if let Some(msg) = warning {
630 println!(" {} Program is {msg}.", "β οΈ ".bold().yellow());
631 }
632 println!(" {:22}{}", "Total Variables:".cyan(), variables.to_formatted_string(&Locale::en).yellow());
633 println!(" {:22}{}", "Total Constraints:".cyan(), constraints.to_formatted_string(&Locale::en).yellow());
634 println!(
635 " {:22}{}",
636 "Max Variables:".cyan(),
637 N::MAX_DEPLOYMENT_VARIABLES.to_formatted_string(&Locale::en).green()
638 );
639 println!(
640 " {:22}{}",
641 "Max Constraints:".cyan(),
642 N::MAX_DEPLOYMENT_CONSTRAINTS.to_formatted_string(&Locale::en).green()
643 );
644
645 println!("\n{}", "π° Cost Breakdown (credits)".bold());
647 println!(
648 " {:22}{}{:.6}",
649 "Transaction Storage:".cyan(),
650 "".yellow(), storage_cost as f64 / 1_000_000.0
652 );
653 println!(" {:22}{}{:.6}", "Program Synthesis:".cyan(), "".yellow(), synthesis_cost as f64 / 1_000_000.0);
654 println!(" {:22}{}{:.6}", "Namespace:".cyan(), "".yellow(), namespace_cost as f64 / 1_000_000.0);
655 println!(" {:22}{}{:.6}", "Constructor:".cyan(), "".yellow(), constructor_cost as f64 / 1_000_000.0);
656 println!(" {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_fee_cr);
657 println!(" {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_fee_cr);
658
659 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
661 Ok(())
662}