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::store::helpers::memory::BlockMemory,
28 prelude::{
29 ConsensusVersion,
30 Deployment,
31 Program,
32 ProgramID,
33 TestnetV0,
34 VM,
35 deployment_cost,
36 query::Query as SnarkVMQuery,
37 store::{ConsensusStore, helpers::memory::ConsensusMemory},
38 },
39};
40
41use colored::*;
42use itertools::Itertools;
43use std::{collections::HashSet, fs, path::PathBuf};
44
45#[derive(Parser, Debug)]
47pub struct LeoDeploy {
48 #[clap(flatten)]
49 pub(crate) fee_options: FeeOptions,
50 #[clap(flatten)]
51 pub(crate) action: TransactionAction,
52 #[clap(flatten)]
53 pub(crate) env_override: EnvOptions,
54 #[clap(flatten)]
55 pub(crate) extra: ExtraOptions,
56 #[clap(long, help = "Skips deployment of any program that contains one of the given substrings.", value_delimiter = ',', num_args = 1..)]
57 pub(crate) skip: Vec<String>,
58 #[clap(flatten)]
59 pub(crate) build_options: BuildOptions,
60}
61
62pub struct Task<N: Network> {
63 pub id: ProgramID<N>,
64 pub program: Program<N>,
65 pub edition: Option<u16>,
66 pub is_local: bool,
67 pub priority_fee: Option<u64>,
68 pub record: Option<Record<N, Plaintext<N>>>,
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 = bytecode.parse().map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
194 Ok(Task {
195 id,
196 program: bytecode,
197 edition: program.edition,
198 is_local: program.is_local,
199 priority_fee,
200 record,
201 })
202 })
203 .collect::<Result<_>>()?;
204
205 let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
207
208 let skipped: HashSet<ProgramID<N>> = local
210 .iter()
211 .filter_map(|task| {
212 let id_string = task.id.to_string();
213 command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
214 })
215 .collect();
216
217 let consensus_version =
219 get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
220
221 print_deployment_plan(
223 &private_key,
224 &address,
225 &endpoint,
226 &network,
227 &local,
228 &skipped,
229 &remote,
230 &check_tasks_for_warnings(&endpoint, network, &local, consensus_version, command),
231 consensus_version,
232 command,
233 );
234
235 if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
237 println!("β Deployment aborted.");
238 return Ok(());
239 }
240
241 let rng = &mut rand::thread_rng();
243
244 let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
246
247 let programs_and_editions = remote
249 .into_iter()
250 .map(|task| {
251 (task.program, task.edition.unwrap_or(1))
253 })
254 .collect::<Vec<_>>();
255 vm.process().write().add_programs_with_editions(&programs_and_editions)?;
256
257 let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
259 endpoint
260 .parse::<Uri>()
261 .map_err(|e| CliError::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
262 );
263
264 let mut transactions = Vec::new();
266 for Task { id, program, priority_fee, record, .. } in local {
267 if !skipped.contains(&id) {
269 if let Some(constructor) = program.constructor() {
271 println!(
272 r"
273π§ Your program '{}' has the following constructor.
274ββββββββββββββββββββββββββββββββββββββββββββββ
275{constructor}
276ββββββββββββββββββββββββββββββββββββββββββββββ
277Once it is deployed, it CANNOT be changed.
278",
279 id.to_string().bold()
280 );
281 if !confirm("Would you like to proceed?", command.extra.yes)? {
282 println!("β Deployment aborted.");
283 return Ok(());
284 }
285 }
286 println!("π¦ Creating deployment transaction for '{}'...\n", id.to_string().bold());
287 let transaction =
289 vm.deploy(&private_key, &program, record, priority_fee.unwrap_or(0), Some(&query), rng)
290 .map_err(|e| CliError::custom(format!("Failed to generate deployment transaction: {e}")))?;
291 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
293 print_deployment_stats(&vm, &id.to_string(), deployment, priority_fee, consensus_version)?;
295 transactions.push((id, transaction));
297 }
298 vm.process().write().add_program(&program)?;
300 }
301
302 for (program_id, transaction) in transactions.iter() {
303 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
305 validate_deployment_limits(deployment, program_id, &network)?;
306 }
307
308 if command.action.print {
311 for (program_name, transaction) in transactions.iter() {
312 let transaction_json = serde_json::to_string_pretty(transaction)
314 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
315 println!("π¨οΈ Printing deployment for {program_name}\n{transaction_json}")
316 }
317 }
318
319 if let Some(path) = &command.action.save {
323 std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
325 for (program_name, transaction) in transactions.iter() {
326 let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
328 println!("πΎ Saving deployment for {program_name} at {}", file_path.display());
329 let transaction_json = serde_json::to_string_pretty(transaction)
330 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
331 std::fs::write(file_path, transaction_json)
332 .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
333 }
334 }
335
336 if command.action.broadcast {
338 for (i, (program_id, transaction)) in transactions.iter().enumerate() {
339 println!("\nπ‘ Broadcasting deployment for {}...", program_id.to_string().bold());
340 let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
342 if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
343 println!("β© Deployment skipped.");
344 continue;
345 }
346 let fee_id = fee.id().to_string();
347 let id = transaction.id().to_string();
348 let height_before = check_transaction::current_height(&endpoint, network)?;
349 let (message, status) = handle_broadcast(
351 &format!("{endpoint}/{network}/transaction/broadcast"),
352 transaction,
353 &program_id.to_string(),
354 )?;
355
356 let fail_and_prompt = |msg| {
357 println!("β Failed to deploy program {program_id}: {msg}.");
358 let count = transactions.len() - i - 1;
359 if count > 0 {
361 confirm("Do you want to continue with the next deployment?", command.extra.yes)
362 } else {
363 Ok(false)
364 }
365 };
366
367 match status {
368 200..=299 => {
369 let status = check_transaction::check_transaction_with_message(
370 &id,
371 Some(&fee_id),
372 &endpoint,
373 network,
374 height_before + 1,
375 command.extra.max_wait,
376 command.extra.blocks_to_check,
377 )?;
378 if status == Some(TransactionStatus::Accepted) {
379 println!("β
Deployment confirmed!");
380 } else if fail_and_prompt("could not find the transaction on the network")? {
381 continue;
382 } else {
383 return Ok(());
384 }
385 }
386 _ => {
387 if fail_and_prompt(&message)? {
388 continue;
389 } else {
390 return Ok(());
391 }
392 }
393 }
394 }
395 }
396
397 Ok(())
398}
399
400fn check_tasks_for_warnings<N: Network>(
407 endpoint: &str,
408 network: NetworkName,
409 tasks: &[Task<N>],
410 consensus_version: ConsensusVersion,
411 command: &LeoDeploy,
412) -> Vec<String> {
413 let mut warnings = Vec::new();
414 for Task { id, is_local, program, .. } in tasks {
415 if !is_local || !command.action.broadcast {
416 continue;
417 }
418 if fetch_program_from_network(&id.to_string(), endpoint, network).is_ok() {
420 warnings
421 .push(format!("The program '{id}' already exists on the network. Please use `leo upgrade` instead.",));
422 }
423 if consensus_version >= ConsensusVersion::V7 {
425 if let Err(e) = program.check_program_naming_structure() {
426 warnings.push(format!(
427 "The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
428 ));
429 }
430 }
431
432 if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
434 warnings.push(format!(
435 "The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
436 consensus_version as u8
437 ));
438 }
439 if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
441 warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The deployment will likely fail"));
442 }
443 if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
445 warnings
446 .push(format!("The program '{id}' does not contain a constructor. The deployment will likely fail",));
447 }
448 }
449 if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
451 warnings.push(format!("{e}. In some cases, the deployment may fail"));
452 }
453 warnings
454}
455
456pub(crate) fn validate_deployment_limits<N: Network>(
458 deployment: &Deployment<N>,
459 program_id: &ProgramID<N>,
460 network: &NetworkName,
461) -> Result<()> {
462 let combined_variables = deployment.num_combined_variables()?;
464 if combined_variables > N::MAX_DEPLOYMENT_VARIABLES {
465 return Err(CliError::variable_limit_exceeded(
466 program_id,
467 combined_variables,
468 N::MAX_DEPLOYMENT_VARIABLES,
469 network,
470 )
471 .into());
472 }
473
474 let constraints = deployment.num_combined_constraints()?;
476 if constraints > N::MAX_DEPLOYMENT_CONSTRAINTS {
477 return Err(CliError::constraint_limit_exceeded(
478 program_id,
479 constraints,
480 N::MAX_DEPLOYMENT_CONSTRAINTS,
481 network,
482 )
483 .into());
484 }
485
486 Ok(())
487}
488
489#[allow(clippy::too_many_arguments)]
491pub(crate) fn print_deployment_plan<N: Network>(
492 private_key: &PrivateKey<N>,
493 address: &Address<N>,
494 endpoint: &str,
495 network: &NetworkName,
496 local: &[Task<N>],
497 skipped: &HashSet<ProgramID<N>>,
498 remote: &[Task<N>],
499 warnings: &[String],
500 consensus_version: ConsensusVersion,
501 command: &LeoDeploy,
502) {
503 use colored::*;
504
505 println!("\n{}", "π οΈ Deployment Plan Summary".bold());
506 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
507
508 println!("{}", "π§ Configuration:".bold());
510 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
511 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
512 println!(" {:20}{}", "Endpoint:".cyan(), endpoint.yellow());
513 println!(" {:20}{}", "Network:".cyan(), network.to_string().yellow());
514 println!(" {:20}{}", "Consensus Version:".cyan(), (consensus_version as u8).to_string().yellow());
515
516 println!("\n{}", "π¦ Deployment Tasks:".bold());
518 if local.is_empty() {
519 println!(" (none)");
520 } else {
521 for Task { id, priority_fee, record, .. } in local.iter().filter(|task| !skipped.contains(&task.id)) {
522 let priority_fee_str = priority_fee.map_or("0".into(), |v| v.to_string());
523 let record_str = if record.is_some() { "yes" } else { "no (public fee)" };
524 println!(
525 " β’ {} β priority fee: {} β fee record: {}",
526 id.to_string().cyan(),
527 priority_fee_str,
528 record_str
529 );
530 }
531 }
532
533 if !skipped.is_empty() {
535 println!("\n{}", "π« Skipped Programs:".bold().red());
536 for symbol in skipped {
537 println!(" β’ {}", symbol.to_string().dimmed());
538 }
539 }
540
541 if !remote.is_empty() {
543 println!("\n{}", "π Remote Dependencies:".bold().red());
544 println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
545 for Task { id, .. } in remote {
546 println!(" β’ {}", id.to_string().dimmed());
547 }
548 }
549
550 println!("\n{}", "βοΈ Actions:".bold());
552 if command.action.print {
553 println!(" β’ Transaction(s) will be printed to the console.");
554 } else {
555 println!(" β’ Transaction(s) will NOT be printed to the console.");
556 }
557 if let Some(path) = &command.action.save {
558 println!(" β’ Transaction(s) will be saved to {}", path.bold());
559 } else {
560 println!(" β’ Transaction(s) will NOT be saved to a file.");
561 }
562 if command.action.broadcast {
563 println!(" β’ Transaction(s) will be broadcast to {}", endpoint.bold());
564 } else {
565 println!(" β’ Transaction(s) will NOT be broadcast to the network.");
566 }
567
568 if !warnings.is_empty() {
570 println!("\n{}", "β οΈ Warnings:".bold().red());
571 for warning in warnings {
572 println!(" β’ {}", warning.dimmed());
573 }
574 }
575
576 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ\n".dimmed());
577}
578
579pub(crate) fn print_deployment_stats<N: Network>(
582 vm: &VM<N, ConsensusMemory<N>>,
583 program_id: &str,
584 deployment: &Deployment<N>,
585 priority_fee: Option<u64>,
586 consensus_version: ConsensusVersion,
587) -> Result<()> {
588 use colored::*;
589 use num_format::{Locale, ToFormattedString};
590
591 let variables = deployment.num_combined_variables()?;
593 let constraints = deployment.num_combined_constraints()?;
594 let (base_fee, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
595 deployment_cost(&vm.process().read(), deployment, consensus_version)?;
596
597 let base_fee_cr = base_fee as f64 / 1_000_000.0;
598 let prio_fee_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0;
599 let total_fee_cr = base_fee_cr + prio_fee_cr;
600
601 println!("\n{} {}", "π Deployment Summary for".bold(), program_id.bold());
603 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
604
605 println!(" {:22}{}", "Total Variables:".cyan(), variables.to_formatted_string(&Locale::en).yellow());
607 println!(" {:22}{}", "Total Constraints:".cyan(), constraints.to_formatted_string(&Locale::en).yellow());
608 println!(
609 " {:22}{}",
610 "Max Variables:".cyan(),
611 N::MAX_DEPLOYMENT_VARIABLES.to_formatted_string(&Locale::en).green()
612 );
613 println!(
614 " {:22}{}",
615 "Max Constraints:".cyan(),
616 N::MAX_DEPLOYMENT_CONSTRAINTS.to_formatted_string(&Locale::en).green()
617 );
618
619 println!("\n{}", "π° Cost Breakdown (credits)".bold());
621 println!(
622 " {:22}{}{:.6}",
623 "Transaction Storage:".cyan(),
624 "".yellow(), storage_cost as f64 / 1_000_000.0
626 );
627 println!(" {:22}{}{:.6}", "Program Synthesis:".cyan(), "".yellow(), synthesis_cost as f64 / 1_000_000.0);
628 println!(" {:22}{}{:.6}", "Namespace:".cyan(), "".yellow(), namespace_cost as f64 / 1_000_000.0);
629 println!(" {:22}{}{:.6}", "Constructor:".cyan(), "".yellow(), constructor_cost as f64 / 1_000_000.0);
630 println!(" {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_fee_cr);
631 println!(" {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_fee_cr);
632
633 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
635 Ok(())
636}