1use super::*;
18
19use check_transaction::TransactionStatus;
20use leo_package::{Manifest, NetworkName, Package, fetch_program_from_network};
21
22#[cfg(not(feature = "only_testnet"))]
23use snarkvm::prelude::{CanaryV0, MainnetV0};
24use snarkvm::{
25 ledger::query::Query as SnarkVMQuery,
26 prelude::{
27 Deployment,
28 Program,
29 TestnetV0,
30 VM,
31 deployment_cost,
32 store::{ConsensusStore, helpers::memory::ConsensusMemory},
33 },
34};
35
36use aleo_std::StorageMode;
37use colored::*;
38use snarkvm::prelude::{ConsensusVersion, ProgramID};
39use std::path::PathBuf;
40
41type DeploymentTask<N> =
42 (ProgramID<N>, Program<N>, Option<Manifest>, Option<u64>, Option<u64>, Option<Record<N, Plaintext<N>>>);
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.")]
56 pub(crate) skip: Vec<String>,
57 #[clap(flatten)]
58 pub(crate) build_options: BuildOptions,
59}
60
61impl Command for LeoDeploy {
62 type Input = Package;
63 type Output = ();
64
65 fn log_span(&self) -> Span {
66 tracing::span!(tracing::Level::INFO, "Leo")
67 }
68
69 fn prelude(&self, context: Context) -> Result<Self::Input> {
70 LeoBuild {
71 env_override: self.env_override.clone(),
72 options: {
73 let mut options = self.build_options.clone();
74 options.no_cache = true;
75 options
76 },
77 }
78 .execute(context)
79 }
80
81 fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
82 let network = context.get_network(&self.env_override.network)?.parse()?;
84 match network {
86 NetworkName::TestnetV0 => handle_deploy::<TestnetV0>(&self, context, network, input),
87 NetworkName::MainnetV0 => {
88 #[cfg(feature = "only_testnet")]
89 panic!("Mainnet chosen with only_testnet feature");
90 #[cfg(not(feature = "only_testnet"))]
91 handle_deploy::<MainnetV0>(&self, context, network, input)
92 }
93 NetworkName::CanaryV0 => {
94 #[cfg(feature = "only_testnet")]
95 panic!("Canary chosen with only_testnet feature");
96 #[cfg(not(feature = "only_testnet"))]
97 handle_deploy::<CanaryV0>(&self, context, network, input)
98 }
99 }
100 }
101}
102
103fn handle_deploy<N: Network>(
105 command: &LeoDeploy,
106 context: Context,
107 network: NetworkName,
108 package: Package,
109) -> Result<<LeoDeploy as Command>::Output> {
110 let private_key = context.get_private_key(&command.env_override.private_key)?;
112 let address =
113 Address::try_from(&private_key).map_err(|e| CliError::custom(format!("Failed to parse address: {e}")))?;
114
115 let endpoint = context.get_endpoint(&command.env_override.endpoint)?;
117
118 let programs_and_manifests = package
120 .get_programs_and_manifests(context.home()?)?
121 .into_iter()
122 .map(|(program_name, program_string, manifest)| {
123 let program_id = ProgramID::<N>::from_str(&format!("{}.aleo", program_name))
125 .map_err(|e| CliError::custom(format!("Failed to parse program ID: {e}")))?;
126 let bytecode = Program::<N>::from_str(&program_string)
128 .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
129 Ok((program_id, bytecode, manifest))
130 })
131 .collect::<Result<Vec<_>>>()?;
132
133 let fee_options = parse_fee_options(&private_key, &command.fee_options, programs_and_manifests.len())?;
135
136 let tasks = programs_and_manifests
138 .into_iter()
139 .zip(fee_options)
140 .map(|((program, data, manifest), (base_fee, priority_fee, record))| {
141 (program, data, manifest, base_fee, priority_fee, record)
142 })
143 .collect::<Vec<_>>();
144
145 let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|(_, _, manifest, _, _, _)| manifest.is_some());
147
148 let skipped = local
150 .iter()
151 .filter(|(program_id, _, _, _, _, _)| command.skip.iter().any(|skip| program_id.to_string().contains(skip)))
152 .map(|(program_id, _, _, _, _, _)| *program_id)
153 .collect::<Vec<_>>();
154
155 let consensus_version = get_consensus_version::<N>(&command.extra.consensus_version, &endpoint, network, &context)?;
157
158 print_deployment_plan(
160 &private_key,
161 &address,
162 &endpoint,
163 &network,
164 &local,
165 &skipped,
166 &remote,
167 &check_tasks_for_warnings(&endpoint, network, &local, &command.action),
168 consensus_version,
169 command,
170 );
171
172 if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
174 println!("β Deployment aborted.");
175 return Ok(());
176 }
177
178 let rng = &mut rand::thread_rng();
180
181 let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
183
184 for (_, program, _, _, _, _) in remote {
186 vm.process().write().add_program(&program)?;
188 }
189
190 let query = SnarkVMQuery::from(&endpoint);
192
193 let mut transactions = Vec::new();
195 for (program_id, program, manifest, _, priority_fee, fee_record) in local {
196 if manifest.is_some() && !skipped.contains(&program_id) {
198 println!("π¦ Creating deployment transaction for '{}'...\n", program_id.to_string().bold());
199 let transaction = vm
201 .deploy(&private_key, &program, fee_record, priority_fee.unwrap_or(0), Some(query.clone()), rng)
202 .map_err(|e| CliError::custom(format!("Failed to generate deployment transaction: {e}")))?;
203 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
205 print_deployment_stats(&program_id.to_string(), deployment, priority_fee)?;
207 transactions.push((program_id, transaction));
209 }
210 vm.process().write().add_program(&program)?;
212 }
213
214 for (program_id, transaction) in transactions.iter() {
215 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
217 validate_deployment_limits(deployment, program_id, &network)?;
218 }
219
220 if command.action.print {
223 for (program_name, transaction) in transactions.iter() {
224 let transaction_json = serde_json::to_string_pretty(transaction)
226 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
227 println!("π¨οΈ Printing deployment for {program_name}\n{transaction_json}")
228 }
229 }
230
231 if let Some(path) = &command.action.save {
235 std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
237 for (program_name, transaction) in transactions.iter() {
238 let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
240 println!("πΎ Saving deployment for {program_name} at {}", file_path.display());
241 let transaction_json = serde_json::to_string_pretty(transaction)
242 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
243 std::fs::write(file_path, transaction_json)
244 .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
245 }
246 }
247
248 if command.action.broadcast {
250 for (i, (program_id, transaction)) in transactions.iter().enumerate() {
251 println!("\nπ‘ Broadcasting deployment for {}...", program_id.to_string().bold());
252 let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
254 if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
255 println!("β© Deployment skipped.");
256 continue;
257 }
258 let fee_id = fee.id().to_string();
259 let id = transaction.id().to_string();
260 let height_before = check_transaction::current_height(&endpoint, network)?;
261 let response = handle_broadcast(
263 &format!("{}/{}/transaction/broadcast", endpoint, network),
264 transaction,
265 &program_id.to_string(),
266 )?;
267
268 let fail_and_prompt = |msg| {
269 println!("β Failed to deploy program {program_id}: {msg}.");
270 let count = transactions.len() - i - 1;
271 if count > 0 {
273 confirm("Do you want to continue with the next deployment?", command.extra.yes)
274 } else {
275 Ok(false)
276 }
277 };
278
279 match response.status() {
280 200..=299 => {
281 let status = check_transaction::check_transaction_with_message(
282 &id,
283 Some(&fee_id),
284 &endpoint,
285 network,
286 height_before + 1,
287 command.extra.max_wait,
288 command.extra.blocks_to_check,
289 )?;
290 if status == Some(TransactionStatus::Accepted) {
291 println!("β
Deployment confirmed!");
292 } else if fail_and_prompt("Transaction apparently not accepted")? {
293 continue;
294 } else {
295 return Ok(());
296 }
297 }
298 _ => {
299 let error_message = response
300 .into_string()
301 .map_err(|e| CliError::custom(format!("Failed to read response: {e}")))?;
302 if fail_and_prompt(&error_message)? {
303 continue;
304 } else {
305 return Ok(());
306 }
307 }
308 }
309 }
310 }
311
312 Ok(())
313}
314
315fn check_tasks_for_warnings<N: Network>(
322 endpoint: &str,
323 network: NetworkName,
324 tasks: &[DeploymentTask<N>],
325 action: &TransactionAction,
326) -> Vec<String> {
327 let mut warnings = Vec::new();
328 for (program_id, _, manifest, _, _, _) in tasks {
329 if manifest.is_none() || !action.broadcast {
330 continue;
331 }
332 if fetch_program_from_network(&program_id.to_string(), endpoint, network).is_ok() {
334 warnings.push(format!(
335 "The program '{}' already exists on the network. The deployment will likely fail.",
336 program_id
337 ));
338 }
339 }
340 warnings
341}
342
343fn validate_deployment_limits<N: Network>(
345 deployment: &Deployment<N>,
346 program_id: &ProgramID<N>,
347 network: &NetworkName,
348) -> Result<()> {
349 let combined_variables = deployment.num_combined_variables()?;
351 if combined_variables > N::MAX_DEPLOYMENT_VARIABLES {
352 return Err(CliError::variable_limit_exceeded(
353 program_id,
354 combined_variables,
355 N::MAX_DEPLOYMENT_VARIABLES,
356 network,
357 )
358 .into());
359 }
360
361 let constraints = deployment.num_combined_constraints()?;
363 if constraints > N::MAX_DEPLOYMENT_CONSTRAINTS {
364 return Err(CliError::constraint_limit_exceeded(
365 program_id,
366 constraints,
367 N::MAX_DEPLOYMENT_CONSTRAINTS,
368 network,
369 )
370 .into());
371 }
372
373 Ok(())
374}
375
376#[allow(clippy::too_many_arguments)]
378fn print_deployment_plan<N: Network>(
379 private_key: &PrivateKey<N>,
380 address: &Address<N>,
381 endpoint: &str,
382 network: &NetworkName,
383 local: &[DeploymentTask<N>],
384 skipped: &[ProgramID<N>],
385 remote: &[DeploymentTask<N>],
386 warnings: &[String],
387 consensus_version: ConsensusVersion,
388 command: &LeoDeploy,
389) {
390 use colored::*;
391
392 println!("\n{}", "π οΈ Deployment Plan Summary".bold());
393 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
394
395 println!("{}", "π§ Configuration:".bold());
397 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
398 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
399 println!(" {:20}{}", "Endpoint:".cyan(), endpoint.yellow());
400 println!(" {:20}{}", "Network:".cyan(), network.to_string().yellow());
401 println!(" {:20}{}", "Consensus Version:".cyan(), (consensus_version as u8).to_string().yellow());
402
403 println!("\n{}", "π¦ Deployment Tasks:".bold());
405 if local.is_empty() {
406 println!(" (none)");
407 } else {
408 for (name, _, _, _, priority_fee, record) in local.iter().filter(|(p, ..)| !skipped.contains(p)) {
409 let priority_fee_str = priority_fee.map_or("0".into(), |v| v.to_string());
410 let record_str = if record.is_some() { "yes" } else { "no (public fee)" };
411 println!(
412 " β’ {} β priority fee: {} β fee record: {}",
413 name.to_string().cyan(),
414 priority_fee_str,
415 record_str
416 );
417 }
418 }
419
420 if !skipped.is_empty() {
422 println!("\n{}", "π« Skipped Programs:".bold().red());
423 for symbol in skipped {
424 println!(" β’ {}", symbol.to_string().dimmed());
425 }
426 }
427
428 if !remote.is_empty() {
430 println!("\n{}", "π Remote Dependencies:".bold().red());
431 println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
432 for (symbol, _, _, _, _, _) in remote {
433 println!(" β’ {}", symbol.to_string().dimmed());
434 }
435 }
436
437 println!("\n{}", "βοΈ Actions:".bold());
439 if command.action.print {
440 println!(" β’ Transaction(s) will be printed to the console.");
441 } else {
442 println!(" β’ Transaction(s) will NOT be printed to the console.");
443 }
444 if let Some(path) = &command.action.save {
445 println!(" β’ Transaction(s) will be saved to {}", path.bold());
446 } else {
447 println!(" β’ Transaction(s) will NOT be saved to a file.");
448 }
449 if command.action.broadcast {
450 println!(" β’ Transaction(s) will be broadcast to {}", endpoint.bold());
451 } else {
452 println!(" β’ Transaction(s) will NOT be broadcast to the network.");
453 }
454
455 if !warnings.is_empty() {
457 println!("\n{}", "β οΈ Warnings:".bold().red());
458 for warning in warnings {
459 println!(" β’ {}", warning.dimmed());
460 }
461 }
462
463 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ\n".dimmed());
464}
465
466fn print_deployment_stats<N: Network>(
469 program_id: &str,
470 deployment: &Deployment<N>,
471 priority_fee: Option<u64>,
472) -> Result<()> {
473 use colored::*;
474 use num_format::{Locale, ToFormattedString};
475
476 let variables = deployment.num_combined_variables()?;
478 let constraints = deployment.num_combined_constraints()?;
479 let (base_fee, (storage_cost, synthesis_cost, namespace_cost)) = deployment_cost(deployment)?;
480
481 let base_fee_cr = base_fee as f64 / 1_000_000.0;
482 let prio_fee_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0;
483 let total_fee_cr = base_fee_cr + prio_fee_cr;
484
485 println!("\n{} {}", "π Deployment Summary for".bold(), program_id.bold());
487 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
488
489 println!(" {:22}{}", "Total Variables:".cyan(), variables.to_formatted_string(&Locale::en).yellow());
491 println!(" {:22}{}", "Total Constraints:".cyan(), constraints.to_formatted_string(&Locale::en).yellow());
492 println!(
493 " {:22}{}",
494 "Max Variables:".cyan(),
495 N::MAX_DEPLOYMENT_VARIABLES.to_formatted_string(&Locale::en).green()
496 );
497 println!(
498 " {:22}{}",
499 "Max Constraints:".cyan(),
500 N::MAX_DEPLOYMENT_CONSTRAINTS.to_formatted_string(&Locale::en).green()
501 );
502
503 println!("\n{}", "π° Cost Breakdown (credits)".bold());
505 println!(
506 " {:22}{}{:.6}",
507 "Transaction Storage:".cyan(),
508 "".yellow(), storage_cost as f64 / 1_000_000.0
510 );
511 println!(" {:22}{}{:.6}", "Program Synthesis:".cyan(), "".yellow(), synthesis_cost as f64 / 1_000_000.0);
512 println!(" {:22}{}{:.6}", "Namespace:".cyan(), "".yellow(), namespace_cost as f64 / 1_000_000.0);
513 println!(" {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_fee_cr);
514 println!(" {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_fee_cr);
515
516 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed());
518 Ok(())
519}