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_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/// 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.", 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        // Get the network, accounting for overrides.
93        let network = get_network(&self.env_override.network)?;
94        // Handle each network with the appropriate parameterization.
95        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
113// A helper function to handle deployment logic.
114fn handle_deploy<N: Network>(
115    command: &LeoDeploy,
116    context: Context,
117    network: NetworkName,
118    package: Package,
119) -> Result<<LeoDeploy as Command>::Output> {
120    // Get the private key and associated address, accounting for overrides.
121    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    // Get the endpoint, accounting for overrides.
126    let endpoint = get_endpoint(&command.env_override.endpoint)?;
127
128    // Get whether the network is a devnet, accounting for overrides.
129    let is_devnet = get_is_devnet(command.env_override.devnet);
130
131    // If the consensus heights are provided, use them; otherwise, use the default heights for the network.
132    let consensus_heights =
133        command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet));
134    // Validate the provided consensus heights.
135    validate_consensus_heights(&consensus_heights)
136        .map_err(|e| CliError::custom(format!("⚠️ Invalid consensus heights: {e}")))?;
137    // Print the consensus heights being used.
138    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    // Set the consensus heights in the environment.
144    #[allow(unsafe_code)]
145    unsafe {
146        // SAFETY:
147        //  - `CONSENSUS_VERSION_HEIGHTS` is only set once and is only read in `snarkvm::prelude::load_consensus_heights`.
148        //  - There are no concurrent threads running at this point in the execution.
149        // WHY:
150        //  - This is needed because there is no way to set the desired consensus heights for a particular `VM` instance
151        //    without using the environment variable `CONSENSUS_VERSION_HEIGHTS`. Which is itself read once, and stored in a `OnceLock`.
152        std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
153    }
154
155    // Get all the programs but tests.
156    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                    // We need to read the bytecode from the filesystem.
165                    let aleo_name = format!("{}.aleo", program.name);
166                    let aleo_path = if package.manifest.program == aleo_name {
167                        // The main program in the package, so its .aleo file
168                        // will be in the build directory.
169                        package.build_directory().join("main.aleo")
170                    } else {
171                        // Some other dependency, so look in `imports`.
172                        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    // Parse the fee options.
184    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    // Split the tasks into local and remote dependencies.
209    let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
210
211    // Get the skipped programs.
212    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    // Get the consensus version.
221    let consensus_version =
222        get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
223
224    // Print a summary of the deployment plan.
225    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    // Prompt the user to confirm the plan.
239    if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
240        println!("❌ Deployment aborted.");
241        return Ok(());
242    }
243
244    // Initialize an RNG.
245    let rng = &mut rand::thread_rng();
246
247    // Initialize a new VM.
248    let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
249
250    // Load the remote dependencies into the VM.
251    let programs_and_editions = remote
252        .into_iter()
253        .map(|task| {
254            // Get the actual edition from the network if not specified.
255            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 for programs that violate edition/constructor requirements.
264    check_edition_constructor_requirements(&programs_and_editions, consensus_version, "deploy")?;
265
266    vm.process().write().add_programs_with_editions(&programs_and_editions)?;
267
268    // Remove version suffixes from the endpoint.
269    let re = regex::Regex::new(r"v\d+$").unwrap();
270    let query_endpoint = re.replace(&endpoint, "").to_string();
271
272    // Specify the query
273    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    // For each of the programs, generate a deployment transaction.
280    let mut transactions = Vec::new();
281    for Task { id, program, priority_fee, record, bytecode_size, .. } in local {
282        // If the program is a local dependency that is not skipped, generate a deployment transaction.
283        if !skipped.contains(&id) {
284            // If the program contains an upgrade config, confirm with the user that they want to proceed.
285            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            // Generate the transaction.
303            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            // Get the deployment.
307            let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
308            // Print the deployment stats.
309            print_deployment_stats(&vm, &id.to_string(), deployment, priority_fee, consensus_version, bytecode_size)?;
310            // Save the transaction.
311            transactions.push((id, transaction));
312        }
313        // Add the program to the VM.
314        vm.process().write().add_program(&program)?;
315    }
316
317    for (program_id, transaction) in transactions.iter() {
318        // Validate the deployment limits.
319        let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
320        validate_deployment_limits(deployment, program_id, &network)?;
321    }
322
323    // If the `print` option is set, print the deployment transaction to the console.
324    // The transaction is printed in JSON format.
325    if command.action.print {
326        for (program_name, transaction) in transactions.iter() {
327            // Pretty-print the transaction.
328            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 the `save` option is set, save each deployment transaction to a file in the specified directory.
335    // The file format is `program_name.deployment.json`.
336    // The directory is created if it doesn't exist.
337    if let Some(path) = &command.action.save {
338        // Create the directory if it doesn't exist.
339        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            // Save the transaction to a file.
342            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 the `broadcast` option is set, broadcast each deployment transaction to the network.
352    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            // Get and confirm the fee with the user.
356            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            // Broadcast the transaction to the network.
365            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                // Check if the user wants to continue with the next deployment.
375                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
415/// Check the tasks to warn the user about any potential issues.
416/// The following properties are checked:
417/// - If the transaction is to be broadcast:
418///     - The program does not exist on the network.
419///     - If the consensus version is less than V9, the program does not use V9 features.
420///     - If the consensus version is V9 or greater, the program contains a constructor.
421///     - The program size is approaching the limit.
422fn 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        // Check if the program exists on the network.
435        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        // Check if the program has a valid naming scheme.
440        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        // Check if the program contains restricted keywords.
449        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        // Check if the program uses V9 features.
456        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        // Check if the program contains a constructor.
460        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        // Check if the program size is approaching the limit.
465        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    // Check for a consensus version mismatch.
470    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
476/// Check if the number of variables and constraints are within the limits.
477pub(crate) fn validate_deployment_limits<N: Network>(
478    deployment: &Deployment<N>,
479    program_id: &ProgramID<N>,
480    network: &NetworkName,
481) -> Result<()> {
482    // Check if the number of variables is within the limits.
483    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    // Check if the number of constraints is within the limits.
495    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/// Pretty‑print the deployment plan without using a table.
510#[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    // ── Configuration ────────────────────────────────────────────────────
529    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    // ── Deployment tasks (bullet list) ───────────────────────────────────
537    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    // ── Skipped programs ─────────────────────────────────────────────────
554    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    // ── Remote dependencies ──────────────────────────────────────────────
562    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    // ── Actions ──────────────────────────────────────────────────────────
571    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    // ── Warnings ─────────────────────────────────────────────────────────
589    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
599/// Pretty‑print deployment statistics without a table, using the same UI
600/// conventions as `print_deployment_plan`.
601pub(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    // ── Collect statistics ────────────────────────────────────────────────
613    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    // ── Header ────────────────────────────────────────────────────────────
623    println!("\n{} {}", "πŸ“Š Deployment Summary for".bold(), program_id.bold());
624    println!("{}", "──────────────────────────────────────────────".dimmed());
625
626    // ── High‑level metrics ────────────────────────────────────────────────
627    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    // ── Cost breakdown ────────────────────────────────────────────────────
646    println!("\n{}", "πŸ’° Cost Breakdown (credits)".bold());
647    println!(
648        "  {:22}{}{:.6}",
649        "Transaction Storage:".cyan(),
650        "".yellow(), // spacer for alignment
651        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    // ── Footer rule ───────────────────────────────────────────────────────
660    println!("{}", "──────────────────────────────────────────────".dimmed());
661    Ok(())
662}