leo_lang/cli/commands/
deploy.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use 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/// Deploys an Aleo program.
45#[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        // Get the network, accounting for overrides.
83        let network = context.get_network(&self.env_override.network)?.parse()?;
84        // Handle each network with the appropriate parameterization.
85        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
103// A helper function to handle deployment logic.
104fn handle_deploy<N: Network>(
105    command: &LeoDeploy,
106    context: Context,
107    network: NetworkName,
108    package: Package,
109) -> Result<<LeoDeploy as Command>::Output> {
110    // Get the private key and associated address, accounting for overrides.
111    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    // Get the endpoint, accounting for overrides.
116    let endpoint = context.get_endpoint(&command.env_override.endpoint)?;
117
118    // Get the programs and optional manifests for all the programs.
119    let programs_and_manifests = package
120        .get_programs_and_manifests(context.home()?)?
121        .into_iter()
122        .map(|(program_name, program_string, manifest)| {
123            // Parse the program ID from the program name.
124            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            // Parse the program bytecode.
127            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    // Parse the fee options.
134    let fee_options = parse_fee_options(&private_key, &command.fee_options, programs_and_manifests.len())?;
135
136    // Zip up the programs and manifests with the fee options.
137    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    // Split the tasks into local and remote dependencies.
146    let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|(_, _, manifest, _, _, _)| manifest.is_some());
147
148    // Get the skipped programs.
149    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    // Get the consensus version.
156    let consensus_version = get_consensus_version::<N>(&command.extra.consensus_version, &endpoint, network, &context)?;
157
158    // Print a summary of the deployment plan.
159    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    // Prompt the user to confirm the plan.
173    if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
174        println!("❌ Deployment aborted.");
175        return Ok(());
176    }
177
178    // Initialize an RNG.
179    let rng = &mut rand::thread_rng();
180
181    // Initialize a new VM.
182    let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
183
184    // Load the remote dependencies into the VM.
185    for (_, program, _, _, _, _) in remote {
186        // If the program is a remote dependency, add it to the VM.
187        vm.process().write().add_program(&program)?;
188    }
189
190    // Specify the query
191    let query = SnarkVMQuery::from(&endpoint);
192
193    // For each of the programs, generate a deployment transaction.
194    let mut transactions = Vec::new();
195    for (program_id, program, manifest, _, priority_fee, fee_record) in local {
196        // If the program is a local dependency that is not skipped, generate a deployment transaction.
197        if manifest.is_some() && !skipped.contains(&program_id) {
198            println!("πŸ“¦ Creating deployment transaction for '{}'...\n", program_id.to_string().bold());
199            // Generate the transaction.
200            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            // Get the deployment.
204            let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
205            // Print the deployment stats.
206            print_deployment_stats(&program_id.to_string(), deployment, priority_fee)?;
207            // Save the transaction.
208            transactions.push((program_id, transaction));
209        }
210        // Add the program to the VM.
211        vm.process().write().add_program(&program)?;
212    }
213
214    for (program_id, transaction) in transactions.iter() {
215        // Validate the deployment limits.
216        let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
217        validate_deployment_limits(deployment, program_id, &network)?;
218    }
219
220    // If the `print` option is set, print the deployment transaction to the console.
221    // The transaction is printed in JSON format.
222    if command.action.print {
223        for (program_name, transaction) in transactions.iter() {
224            // Pretty-print the transaction.
225            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 the `save` option is set, save each deployment transaction to a file in the specified directory.
232    // The file format is `program_name.deployment.json`.
233    // The directory is created if it doesn't exist.
234    if let Some(path) = &command.action.save {
235        // Create the directory if it doesn't exist.
236        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            // Save the transaction to a file.
239            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 the `broadcast` option is set, broadcast each deployment transaction to the network.
249    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            // Get and confirm the fee with the user.
253            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            // Broadcast the transaction to the network.
262            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                // Check if the user wants to continue with the next deployment.
272                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
315/// Check the tasks to warn the user about any potential issues.
316/// Only local programs are checked.
317/// The following properties are checked:
318/// - If the transaction is to be broadcast:
319///     - The program does not exist on the network.
320/// - The program's external dependencies are the latest version.
321fn 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        // Check if the program exists on the network.
333        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
343/// Check if the number of variables and constraints are within the limits.
344fn validate_deployment_limits<N: Network>(
345    deployment: &Deployment<N>,
346    program_id: &ProgramID<N>,
347    network: &NetworkName,
348) -> Result<()> {
349    // Check if the number of variables is within the limits.
350    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    // Check if the number of constraints is within the limits.
362    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/// Pretty‑print the deployment plan without using a table.
377#[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    // ── Configuration ────────────────────────────────────────────────────
396    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    // ── Deployment tasks (bullet list) ───────────────────────────────────
404    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    // ── Skipped programs ─────────────────────────────────────────────────
421    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    // ── Remote dependencies ──────────────────────────────────────────────
429    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    // ── Actions ──────────────────────────────────────────────────────────
438    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    // ── Warnings ─────────────────────────────────────────────────────────
456    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
466/// Pretty‑print deployment statistics without a table, using the same UI
467/// conventions as `print_deployment_plan`.
468fn 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    // ── Collect statistics ────────────────────────────────────────────────
477    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    // ── Header ────────────────────────────────────────────────────────────
486    println!("\n{} {}", "πŸ“Š Deployment Summary for".bold(), program_id.bold());
487    println!("{}", "──────────────────────────────────────────────".dimmed());
488
489    // ── High‑level metrics ────────────────────────────────────────────────
490    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    // ── Cost breakdown ────────────────────────────────────────────────────
504    println!("\n{}", "πŸ’° Cost Breakdown (credits)".bold());
505    println!(
506        "  {:22}{}{:.6}",
507        "Transaction Storage:".cyan(),
508        "".yellow(), // spacer for alignment
509        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    // ── Footer rule ───────────────────────────────────────────────────────
517    println!("{}", "──────────────────────────────────────────────".dimmed());
518    Ok(())
519}