Skip to main content

leo_lang/cli/commands/
deploy.rs

1// Copyright (C) 2019-2026 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, retry_network_call};
22
23use aleo_std::StorageMode;
24use rand::CryptoRng;
25#[cfg(not(feature = "only_testnet"))]
26use snarkvm::prelude::{CanaryV0, MainnetV0};
27use snarkvm::{
28    circuit::{Aleo, AleoTestnetV0},
29    ledger::{
30        query::{Query as SnarkVMQuery, QueryTrait},
31        store::helpers::memory::BlockMemory,
32    },
33    prelude::{
34        Certificate,
35        ConsensusVersion,
36        Deployment,
37        Fee,
38        Program,
39        ProgramID,
40        ProgramOwner,
41        Rng,
42        Stack,
43        TestnetV0,
44        ToBytes,
45        Transaction,
46        VM,
47        VerifyingKey,
48        check_program_plaintext_sizes,
49        deployment_cost,
50        execution_cost_for_authorization,
51        minimum_cost_in_microcredits_v1,
52        minimum_cost_in_microcredits_v2,
53        minimum_cost_in_microcredits_v3,
54        store::{ConsensusStore, helpers::memory::ConsensusMemory},
55    },
56    synthesizer::program::StackTrait,
57};
58
59use colored::*;
60use itertools::Itertools;
61use std::{
62    collections::{HashMap, HashSet},
63    fs,
64    path::PathBuf,
65};
66
67/// Deploys an Aleo program.
68#[derive(Parser, Debug)]
69pub struct LeoDeploy {
70    #[clap(flatten)]
71    pub(crate) fee_options: FeeOptions,
72    #[clap(flatten)]
73    pub(crate) action: TransactionAction,
74    #[clap(flatten)]
75    pub(crate) env_override: EnvOptions,
76    #[clap(flatten)]
77    pub(crate) key_override: PrivateKeyOptions,
78    #[clap(flatten)]
79    pub(crate) consensus_override: ConsensusOptions,
80    #[clap(flatten)]
81    pub(crate) extra: ExtraOptions,
82    #[clap(long, help = "Skips deployment of any program that contains one of the given substrings.", value_delimiter = ',', num_args = 1..)]
83    pub(crate) skip: Vec<String>,
84    #[clap(
85        long,
86        help = "Deploy the program under a different name, producing a genuinely distinct on-chain deployment. Programs importing the original name are not redirected to the renamed copy."
87    )]
88    pub(crate) rename: Option<String>,
89    #[clap(flatten)]
90    pub(crate) build_options: BuildOptions,
91    #[clap(
92        long,
93        value_name = "DIR",
94        help = "Directory containing local .aleo imports when --path points to an Aleo bytecode file; other imports are fetched from the network"
95    )]
96    pub(crate) imports_dir: Option<PathBuf>,
97    #[clap(long, help = "Use placeholder certificate and verifying keys during deployment.", default_value = "false")]
98    pub(crate) skip_deploy_certificate: bool,
99}
100
101pub struct Task<N: Network> {
102    pub id: ProgramID<N>,
103    pub program: Program<N>,
104    pub edition: Option<u16>,
105    pub is_local: bool,
106    pub priority_fee: Option<u64>,
107    pub record: Option<Record<N, Plaintext<N>>>,
108    pub bytecode_size: usize,
109}
110
111/// Prepared tasks partitioned into local, remote, and skipped sets.
112type PreparedTasks<N> = (Vec<Task<N>>, Vec<Task<N>>, HashSet<ProgramID<N>>);
113
114/// Deployment transactions paired with their stats.
115type DeployTransactions<N> = (Vec<(ProgramID<N>, Transaction<N>)>, Vec<DeploymentStats>);
116
117/// Per-member task info: (member name, local tasks, skipped set).
118type MemberTasks<N> = (String, Vec<Task<N>>, HashSet<ProgramID<N>>);
119
120/// One-time deployment state shared across workspace members.
121struct DeploySetup<N: Network> {
122    private_key: PrivateKey<N>,
123    address: Address<N>,
124    endpoint: String,
125    consensus_version: ConsensusVersion,
126    network: NetworkName,
127    vm: VM<N, ConsensusMemory<N>>,
128    query: SnarkVMQuery<N, BlockMemory<N>>,
129}
130
131impl Command for LeoDeploy {
132    type Input = Package;
133    type Output = DeployOutput;
134
135    fn log_span(&self) -> Span {
136        tracing::span!(tracing::Level::INFO, "Leo")
137    }
138
139    fn prelude(&self, context: Context) -> Result<Self::Input> {
140        let path = context.dir()?;
141        if path.extension().and_then(|extension| extension.to_str()) == Some("aleo") {
142            if context.package_filter.is_some() {
143                return Err(crate::errors::custom("`--package` cannot be used with an Aleo bytecode file.").into());
144            }
145            if self.rename.is_some() {
146                return Err(crate::errors::custom("`--rename` is not supported when deploying Aleo bytecode.").into());
147            }
148            let home_path = context.home()?;
149            let network = get_network(&self.env_override.network)?;
150            let endpoint = get_endpoint(&self.env_override.endpoint)?;
151            return Package::from_aleo_file(
152                path,
153                home_path,
154                self.imports_dir.as_deref(),
155                true,
156                self.build_options.no_local,
157                Some(network),
158                Some(&endpoint),
159                self.env_override.network_retries,
160            );
161        }
162        if self.imports_dir.is_some() {
163            return Err(
164                crate::errors::custom("`--imports-dir` requires `--path` to point to an Aleo bytecode file.").into()
165            );
166        }
167
168        LeoBuild {
169            env_override: self.env_override.clone(),
170            options: {
171                let mut options = self.build_options.clone();
172                options.no_cache = true;
173                options
174            },
175            rename: self.rename.clone(),
176        }
177        .execute(context)
178    }
179
180    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
181        // Libraries cannot be deployed.
182        if input.compilation_units.last().is_some_and(|p| p.kind.is_library()) {
183            return Err(crate::errors::custom("Cannot deploy a library package. Only programs can be deployed.").into());
184        }
185
186        // Get the network, accounting for overrides.
187        let network = get_network(&self.env_override.network)?;
188        // Handle each network with the appropriate parameterization.
189        match network {
190            NetworkName::TestnetV0 => handle_deploy::<TestnetV0, AleoTestnetV0>(&self, context, network, input),
191            NetworkName::MainnetV0 => {
192                #[cfg(feature = "only_testnet")]
193                panic!("Mainnet chosen with only_testnet feature");
194                #[cfg(not(feature = "only_testnet"))]
195                handle_deploy::<MainnetV0, snarkvm::circuit::AleoV0>(&self, context, network, input)
196            }
197            NetworkName::CanaryV0 => {
198                #[cfg(feature = "only_testnet")]
199                panic!("Canary chosen with only_testnet feature");
200                #[cfg(not(feature = "only_testnet"))]
201                handle_deploy::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(&self, context, network, input)
202            }
203        }
204    }
205
206    fn execute(self, context: Context) -> Result<Self::Output> {
207        // Intercept workspace mode before the default prelude+apply flow.
208        let path = context.dir()?;
209        let is_aleo_file = path.extension().and_then(|extension| extension.to_str()) == Some("aleo");
210        if self.imports_dir.is_some() && !is_aleo_file {
211            return Err(
212                crate::errors::custom("`--imports-dir` requires `--path` to point to an Aleo bytecode file.").into()
213            );
214        }
215        let workspace_targets = if is_aleo_file { None } else { context.resolve_targets()? };
216        match workspace_targets {
217            Some((_, targets)) if targets.len() > 1 => handle_workspace_deploy(self, context, targets),
218            _ => {
219                // Single target or no workspace: use the standard flow.
220                let input = self.prelude(context.clone())?;
221                let span = self.log_span();
222                let span = span.enter();
223                let out = self.apply(context, input);
224                drop(span);
225                out
226            }
227        }
228    }
229}
230
231// ── Extracted helpers ───────────────────────────────────────────────────────
232
233/// Create the one-time deployment setup: credentials, consensus, VM, and query.
234fn create_deploy_setup<N: Network>(
235    command: &LeoDeploy,
236    context: &Context,
237    network: NetworkName,
238) -> Result<DeploySetup<N>> {
239    // Get the private key and associated address, accounting for overrides.
240    let private_key = get_private_key(&command.key_override.private_key)?;
241    let address =
242        Address::try_from(&private_key).map_err(|e| crate::errors::custom(format!("Failed to parse address: {e}")))?;
243
244    // Get the endpoint, accounting for overrides.
245    let endpoint = get_endpoint(&command.env_override.endpoint)?;
246
247    // Get whether the network is a devnet, accounting for overrides.
248    let is_devnet = get_is_devnet(command.consensus_override.devnet);
249
250    // If the consensus heights are provided, use them; otherwise, use the default heights for the network.
251    let consensus_heights = command
252        .consensus_override
253        .consensus_heights
254        .clone()
255        .unwrap_or_else(|| get_consensus_heights(network, is_devnet));
256    // Validate the provided consensus heights.
257    validate_consensus_heights(&consensus_heights)
258        .map_err(|e| crate::errors::custom(format!("⚠️ Invalid consensus heights: {e}")))?;
259    // Print the consensus heights being used.
260    let consensus_heights_string = consensus_heights.iter().format(",").to_string();
261    println!(
262        "\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"
263    );
264
265    // Set the consensus heights in the environment.
266    #[allow(unsafe_code)]
267    unsafe {
268        // SAFETY:
269        //  - `CONSENSUS_VERSION_HEIGHTS` is only set once and is only read in `snarkvm::prelude::load_consensus_heights`.
270        //  - There are no concurrent threads running at this point in the execution.
271        // WHY:
272        //  - This is needed because there is no way to set the desired consensus heights for a particular `VM` instance
273        //    without using the environment variable `CONSENSUS_VERSION_HEIGHTS`. Which is itself read once, and stored in a `OnceLock`.
274        std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
275    }
276
277    // Get the consensus version.
278    let consensus_version = get_consensus_version(
279        &command.extra.consensus_version,
280        &endpoint,
281        network,
282        &consensus_heights,
283        context,
284        command.env_override.network_retries,
285    )?;
286
287    // Initialize a new VM.
288    let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
289
290    // Specify the query.
291    let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
292        endpoint
293            .parse::<Uri>()
294            .map_err(|e| crate::errors::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
295    );
296
297    Ok(DeploySetup { private_key, address, endpoint, consensus_version, network, vm, query })
298}
299
300/// Extract programs from a package and create deployment tasks.
301fn prepare_package_tasks<N: Network>(
302    command: &LeoDeploy,
303    setup: &DeploySetup<N>,
304    package: &Package,
305) -> Result<PreparedTasks<N>> {
306    // Get all the programs but tests and libraries (libraries have no AVM bytecode).
307    let programs = package.compilation_units.iter().filter(|unit| unit.kind.is_program()).cloned();
308
309    let programs_and_bytecode: Vec<(leo_package::CompilationUnit, String)> = programs
310        .into_iter()
311        .map(|program| {
312            let bytecode = match &program.data {
313                ProgramData::Bytecode(s) => s.clone(),
314                ProgramData::SourcePath { .. } => {
315                    // We need to read the bytecode from its own build directory.
316                    let aleo_path = package.unit_bytecode_path(&program.name.to_string());
317                    fs::read_to_string(aleo_path.clone()).map_err(|e| {
318                        crate::errors::custom(format!("Failed to read file {}: {e}", aleo_path.display()))
319                    })?
320                }
321            };
322
323            Ok((program, bytecode))
324        })
325        .collect::<Result<_>>()?;
326
327    // Parse the fee options.
328    let fee_options = parse_fee_options(&setup.private_key, &command.fee_options, programs_and_bytecode.len())?;
329
330    let tasks: Vec<Task<N>> = programs_and_bytecode
331        .into_iter()
332        .zip(fee_options)
333        .map(|((program, bytecode), (priority_fee, record))| {
334            let id_str = format!("{}", program.name);
335            let id = id_str
336                .parse()
337                .map_err(|e| crate::errors::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
338            let bytecode_size = bytecode.len();
339            let parsed_program =
340                bytecode.parse().map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
341            Ok(Task {
342                id,
343                program: parsed_program,
344                edition: program.edition,
345                is_local: program.is_local,
346                priority_fee,
347                record,
348                bytecode_size,
349            })
350        })
351        .collect::<Result<_>>()?;
352
353    // Split the tasks into local and remote dependencies.
354    let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
355
356    // Get the skipped programs.
357    let skipped: HashSet<ProgramID<N>> = local
358        .iter()
359        .filter_map(|task| {
360            let id_string = task.id.to_string();
361            command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
362        })
363        .collect();
364
365    Ok((local, remote, skipped))
366}
367
368/// Load remote dependencies into the shared VM.
369fn load_remote_deps<N: Network>(command: &LeoDeploy, setup: &DeploySetup<N>, remote: Vec<Task<N>>) -> Result<()> {
370    if remote.is_empty() {
371        return Ok(());
372    }
373
374    let programs_and_editions = remote
375        .into_iter()
376        .map(|task| {
377            // Get the actual edition from the network if not specified.
378            let edition = match task.edition {
379                Some(e) => e,
380                None => leo_package::fetch_latest_edition(
381                    &task.id.to_string(),
382                    &setup.endpoint,
383                    setup.network,
384                    command.env_override.network_retries,
385                )?,
386            };
387            Ok((task.program, edition))
388        })
389        .collect::<Result<Vec<_>>>()?;
390
391    // Check for programs that violate edition/constructor requirements.
392    check_edition_constructor_requirements(&programs_and_editions, setup.consensus_version, "deploy")?;
393    setup.vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
394    Ok(())
395}
396
397/// Generate deployment transactions for local programs.
398///
399/// Returns `None` if the user aborted (e.g. declined a constructor confirmation).
400fn generate_deploy_transactions<N: Network, A: Aleo<Network = N>>(
401    command: &LeoDeploy,
402    setup: &DeploySetup<N>,
403    local: Vec<Task<N>>,
404    skipped: &HashSet<ProgramID<N>>,
405    already_deployed: &mut HashSet<ProgramID<N>>,
406) -> Result<Option<DeployTransactions<N>>> {
407    let rng = &mut rand::rng();
408    let mut transactions = Vec::new();
409    let mut all_stats = Vec::new();
410
411    for Task { id, program, edition, priority_fee, record, bytecode_size, .. } in local {
412        // Deploy if not user-skipped and not already deployed by an earlier workspace member.
413        if !skipped.contains(&id) && !already_deployed.contains(&id) {
414            // Check plaintext declarations before deployment synthesis.
415            if setup.consensus_version >= ConsensusVersion::V20 {
416                let max_bits = consensus_limit(
417                    &N::MAX_PLAINTEXT_TYPE_SIZE_IN_BITS,
418                    setup.consensus_version,
419                    N::LATEST_MAX_PLAINTEXT_TYPE_SIZE_IN_BITS(),
420                );
421                let stack = Stack::new(setup.vm.process(), &program)?;
422                check_program_plaintext_sizes(&program, &stack, max_bits)?;
423            }
424            // If the program has a constructor, confirm with the user.
425            if let Some(constructor) = program.constructor() {
426                println!(
427                    r"
428🔧 Your program '{}' has the following constructor.
429──────────────────────────────────────────────
430{constructor}
431──────────────────────────────────────────────
432Once it is deployed, it CANNOT be changed.
433",
434                    id.to_string().bold()
435                );
436                if !confirm("Would you like to proceed?", command.extra.yes)? {
437                    println!("❌ Deployment aborted.");
438                    return Ok(None);
439                }
440            }
441            println!("📦 Creating deployment transaction for '{}'...\n", id.to_string().bold());
442            let (transaction, stats) = if command.skip_deploy_certificate {
443                println!("⚠️  Skipping deployment certificate and verifier key generation as per user request.\n");
444                deploy_with_placeholder_certificate::<N, A, _>(
445                    &setup.vm,
446                    &setup.private_key,
447                    &program,
448                    edition.unwrap_or(0),
449                    record,
450                    priority_fee,
451                    bytecode_size,
452                    setup.consensus_version,
453                    &setup.query,
454                    command.env_override.network_retries,
455                    rng,
456                )?
457            } else {
458                // Generate the transaction.
459                let transaction = setup
460                    .vm
461                    .deploy(&setup.private_key, &program, record, priority_fee.unwrap_or(0), Some(&setup.query), rng)
462                    .map_err(|e| crate::errors::custom(format!("Failed to generate deployment transaction: {e}")))?;
463                // Get the deployment.
464                let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
465                // Add the program to the VM before calculating function costs.
466                setup.vm.process().lock().add_program(&program)?;
467                // Compute the deployment stats.
468                let stats = compute_deployment_stats(
469                    &setup.vm,
470                    deployment,
471                    priority_fee,
472                    setup.consensus_version,
473                    bytecode_size,
474                    rng,
475                )?;
476                (transaction, stats)
477            };
478
479            // Print the deployment summary and save the transaction and stats.
480            print_deployment_summary(&id.to_string(), &stats);
481            if command.action.broadcast {
482                warn_if_transaction_oversized(&id, &transaction, setup.consensus_version, "deployment");
483            }
484            transactions.push((id, transaction));
485            all_stats.push(stats);
486            already_deployed.insert(id);
487
488            if !command.skip_deploy_certificate {
489                // Validate the deployment limits for the current transaction.
490                let (program_id, transaction) = transactions.last().expect("Transaction was just pushed");
491                let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
492                validate_deployment_limits(deployment, program_id, &setup.network, setup.consensus_version)?;
493            }
494        }
495
496        // Add the program to the VM. This ensures skipped programs are available for later imports.
497        if let Err(e) = setup.vm.process().lock().add_program(&program) {
498            tracing::debug!("Program {id} already in VM: {e}");
499        }
500    }
501
502    Ok(Some((transactions, all_stats)))
503}
504
505/// Handle print/save/broadcast of deployment transactions.
506fn output_and_broadcast<N: Network>(
507    command: &LeoDeploy,
508    setup: &DeploySetup<N>,
509    context: &Context,
510    config: Option<Config>,
511    transactions: &[(ProgramID<N>, Transaction<N>)],
512    all_stats: &[DeploymentStats],
513) -> Result<DeployOutput> {
514    // If the `print` option is set, print the deployment transaction to the console.
515    // The transaction is printed in JSON format.
516    if command.action.print {
517        for (program_name, transaction) in transactions.iter() {
518            // Pretty-print the transaction.
519            let transaction_json = serde_json::to_string_pretty(transaction)
520                .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
521            println!("🖨️ Printing deployment for {program_name}\n{transaction_json}")
522        }
523    }
524
525    // If the `save` option is set, save each deployment transaction to a file in the specified directory.
526    // The file format is `program_name.deployment.json`.
527    // The directory is created if it doesn't exist.
528    if let Some(path) = &command.action.save {
529        // Create the directory if it doesn't exist.
530        std::fs::create_dir_all(path).map_err(|e| crate::errors::custom(format!("Failed to create directory: {e}")))?;
531        for (program_name, transaction) in transactions.iter() {
532            // Save the transaction to a file.
533            let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
534            println!("💾 Saving deployment for {program_name} at {}", file_path.display());
535            let transaction_json = serde_json::to_string_pretty(transaction)
536                .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
537            std::fs::write(file_path, transaction_json)
538                .map_err(|e| crate::errors::custom(format!("Failed to write transaction to file: {e}")))?;
539        }
540    }
541
542    // If the `broadcast` option is set, broadcast each deployment transaction to the network.
543    let mut all_broadcasts = Vec::new();
544    if command.action.broadcast {
545        for (i, (program_id, transaction)) in transactions.iter().enumerate() {
546            println!("\n📡 Broadcasting deployment for {}...", program_id.to_string().bold());
547            // Get and confirm the fee with the user.
548            let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
549            if !confirm_fee(
550                &fee,
551                &setup.private_key,
552                &setup.address,
553                &setup.endpoint,
554                setup.network,
555                context,
556                command.extra.yes,
557            )? {
558                println!("⏩ Deployment skipped.");
559                continue;
560            }
561            let fee_id = fee.id().to_string();
562            let fee_transaction_id = Transaction::from_fee(fee.clone())?.id().to_string();
563            let id = transaction.id().to_string();
564            let height_before = check_transaction::current_height(
565                &setup.endpoint,
566                setup.network,
567                command.env_override.network_retries,
568            )?;
569            // Broadcast the transaction to the network.
570            let (message, status) = handle_broadcast(
571                &format!("{}/{}/transaction/broadcast", setup.endpoint, setup.network),
572                transaction,
573                &program_id.to_string(),
574            )?;
575
576            let fail_and_prompt = |msg| {
577                println!("❌ Failed to deploy program {program_id}: {msg}.");
578                let count = transactions.len() - i - 1;
579                // Check if the user wants to continue with the next deployment.
580                if count > 0 {
581                    confirm("Do you want to continue with the next deployment?", command.extra.yes)
582                } else {
583                    Ok(false)
584                }
585            };
586
587            match status {
588                200..=299 => {
589                    let tx_status = check_transaction::check_transaction_with_message(
590                        &id,
591                        Some(&fee_id),
592                        &setup.endpoint,
593                        setup.network,
594                        height_before + 1,
595                        command.extra.max_wait,
596                        command.extra.blocks_to_check,
597                        command.env_override.network_retries,
598                    )?;
599                    let confirmed = tx_status == Some(TransactionStatus::Accepted);
600                    if confirmed {
601                        println!("✅ Deployment confirmed!");
602                    } else if fail_and_prompt("could not find the transaction on the network")? {
603                        continue;
604                    } else {
605                        return Ok(build_deploy_output(config.clone(), transactions, all_stats, &all_broadcasts));
606                    }
607                    all_broadcasts.push(BroadcastStats {
608                        fee_id: fee_id.clone(),
609                        fee_transaction_id: fee_transaction_id.clone(),
610                        confirmed,
611                    });
612                }
613                _ => {
614                    if fail_and_prompt(&message)? {
615                        continue;
616                    } else {
617                        return Ok(build_deploy_output(config.clone(), transactions, all_stats, &all_broadcasts));
618                    }
619                }
620            }
621        }
622    }
623
624    Ok(build_deploy_output(config, transactions, all_stats, &all_broadcasts))
625}
626
627fn enforce_local_deploy_constructor_requirements<N: Network>(
628    tasks: &[Task<N>],
629    skipped: &HashSet<ProgramID<N>>,
630    consensus_version: ConsensusVersion,
631) -> Result<()> {
632    if consensus_version < ConsensusVersion::V9 {
633        return Ok(());
634    }
635
636    for Task { id, program, .. } in tasks {
637        if !skipped.contains(id) && !program.contains_constructor() {
638            return Err(crate::errors::missing_constructor(id).into());
639        }
640    }
641
642    Ok(())
643}
644
645// ── Single-package deploy ───────────────────────────────────────────────────
646
647// Handle deployment for a single package (non-workspace mode).
648fn handle_deploy<N: Network, A: Aleo<Network = N>>(
649    command: &LeoDeploy,
650    context: Context,
651    network: NetworkName,
652    package: Package,
653) -> Result<<LeoDeploy as Command>::Output> {
654    let setup = create_deploy_setup::<N>(command, &context, network)?;
655    let config = Some(Config {
656        address: setup.address.to_string(),
657        network: network.to_string(),
658        endpoint: Some(setup.endpoint.clone()),
659        consensus_version: Some(setup.consensus_version as u8),
660    });
661
662    let (local, remote, skipped) = prepare_package_tasks::<N>(command, &setup, &package)?;
663    enforce_local_deploy_constructor_requirements(&local, &skipped, setup.consensus_version)?;
664
665    // Print a summary of the deployment plan.
666    print_deployment_plan(
667        &setup.private_key,
668        &setup.address,
669        &setup.endpoint,
670        &network,
671        &local,
672        &skipped,
673        &remote,
674        &check_tasks_for_warnings(&setup.endpoint, network, &local, setup.consensus_version, command),
675        setup.consensus_version,
676        command,
677    );
678
679    // Prompt the user to confirm the plan.
680    if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
681        println!("❌ Deployment aborted.");
682        return Ok(DeployOutput::default());
683    }
684
685    // Load remote dependencies into the VM.
686    load_remote_deps(command, &setup, remote)?;
687
688    // Generate deployment transactions.
689    let mut already_deployed = HashSet::new();
690    match generate_deploy_transactions::<N, A>(command, &setup, local, &skipped, &mut already_deployed)? {
691        Some((transactions, stats)) => output_and_broadcast(command, &setup, &context, config, &transactions, &stats),
692        None => Ok(DeployOutput::default()),
693    }
694}
695
696// ── Workspace deploy ────────────────────────────────────────────────────────
697
698/// Handle deployment for workspace mode: build each member, then deploy all.
699fn handle_workspace_deploy(command: LeoDeploy, context: Context, targets: Vec<PathBuf>) -> Result<DeployOutput> {
700    // A single `--rename` cannot apply across multiple workspace members.
701    if command.rename.is_some() {
702        return Err(crate::errors::custom(
703            "`--rename` is not supported when deploying multiple workspace members; deploy a single program instead.",
704        )
705        .into());
706    }
707    // Build each member, collecting deployable packages.
708    let mut packages = Vec::new();
709    for target in &targets {
710        let member_name = target.file_name().and_then(|n| n.to_str()).unwrap_or("?");
711        println!("\n--- building workspace member '{member_name}' ---");
712        let member_ctx = context.with_path(target.clone());
713        let package = LeoBuild {
714            env_override: command.env_override.clone(),
715            options: {
716                let mut opts = command.build_options.clone();
717                opts.no_cache = true;
718                opts
719            },
720            // Workspace deploys reject `--rename` upfront, so members never rename.
721            rename: None,
722        }
723        .execute(member_ctx)?;
724        // Skip library packages - they cannot be deployed.
725        if !package.compilation_units.last().is_some_and(|p| p.kind.is_library()) {
726            packages.push((member_name.to_string(), package));
727        }
728    }
729
730    if packages.is_empty() {
731        return Err(crate::errors::custom("No deployable workspace members found (all are libraries).").into());
732    }
733
734    // Get the network, accounting for overrides.
735    let network = get_network(&command.env_override.network)?;
736    match network {
737        NetworkName::TestnetV0 => {
738            handle_workspace_deploy_inner::<TestnetV0, AleoTestnetV0>(&command, context, network, packages)
739        }
740        NetworkName::MainnetV0 => {
741            #[cfg(feature = "only_testnet")]
742            panic!("Mainnet chosen with only_testnet feature");
743            #[cfg(not(feature = "only_testnet"))]
744            handle_workspace_deploy_inner::<MainnetV0, snarkvm::circuit::AleoV0>(&command, context, network, packages)
745        }
746        NetworkName::CanaryV0 => {
747            #[cfg(feature = "only_testnet")]
748            panic!("Canary chosen with only_testnet feature");
749            #[cfg(not(feature = "only_testnet"))]
750            handle_workspace_deploy_inner::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(
751                &command, context, network, packages,
752            )
753        }
754    }
755}
756
757/// Inner workspace deploy logic, generic over network type.
758fn handle_workspace_deploy_inner<N: Network, A: Aleo<Network = N>>(
759    command: &LeoDeploy,
760    context: Context,
761    network: NetworkName,
762    packages: Vec<(String, Package)>,
763) -> Result<DeployOutput> {
764    let setup = create_deploy_setup::<N>(command, &context, network)?;
765    let config = Some(Config {
766        address: setup.address.to_string(),
767        network: network.to_string(),
768        endpoint: Some(setup.endpoint.clone()),
769        consensus_version: Some(setup.consensus_version as u8),
770    });
771
772    // Prepare tasks for all members, deduplicating remote deps.
773    let mut member_tasks: Vec<MemberTasks<N>> = Vec::new();
774    let mut all_remote: Vec<Task<N>> = Vec::new();
775    let mut seen_remote: HashMap<ProgramID<N>, Option<u16>> = HashMap::new();
776
777    for (name, package) in &packages {
778        let (local, remote, skipped) = prepare_package_tasks::<N>(command, &setup, package)?;
779        for task in remote {
780            match seen_remote.entry(task.id) {
781                std::collections::hash_map::Entry::Vacant(e) => {
782                    e.insert(task.edition);
783                    all_remote.push(task);
784                }
785                std::collections::hash_map::Entry::Occupied(e) => {
786                    if *e.get() != task.edition {
787                        return Err(crate::errors::custom(format!(
788                            "Workspace members disagree on edition for remote dependency '{}': \
789                             {:?} vs {:?}. Ensure all members specify the same edition.",
790                            task.id,
791                            e.get(),
792                            task.edition,
793                        ))
794                        .into());
795                    }
796                }
797            }
798        }
799        member_tasks.push((name.clone(), local, skipped));
800    }
801
802    for (_, local, skipped) in &member_tasks {
803        enforce_local_deploy_constructor_requirements(local, skipped, setup.consensus_version)?;
804    }
805
806    // Collect warnings across all members.
807    let mut all_warnings = Vec::new();
808    for (_, local, _) in &member_tasks {
809        all_warnings.extend(check_tasks_for_warnings(
810            &setup.endpoint,
811            network,
812            local,
813            setup.consensus_version,
814            command,
815        ));
816    }
817
818    // Print workspace deployment plan.
819    print_workspace_deployment_plan(&setup, &member_tasks, &all_remote, &all_warnings, command);
820
821    // Prompt user confirmation once for the entire workspace.
822    if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
823        println!("❌ Deployment aborted.");
824        return Ok(DeployOutput::default());
825    }
826
827    // Load all unique remote deps into the shared VM.
828    load_remote_deps(command, &setup, all_remote)?;
829
830    // Deploy each member's programs in dependency order with shared VM.
831    // Edition mismatch is not a concern here: `already_deployed` only tracks
832    // local programs, which all have `edition: None`.
833    let mut already_deployed: HashSet<ProgramID<N>> = HashSet::new();
834    let mut all_transactions = Vec::new();
835    let mut all_stats = Vec::new();
836
837    for (name, local, skipped) in member_tasks {
838        println!("\n--- deploying workspace member '{name}' ---");
839        match generate_deploy_transactions::<N, A>(command, &setup, local, &skipped, &mut already_deployed)? {
840            Some((txns, stats)) => {
841                all_transactions.extend(txns);
842                all_stats.extend(stats);
843            }
844            None => return Ok(DeployOutput::default()),
845        }
846    }
847
848    output_and_broadcast(command, &setup, &context, config, &all_transactions, &all_stats)
849}
850
851/// Pretty-print the workspace deployment plan.
852fn print_workspace_deployment_plan<N: Network>(
853    setup: &DeploySetup<N>,
854    member_tasks: &[MemberTasks<N>],
855    all_remote: &[Task<N>],
856    warnings: &[String],
857    command: &LeoDeploy,
858) {
859    println!("\n{}", "🛠️  Workspace Deployment Plan".bold());
860    println!("{}", "──────────────────────────────────────────────".dimmed());
861
862    // ── Configuration ────────────────────────────────────────────────────
863    println!("{}", "🔧 Configuration:".bold());
864    println!("  {:20}{}", "Private Key:".cyan(), format!("{}...", &setup.private_key.to_string()[..24]).yellow());
865    println!("  {:20}{}", "Address:".cyan(), format!("{}...", &setup.address.to_string()[..24]).yellow());
866    println!("  {:20}{}", "Endpoint:".cyan(), setup.endpoint.yellow());
867    println!("  {:20}{}", "Network:".cyan(), setup.network.to_string().yellow());
868    println!("  {:20}{}", "Consensus Version:".cyan(), (setup.consensus_version as u8).to_string().yellow());
869
870    // ── Workspace members ───────────────────────────────────────────────
871    let mut already_seen: HashSet<ProgramID<N>> = HashSet::new();
872    println!("\n{}", "📦 Workspace Members:".bold());
873    for (name, local, skipped) in member_tasks {
874        println!("  {}:", name.bold());
875        if local.is_empty() {
876            println!("    (no programs)");
877            continue;
878        }
879        for task in local {
880            if skipped.contains(&task.id) {
881                println!("    • {} {}", task.id.to_string().dimmed(), "(skipped by --skip)".dimmed());
882            } else if already_seen.contains(&task.id) {
883                println!("    • {} {}", task.id.to_string().dimmed(), "(already covered by earlier member)".dimmed());
884            } else {
885                let priority_fee_str = task.priority_fee.map_or("0".into(), |v| v.to_string());
886                let record_str = if task.record.is_some() { "yes" } else { "no (public fee)" };
887                println!(
888                    "    • {}  │ priority fee: {}  │ fee record: {}",
889                    task.id.to_string().cyan(),
890                    priority_fee_str,
891                    record_str
892                );
893                already_seen.insert(task.id);
894            }
895        }
896    }
897
898    // ── Remote dependencies ──────────────────────────────────────────────
899    if !all_remote.is_empty() {
900        println!("\n{}", "🌐 Remote Dependencies:".bold().red());
901        println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
902        for task in all_remote {
903            println!("  • {}", task.id.to_string().dimmed());
904        }
905    }
906
907    // ── Actions ──────────────────────────────────────────────────────────
908    println!("\n{}", "⚙️ Actions:".bold());
909    if command.action.print {
910        println!("  • Transaction(s) will be printed to the console.");
911    } else {
912        println!("  • Transaction(s) will NOT be printed to the console.");
913    }
914    if let Some(path) = &command.action.save {
915        println!("  • Transaction(s) will be saved to {}", path.bold());
916    } else {
917        println!("  • Transaction(s) will NOT be saved to a file.");
918    }
919    if command.action.broadcast {
920        println!("  • Transaction(s) will be broadcast to {}", setup.endpoint.bold());
921    } else {
922        println!("  • Transaction(s) will NOT be broadcast to the network.");
923    }
924
925    // ── Warnings ─────────────────────────────────────────────────────────
926    if !warnings.is_empty() {
927        println!("\n{}", "⚠️ Warnings:".bold().red());
928        for warning in warnings {
929            println!("  • {}", warning.dimmed());
930        }
931    }
932
933    println!("{}", "──────────────────────────────────────────────\n".dimmed());
934}
935
936// ── Unchanged helpers ───────────────────────────────────────────────────────
937
938/// Returns the value in effect at `consensus_version` from a `[(version, value)]` table ordered by
939/// ascending version: the last entry whose version does not exceed it.
940///
941/// Older networks (e.g. mainnet before V16) enforce smaller limits than the latest, so deploys must
942/// check against the version-specific value rather than `LATEST_*`.
943pub(crate) fn consensus_limit(
944    table: &[(ConsensusVersion, usize)],
945    consensus_version: ConsensusVersion,
946    latest: usize,
947) -> usize {
948    table
949        .iter()
950        .take_while(|(version, _)| *version <= consensus_version)
951        .last()
952        .map(|(_, size)| *size)
953        .unwrap_or(latest)
954}
955
956pub(crate) fn max_program_size_for_consensus_version<N: Network>(consensus_version: ConsensusVersion) -> usize {
957    consensus_limit(&N::MAX_PROGRAM_SIZE, consensus_version, N::LATEST_MAX_PROGRAM_SIZE())
958}
959
960pub(crate) fn max_transaction_size_for_consensus_version<N: Network>(consensus_version: ConsensusVersion) -> usize {
961    consensus_limit(&N::MAX_TRANSACTION_SIZE, consensus_version, N::LATEST_MAX_TRANSACTION_SIZE())
962}
963
964/// Warns when a built transaction exceeds the size limit for the target consensus version.
965pub(crate) fn warn_if_transaction_oversized<N: Network>(
966    id: &ProgramID<N>,
967    transaction: &Transaction<N>,
968    consensus_version: ConsensusVersion,
969    action: &str,
970) {
971    let Ok(size) = transaction.to_bytes_le().map(|bytes| bytes.len()) else { return };
972    let max_size = max_transaction_size_for_consensus_version::<N>(consensus_version);
973    if size > max_size {
974        println!(
975            "⚠️  The '{id}' transaction is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The {action} will likely fail.",
976            size as f64 / 1024.0,
977            max_size as f64 / 1024.0,
978            consensus_version as u8,
979        );
980    }
981}
982
983/// Check the tasks to warn the user about any potential issues.
984/// The following properties are checked:
985/// - If the transaction is to be broadcast:
986///     - The program does not exist on the network.
987///     - If the consensus version is less than V9, the program does not use V9 features.
988///     - If the consensus version is V9 or greater, the program contains a constructor.
989///     - The program size is within the limit for the target consensus version.
990fn check_tasks_for_warnings<N: Network>(
991    endpoint: &str,
992    network: NetworkName,
993    tasks: &[Task<N>],
994    consensus_version: ConsensusVersion,
995    command: &LeoDeploy,
996) -> Vec<String> {
997    let mut warnings = Vec::new();
998    for Task { id, is_local, program, bytecode_size, .. } in tasks {
999        if !is_local || !command.action.broadcast {
1000            continue;
1001        }
1002        // Check if the program exists on the network.
1003        if fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries).is_ok()
1004        {
1005            warnings
1006                .push(format!("The program '{id}' already exists on the network. Please use `leo upgrade` instead.",));
1007        }
1008        // Check if the program has a valid naming scheme.
1009        if consensus_version >= ConsensusVersion::V7
1010            && let Err(e) = program.check_program_naming_structure()
1011        {
1012            warnings.push(format!(
1013                "The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
1014            ));
1015        }
1016
1017        // Check if the program contains restricted keywords.
1018        if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
1019            warnings.push(format!(
1020                "The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
1021                consensus_version as u8
1022            ));
1023        }
1024        // Check if the program uses V9 features.
1025        if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
1026            warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The deployment will likely fail"));
1027        }
1028        // Check if the program uses V15 features (e.g., `view` blocks).
1029        if consensus_version < ConsensusVersion::V15 && program.contains_v15_syntax() {
1030            warnings.push(format!("The program '{id}' uses V15 features (e.g., `view fn`) but the consensus version is less than V15. The deployment will likely fail"));
1031        }
1032        // Check if the program uses V16 features (e.g., `std::prog::function_checksum`).
1033        if consensus_version < ConsensusVersion::V16 && program.contains_v16_syntax() {
1034            warnings.push(format!("The program '{id}' uses V16 features (e.g., `std::prog::function_checksum`) but the consensus version is less than V16. The deployment will likely fail"));
1035        }
1036        // Check if the program contains a constructor.
1037        if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
1038            warnings
1039                .push(format!("The program '{id}' does not contain a constructor. The deployment will likely fail",));
1040        }
1041        // The limit grows at later versions, so a program within the latest limit can still exceed
1042        // an older network's.
1043        let max_size = max_program_size_for_consensus_version::<N>(consensus_version);
1044        if *bytecode_size > max_size {
1045            warnings.push(format!(
1046                "The program '{id}' is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The deployment will likely fail.",
1047                *bytecode_size as f64 / 1024.0,
1048                max_size as f64 / 1024.0,
1049                consensus_version as u8,
1050            ));
1051        } else if let (_, _, Some(msg)) = format_program_size(*bytecode_size, max_size) {
1052            warnings.push(format!("The program '{id}' is {msg}."));
1053        }
1054    }
1055    // Check for a consensus version mismatch.
1056    if let Err(e) =
1057        check_consensus_version_mismatch(consensus_version, endpoint, network, command.env_override.network_retries)
1058    {
1059        warnings.push(format!("{e}. In some cases, the deployment may fail"));
1060    }
1061    warnings
1062}
1063
1064/// Check if the number of variables and constraints are within the limits.
1065pub(crate) fn validate_deployment_limits<N: Network>(
1066    deployment: &Deployment<N>,
1067    program_id: &ProgramID<N>,
1068    network: &NetworkName,
1069    consensus_version: ConsensusVersion,
1070) -> Result<()> {
1071    let (max_variables, max_constraints) = if consensus_version >= ConsensusVersion::V19 {
1072        (Some(N::MAX_DEPLOYMENT_VARIABLES_V2), Some(N::MAX_DEPLOYMENT_CONSTRAINTS_V2))
1073    } else if consensus_version >= ConsensusVersion::V18 {
1074        (None, None)
1075    } else {
1076        (Some(N::MAX_DEPLOYMENT_VARIABLES), Some(N::MAX_DEPLOYMENT_CONSTRAINTS))
1077    };
1078
1079    // Check if the number of variables is within the limits.
1080    let combined_variables = deployment.num_combined_variables()?;
1081    if let Some(max_variables) = max_variables
1082        && combined_variables > max_variables
1083    {
1084        return Err(
1085            crate::errors::variable_limit_exceeded(program_id, combined_variables, max_variables, network).into()
1086        );
1087    }
1088
1089    // Check if the number of constraints is within the limits.
1090    let constraints = deployment.num_combined_constraints()?;
1091    if let Some(max_constraints) = max_constraints
1092        && constraints > max_constraints
1093    {
1094        return Err(crate::errors::constraint_limit_exceeded(program_id, constraints, max_constraints, network).into());
1095    }
1096
1097    Ok(())
1098}
1099
1100/// Pretty‑print the deployment plan without using a table.
1101#[allow(clippy::too_many_arguments)]
1102pub(crate) fn print_deployment_plan<N: Network>(
1103    private_key: &PrivateKey<N>,
1104    address: &Address<N>,
1105    endpoint: &str,
1106    network: &NetworkName,
1107    local: &[Task<N>],
1108    skipped: &HashSet<ProgramID<N>>,
1109    remote: &[Task<N>],
1110    warnings: &[String],
1111    consensus_version: ConsensusVersion,
1112    command: &LeoDeploy,
1113) {
1114    use colored::*;
1115
1116    println!("\n{}", "🛠️  Deployment Plan Summary".bold());
1117    println!("{}", "──────────────────────────────────────────────".dimmed());
1118
1119    // ── Configuration ────────────────────────────────────────────────────
1120    println!("{}", "🔧 Configuration:".bold());
1121    println!("  {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
1122    println!("  {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
1123    println!("  {:20}{}", "Endpoint:".cyan(), endpoint.yellow());
1124    println!("  {:20}{}", "Network:".cyan(), network.to_string().yellow());
1125    println!("  {:20}{}", "Consensus Version:".cyan(), (consensus_version as u8).to_string().yellow());
1126
1127    // ── Deployment tasks (bullet list) ───────────────────────────────────
1128    println!("\n{}", "📦 Deployment Tasks:".bold());
1129    if local.is_empty() {
1130        println!("  (none)");
1131    } else {
1132        for Task { id, priority_fee, record, .. } in local.iter().filter(|task| !skipped.contains(&task.id)) {
1133            let priority_fee_str = priority_fee.map_or("0".into(), |v| v.to_string());
1134            let record_str = if record.is_some() { "yes" } else { "no (public fee)" };
1135            println!(
1136                "  • {}  │ priority fee: {}  │ fee record: {}",
1137                id.to_string().cyan(),
1138                priority_fee_str,
1139                record_str
1140            );
1141        }
1142    }
1143
1144    // ── Skipped programs ─────────────────────────────────────────────────
1145    if !skipped.is_empty() {
1146        println!("\n{}", "🚫 Skipped Programs:".bold().red());
1147        for symbol in skipped {
1148            println!("  • {}", symbol.to_string().dimmed());
1149        }
1150    }
1151
1152    // ── Remote dependencies ──────────────────────────────────────────────
1153    if !remote.is_empty() {
1154        println!("\n{}", "🌐 Remote Dependencies:".bold().red());
1155        println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
1156        for Task { id, .. } in remote {
1157            println!("  • {}", id.to_string().dimmed());
1158        }
1159    }
1160
1161    // ── Actions ──────────────────────────────────────────────────────────
1162    println!("\n{}", "⚙️ Actions:".bold());
1163    if command.action.print {
1164        println!("  • Transaction(s) will be printed to the console.");
1165    } else {
1166        println!("  • Transaction(s) will NOT be printed to the console.");
1167    }
1168    if let Some(path) = &command.action.save {
1169        println!("  • Transaction(s) will be saved to {}", path.bold());
1170    } else {
1171        println!("  • Transaction(s) will NOT be saved to a file.");
1172    }
1173    if command.action.broadcast {
1174        println!("  • Transaction(s) will be broadcast to {}", endpoint.bold());
1175    } else {
1176        println!("  • Transaction(s) will NOT be broadcast to the network.");
1177    }
1178
1179    // ── Warnings ─────────────────────────────────────────────────────────
1180    if !warnings.is_empty() {
1181        println!("\n{}", "⚠️ Warnings:".bold().red());
1182        for warning in warnings {
1183            println!("  • {}", warning.dimmed());
1184        }
1185    }
1186
1187    println!("{}", "──────────────────────────────────────────────\n".dimmed());
1188}
1189
1190/// Compute deployment statistics, including per-function cost estimation.
1191pub(crate) fn compute_deployment_stats<N: Network, R: Rng + CryptoRng>(
1192    vm: &VM<N, ConsensusMemory<N>>,
1193    deployment: &Deployment<N>,
1194    priority_fee: Option<u64>,
1195    consensus_version: ConsensusVersion,
1196    bytecode_size: usize,
1197    rng: &mut R,
1198) -> Result<DeploymentStats> {
1199    let variables = deployment.num_combined_variables()?;
1200    let constraints = deployment.num_combined_constraints()?;
1201    let (_, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
1202        deployment_cost(vm.process(), deployment, consensus_version)?;
1203    let priority = priority_fee.unwrap_or(0);
1204
1205    let function_costs = calculate_function_costs(vm, deployment, consensus_version, rng)?;
1206
1207    let (max_variables, max_constraints) = if consensus_version >= ConsensusVersion::V19 {
1208        (Some(N::MAX_DEPLOYMENT_VARIABLES_V2), Some(N::MAX_DEPLOYMENT_CONSTRAINTS_V2))
1209    } else if consensus_version >= ConsensusVersion::V18 {
1210        (None, None)
1211    } else {
1212        (Some(N::MAX_DEPLOYMENT_VARIABLES), Some(N::MAX_DEPLOYMENT_CONSTRAINTS))
1213    };
1214
1215    Ok(DeploymentStats {
1216        program_size_bytes: bytecode_size,
1217        max_program_size_bytes: max_program_size_for_consensus_version::<N>(consensus_version),
1218        total_variables: Some(variables),
1219        total_constraints: Some(constraints),
1220        max_variables,
1221        max_constraints,
1222        storage_cost,
1223        synthesis_cost,
1224        namespace_cost,
1225        constructor_cost,
1226        priority_fee: priority,
1227        total_cost: storage_cost + synthesis_cost + namespace_cost + constructor_cost + priority,
1228        function_costs,
1229    })
1230}
1231
1232/// Create a deployment transaction using placeholder verifying keys and certificates.
1233///
1234/// Used when `--skip-deploy-certificate` is passed. The caller is responsible for printing
1235/// the appropriate warning message and computing the edition to use.
1236#[allow(clippy::too_many_arguments)]
1237pub(crate) fn deploy_with_placeholder_certificate<N: Network, A: Aleo<Network = N>, R: Rng + CryptoRng>(
1238    vm: &VM<N, ConsensusMemory<N>>,
1239    private_key: &PrivateKey<N>,
1240    program: &Program<N>,
1241    edition: u16,
1242    record: Option<Record<N, Plaintext<N>>>,
1243    priority_fee: Option<u64>,
1244    bytecode_size: usize,
1245    consensus_version: ConsensusVersion,
1246    query: &SnarkVMQuery<N, BlockMemory<N>>,
1247    network_retries: u32,
1248    rng: &mut R,
1249) -> Result<(Transaction<N>, DeploymentStats)> {
1250    assert!(!program.functions().is_empty(), "Program `{}` has no functions", program.id());
1251    // Initialize a vector for the placeholder verifying keys and certificates.
1252    let mut verifying_keys = Vec::with_capacity(program.functions().len() + program.records().len());
1253    for function_name in program.functions().keys() {
1254        let verifying_key = VerifyingKey::from_str(leo_compiler::run::PLACEHOLDER_VK)?;
1255        let certificate = Certificate::from_str(leo_compiler::run::PLACEHOLDER_CERT)?;
1256        verifying_keys.push((*function_name, (verifying_key, certificate)));
1257    }
1258    for record_name in program.records().keys() {
1259        let verifying_key = VerifyingKey::from_str(leo_compiler::run::PLACEHOLDER_VK)?;
1260        let certificate = Certificate::from_str(leo_compiler::run::PLACEHOLDER_CERT)?;
1261        verifying_keys.push((*record_name, (verifying_key, certificate)));
1262    }
1263    // Create the deployment.
1264    let mut deployment = Deployment::new(edition, program.clone(), verifying_keys, None, None).unwrap();
1265
1266    // Set the program owner.
1267    deployment.set_program_owner_raw(Some(Address::try_from(private_key)?));
1268
1269    // Compute the checksum of the deployment.
1270    deployment.set_program_checksum_raw(Some(deployment.program().to_checksum()));
1271
1272    // Compute the deployment ID.
1273    let deployment_id = deployment.to_deployment_id()?;
1274
1275    // Construct the owner.
1276    let owner = ProgramOwner::new(private_key, deployment_id, rng)?;
1277
1278    // Construct the fee authorization and capture cost breakdown.
1279    let (minimum_deployment_cost, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
1280        deployment_cost(vm.process(), &deployment, consensus_version)?;
1281    // Authorize the fee.
1282    let fee_authorization = match record {
1283        Some(record) => vm
1284            .process()
1285            .authorize_fee_private::<A, _>(
1286                private_key,
1287                record,
1288                minimum_deployment_cost,
1289                priority_fee.unwrap_or(0),
1290                deployment_id,
1291                rng,
1292            )
1293            .map_err(|e| anyhow::anyhow!("{e}"))?,
1294        None => vm
1295            .process()
1296            .authorize_fee_public::<A, _>(
1297                private_key,
1298                minimum_deployment_cost,
1299                priority_fee.unwrap_or(0),
1300                deployment_id,
1301                rng,
1302            )
1303            .map_err(|e| anyhow::anyhow!("{e}"))?,
1304    };
1305
1306    // Get the state root, retrying on transient network failures.
1307    let state_root = retry_network_call(network_retries, || query.current_state_root())?;
1308
1309    // Create a fee transition without a proof.
1310    let fee = Fee::from(fee_authorization.transitions().into_iter().next().unwrap().1, state_root, None)?;
1311
1312    // Add the program to the VM before calculating function costs.
1313    vm.process().lock().add_program(program)?;
1314    // Compute the deployment stats (circuit fields are None since VKs are placeholders).
1315    let priority = priority_fee.unwrap_or(0);
1316    let function_costs = calculate_function_costs(vm, &deployment, consensus_version, rng)?;
1317    let stats = DeploymentStats {
1318        program_size_bytes: bytecode_size,
1319        max_program_size_bytes: max_program_size_for_consensus_version::<N>(consensus_version),
1320        total_variables: None,
1321        total_constraints: None,
1322        max_variables: None,
1323        max_constraints: None,
1324        storage_cost,
1325        synthesis_cost,
1326        namespace_cost,
1327        constructor_cost,
1328        priority_fee: priority,
1329        total_cost: storage_cost + synthesis_cost + namespace_cost + constructor_cost + priority,
1330        function_costs,
1331    };
1332    // Create the transaction.
1333    let transaction = Transaction::from_deployment(owner, deployment, fee)?;
1334    Ok((transaction, stats))
1335}
1336
1337/// Pretty-print deployment summary, including per-function costs.
1338pub(crate) fn print_deployment_summary(program_id: &str, stats: &DeploymentStats) {
1339    use colored::*;
1340
1341    println!("\n{} {}", "📊 Deployment Summary for".bold(), program_id.bold());
1342    println!("{}", "──────────────────────────────────────────────".dimmed());
1343    print!("{stats}");
1344    println!("{}", "──────────────────────────────────────────────".dimmed());
1345}
1346
1347/// Calculate per-function costs for a deployment.
1348pub(crate) fn calculate_function_costs<N: Network, R: Rng + CryptoRng>(
1349    vm: &VM<N, ConsensusMemory<N>>,
1350    deployment: &Deployment<N>,
1351    consensus_version: ConsensusVersion,
1352    rng: &mut R,
1353) -> Result<Vec<FunctionCostStats>> {
1354    // Get the stack for the program.
1355    let stack = vm.process().get_stack(deployment.program().id())?;
1356
1357    let mut function_costs = Vec::new();
1358
1359    // Generate a single throwaway key for input sampling across all functions.
1360    let sample_key = PrivateKey::new(rng)?;
1361    let sample_address = Address::try_from(&sample_key)?;
1362
1363    for (function_name, _) in deployment.function_verifying_keys() {
1364        let name = function_name.to_string();
1365
1366        // Sample inputs and attempt authorization to estimate execution cost (best-effort).
1367        // When authorization succeeds, use the breakdown directly from snarkVM.
1368        // When it fails, fall back to the static finalize cost.
1369        let input_types = deployment.program().get_function(function_name)?.input_types();
1370        let inputs = input_types
1371            .into_iter()
1372            .map(|ty| {
1373                stack
1374                    .sample_value(&sample_address, &ty.into(), rng)
1375                    .map_err(|e| crate::errors::custom(format!("Failed to sample value: {e}")).into())
1376            })
1377            .collect::<Result<Vec<_>>>()?;
1378        let (finalize_cost, storage_cost, execution_cost) =
1379            match vm.authorize(&sample_key, deployment.program().id(), function_name, inputs.iter(), rng) {
1380                Err(e) => {
1381                    tracing::debug!("Could not estimate execution cost for '{name}': {e}");
1382                    // Fall back to static finalize cost analysis.
1383                    let static_finalize_cost = if consensus_version >= ConsensusVersion::V10 {
1384                        minimum_cost_in_microcredits_v3(&stack, function_name)?
1385                    } else if consensus_version >= ConsensusVersion::V2 {
1386                        minimum_cost_in_microcredits_v2(&stack, function_name)?
1387                    } else {
1388                        minimum_cost_in_microcredits_v1(&stack, function_name)?
1389                    };
1390                    (static_finalize_cost, None, None)
1391                }
1392                Ok(authorization) => {
1393                    let (total, (storage, finalize)) =
1394                        execution_cost_for_authorization(vm.process(), &authorization, consensus_version)?;
1395                    (finalize, Some(storage), Some(total))
1396                }
1397            };
1398
1399        // Check if this function (or any function it calls) uses dynamic dispatch.
1400        // Dynamic calls make costs a lower bound since the target is resolved at runtime.
1401        let has_dynamic_calls = stack.contains_dynamic_call(function_name).unwrap_or(false);
1402
1403        function_costs.push(FunctionCostStats { name, finalize_cost, storage_cost, execution_cost, has_dynamic_calls });
1404    }
1405
1406    Ok(function_costs)
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411    use super::*;
1412    use std::str::FromStr;
1413
1414    fn task_from_source(source: &str) -> Task<TestnetV0> {
1415        let program = Program::<TestnetV0>::from_str(source).unwrap();
1416        Task {
1417            id: *program.id(),
1418            program,
1419            edition: None,
1420            is_local: true,
1421            priority_fee: None,
1422            record: None,
1423            bytecode_size: source.len(),
1424        }
1425    }
1426
1427    #[test]
1428    fn rejects_unskipped_local_deploy_without_constructor() {
1429        let task = task_from_source(concat!(
1430            "program missing_constructor.aleo;\n",
1431            "function main:\n",
1432            "    input r0 as u32.public;\n",
1433            "    output r0 as u32.public;\n",
1434        ));
1435
1436        let result = enforce_local_deploy_constructor_requirements(&[task], &HashSet::new(), ConsensusVersion::V9);
1437
1438        assert!(result.is_err());
1439    }
1440
1441    #[test]
1442    fn allows_skipped_local_deploy_without_constructor() {
1443        let task = task_from_source(concat!(
1444            "program skipped_constructor.aleo;\n",
1445            "function main:\n",
1446            "    input r0 as u32.public;\n",
1447            "    output r0 as u32.public;\n",
1448        ));
1449        let skipped = HashSet::from([task.id]);
1450
1451        enforce_local_deploy_constructor_requirements(&[task], &skipped, ConsensusVersion::V9).unwrap();
1452    }
1453
1454    #[test]
1455    fn allows_local_deploy_without_constructor_before_v9() {
1456        let task = task_from_source(concat!(
1457            "program pre_constructor_gate.aleo;\n",
1458            "function main:\n",
1459            "    input r0 as u32.public;\n",
1460            "    output r0 as u32.public;\n",
1461        ));
1462
1463        enforce_local_deploy_constructor_requirements(&[task], &HashSet::new(), ConsensusVersion::V8).unwrap();
1464    }
1465
1466    #[test]
1467    fn plaintext_size_limit_tracks_deployment_consensus_version() -> Result<()> {
1468        let command = LeoDeploy::try_parse_from(["deploy", "--yes", "--skip-deploy-certificate"])
1469            .expect("Valid deployment options");
1470        for (consensus_version, array_length, skip, rejected) in [
1471            (ConsensusVersion::V19, 512, false, false),
1472            (ConsensusVersion::V20, 512, false, true),
1473            (ConsensusVersion::V21, 512, false, true),
1474            (ConsensusVersion::V20, 511, false, false),
1475            (ConsensusVersion::V20, 512, true, false),
1476        ] {
1477            // Length 512 exceeds the raw data limit by four bits; length 511 is within it.
1478            let task = task_from_source(&format!(
1479                "program plaintext_limit.aleo;\n\
1480                 mapping values:\n\
1481                     key as u8.public;\n\
1482                     value as [[boolean; 2048u32]; {array_length}u32].public;\n\
1483                 function main:\n\
1484                     input r0 as u8.public;\n\
1485                     output r0 as u8.public;\n\
1486                 constructor:\n\
1487                     assert.eq true true;\n",
1488            ));
1489            let skipped = if skip { HashSet::from([task.id]) } else { HashSet::new() };
1490            let private_key = PrivateKey::<TestnetV0>::new(&mut rand::rng())?;
1491            let vm = VM::from(ConsensusStore::<TestnetV0, ConsensusMemory<TestnetV0>>::open(StorageMode::Production)?)?;
1492            let setup = DeploySetup {
1493                address: Address::try_from(&private_key)?,
1494                private_key,
1495                endpoint: String::new(),
1496                consensus_version,
1497                network: NetworkName::TestnetV0,
1498                query: SnarkVMQuery::from(vm.block_store()),
1499                vm,
1500            };
1501            let result = generate_deploy_transactions::<TestnetV0, AleoTestnetV0>(
1502                &command,
1503                &setup,
1504                vec![task],
1505                &skipped,
1506                &mut HashSet::new(),
1507            );
1508
1509            if rejected {
1510                let error = result.err().expect("V20 and later must reject oversized plaintext declarations");
1511                assert!(error.to_string().contains("exceeds the maximum allowed size in bits"), "{error}");
1512            } else {
1513                let (transactions, _) = result?.expect("The deployment plan is confirmed by --yes");
1514                assert_eq!(transactions.len(), usize::from(!skip));
1515            }
1516        }
1517        Ok(())
1518    }
1519
1520    #[test]
1521    fn program_size_limit_tracks_consensus_version() {
1522        // A pre-V16 network must report the smaller limit, not the latest (2048 kB) one.
1523        assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V1), 100_000);
1524        assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V13), 100_000);
1525        assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V14), 512_000);
1526        assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V15), 512_000);
1527        assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V16), 2_048_000);
1528        // The latest version always matches snarkVM's own `LATEST_MAX_PROGRAM_SIZE`.
1529        assert_eq!(
1530            max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::latest()),
1531            TestnetV0::LATEST_MAX_PROGRAM_SIZE()
1532        );
1533    }
1534}