/ cli / src / commands / developer / deploy.rs
deploy.rs
  1  // Copyright (c) 2025-2026 ACDC Network
  2  // This file is part of the alphaos library.
  3  //
  4  // Alpha Chain | Delta Chain Protocol
  5  // International Monetary Graphite.
  6  //
  7  // Derived from Aleo (https://aleo.org) and ProvableHQ (https://provable.com).
  8  // They built world-class ZK infrastructure. We installed the EASY button.
  9  // Their cryptography: elegant. Our modifications: bureaucracy-compatible.
 10  // Original brilliance: theirs. Robert's Rules: ours. Bugs: definitely ours.
 11  //
 12  // Original Aleo/ProvableHQ code subject to Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0
 13  // All modifications and new work: CC0 1.0 Universal Public Domain Dedication.
 14  // No rights reserved. No permission required. No warranty. No refunds.
 15  //
 16  // https://creativecommons.org/publicdomain/zero/1.0/
 17  // SPDX-License-Identifier: CC0-1.0
 18  
 19  use super::{Developer, DEFAULT_ENDPOINT};
 20  use crate::{
 21      commands::StoreFormat,
 22      helpers::args::{parse_private_key, prepare_endpoint},
 23  };
 24  
 25  use alphavm::{
 26      circuit::{Alpha, AlphaCanaryV0, AlphaTestnetV0, AlphaV0},
 27      console::{
 28          network::{CanaryV0, MainnetV0, Network, TestnetV0},
 29          program::ProgramOwner,
 30      },
 31      ledger::store::helpers::memory::BlockMemory,
 32      prelude::{
 33          block::Transaction,
 34          deployment_cost,
 35          query::{Query, QueryTrait},
 36          store::{helpers::memory::ConsensusMemory, ConsensusStore},
 37          ProgramID,
 38          VM,
 39      },
 40  };
 41  
 42  use alphastd::StorageMode;
 43  use alphavm::prelude::{Address, ConsensusVersion};
 44  use anyhow::Result;
 45  use clap::{builder::NonEmptyStringValueParser, Parser};
 46  use colored::Colorize;
 47  use std::str::FromStr;
 48  use ureq::http::Uri;
 49  use zeroize::Zeroize;
 50  
 51  use anyhow::Context;
 52  
 53  /// Deploys an Alpha program.
 54  #[derive(Debug, Parser)]
 55  #[command(
 56      group(clap::ArgGroup::new("mode").required(true).multiple(false)),
 57      group(clap::ArgGroup::new("key").required(true).multiple(false))
 58  )]
 59  pub struct Deploy {
 60      /// The name of the program to deploy.
 61      program_id: String,
 62      /// A path to a directory containing a manifest file. Defaults to the current working directory.
 63      #[clap(long)]
 64      path: Option<String>,
 65      /// The private key used to generate the deployment.
 66      #[clap(short = 'p', long, group = "key", value_parser=NonEmptyStringValueParser::default())]
 67      private_key: Option<String>,
 68      /// Use a developer validator key tok generate the deployment.
 69      #[clap(long, group = "key")]
 70      dev_key: Option<u16>,
 71      /// Specify the path to a file containing the account private key of the node
 72      #[clap(long, group = "key", value_parser=NonEmptyStringValueParser::default())]
 73      private_key_file: Option<String>,
 74      /// The endpoint to query node state from and broadcast to (if set to broadcast).
 75      ///
 76      /// The given value is expected to be the base URL, e.g., "https://mynode.com", and will be extended automatically
 77      /// to fit the network type and query.
 78      /// For example, the base URL may extend to "http://mynode.com/testnet/transaction/unconfirmed/ID" to retrieve
 79      /// an unconfirmed transaction on the test network.
 80      #[clap(short, long, alias="query", default_value=DEFAULT_ENDPOINT, verbatim_doc_comment)]
 81      endpoint: Uri,
 82      /// The priority fee in microcredits.
 83      #[clap(long, default_value_t = 0)]
 84      priority_fee: u64,
 85      /// The record to spend the fee from.
 86      #[clap(short, long)]
 87      record: Option<String>,
 88      /// Set the URL used to broadcast the transaction (if no value is given, the query endpoint is used).
 89      ///
 90      /// The given value is expected the full URL of the endpoint, not just the base URL, e.g., "http://mynode.com/testnet/transaction/broadcast".
 91      #[clap(short, long, group = "mode", verbatim_doc_comment)]
 92      broadcast: Option<Option<Uri>>,
 93      /// Performs a dry-run of transaction generation.
 94      #[clap(short, long, group = "mode")]
 95      dry_run: bool,
 96      /// Store generated deployment transaction to a local file.
 97      #[clap(long, group = "mode")]
 98      store: Option<String>,
 99      /// If --store is specified, the format in which the transaction should be stored : string or
100      /// bytes, by default : bytes.
101      #[clap(long, value_enum, default_value_t = StoreFormat::Bytes, requires="store")]
102      store_format: StoreFormat,
103      /// Wait for the transaction to be accepted by the network. Requires --broadcast.
104      #[clap(long, requires = "broadcast")]
105      wait: bool,
106      /// Timeout in seconds when waiting for transaction confirmation. Default is 60 seconds.
107      #[clap(long, default_value_t = 60, requires = "wait")]
108      timeout: u64,
109  }
110  
111  impl Drop for Deploy {
112      /// Zeroize the private key when the `Deploy` struct goes out of scope.
113      fn drop(&mut self) {
114          self.private_key.zeroize();
115      }
116  }
117  
118  impl Deploy {
119      /// Deploys an Alpha program.
120      pub fn parse<N: Network>(self) -> Result<String> {
121          // Construct the deployment for the specified network.
122          match N::ID {
123              MainnetV0::ID => self.construct_deployment::<MainnetV0, AlphaV0>(),
124              TestnetV0::ID => self.construct_deployment::<TestnetV0, AlphaTestnetV0>(),
125              CanaryV0::ID => self.construct_deployment::<CanaryV0, AlphaCanaryV0>(),
126              _ => unreachable!(),
127          }
128          .with_context(|| "Deployment failed")
129      }
130  
131      /// Construct and process the deployment transaction.
132      fn construct_deployment<N: Network, A: Alpha<Network = N, BaseField = N::Field>>(self) -> Result<String> {
133          let endpoint = prepare_endpoint(self.endpoint.clone())?;
134  
135          // Specify the query
136          let query = Query::<N, BlockMemory<N>>::from(endpoint.clone());
137  
138          // Retrieve the private key.
139          let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?;
140  
141          // Retrieve the program ID.
142          let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?;
143  
144          // Fetch the package from the directory.
145          let package =
146              Developer::parse_package(program_id, &self.path).with_context(|| "Failed to parse program package")?;
147  
148          println!("📦 Creating deployment transaction for '{}'...\n", &program_id.to_string().bold());
149  
150          // Generate the process with the appropriate imports.
151          let process = package.get_process()?;
152  
153          // Generate the deployment
154          let mut deployment =
155              package.deploy::<A>(&process, None).with_context(|| "Failed to generate the deployment")?;
156  
157          // Get the consensus version.
158          let consensus_version =
159              N::CONSENSUS_VERSION(query.current_block_height().with_context(|| "Failed to query consensus height")?)?;
160  
161          // If the consensus version is less than `V9`, unset the program checksum and owner in the deployment.
162          // Otherwise, set it to the appropriate values.
163          if consensus_version < ConsensusVersion::V9 {
164              deployment.set_program_checksum_raw(None);
165              deployment.set_program_owner_raw(None);
166          } else {
167              deployment.set_program_checksum_raw(Some(package.program().to_checksum()));
168              deployment.set_program_owner_raw(Some(Address::try_from(&private_key)?));
169          };
170  
171          // Compute the deployment ID.
172          let deployment_id = deployment.to_deployment_id().with_context(|| "Failed to compute deployment ID")?;
173  
174          // Compute the minimum deployment cost.
175          let (minimum_deployment_cost, (_, _, _, _)) = deployment_cost(&process, &deployment, consensus_version)
176              .with_context(|| "Failed to compute the minimum deployment cost")?;
177  
178          // Generate the deployment transaction.
179          let transaction = {
180              // Initialize an RNG.
181              let rng = &mut rand::thread_rng();
182  
183              // Initialize the storage.
184              let store = ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)
185                  .with_context(|| "Failed to open the consensus store")?;
186  
187              // Initialize the VM.
188              let vm = VM::from(store).with_context(|| "Failed to initialize the virtual machine")?;
189  
190              // Prepare the fees.
191              let fee = match &self.record {
192                  Some(record) => {
193                      let fee_record =
194                          Developer::parse_record(&private_key, record).with_context(|| "Failed to parse record")?;
195                      let fee_authorization = vm.authorize_fee_private(
196                          &private_key,
197                          fee_record,
198                          minimum_deployment_cost,
199                          self.priority_fee,
200                          deployment_id,
201                          rng,
202                      )?;
203                      vm.execute_fee_authorization(fee_authorization, Some(&query), rng)
204                          .with_context(|| "Failed to execute fee authorization")?
205                  }
206                  None => {
207                      let fee_authorization = vm.authorize_fee_public(
208                          &private_key,
209                          minimum_deployment_cost,
210                          self.priority_fee,
211                          deployment_id,
212                          rng,
213                      )?;
214                      vm.execute_fee_authorization(fee_authorization, Some(&query), rng)
215                          .with_context(|| "Failed to execute fee authorization")?
216                  }
217              };
218              // Construct the owner.
219              let owner = ProgramOwner::new(&private_key, deployment_id, rng)
220                  .with_context(|| "Failed to construct program owner")?;
221  
222              // Create a new transaction.
223              Transaction::from_deployment(owner, deployment, fee).with_context(|| "Failed to crate transaction")?
224          };
225          println!("✅ Created deployment transaction for '{}'", program_id.to_string().bold());
226  
227          // Determine if the transaction should be broadcast, stored, or displayed to the user.
228          Developer::handle_transaction(
229              &endpoint,
230              &self.broadcast,
231              self.dry_run,
232              &self.store,
233              self.store_format,
234              self.wait,
235              self.timeout,
236              transaction,
237              program_id.to_string(),
238          )
239      }
240  }
241  
242  #[cfg(test)]
243  mod tests {
244      use super::*;
245      use crate::commands::{Command, DeveloperCommand, CLI};
246  
247      use anyhow::bail;
248  
249      #[test]
250      fn clap_alphaos_deploy_missing_mode() {
251          let arg_vec = &[
252              "alphaos",
253              "developer",
254              "deploy",
255              "--private-key=PRIVATE_KEY",
256              "--endpoint=ENDPOINT",
257              "--priority-fee=77",
258              "--record=RECORD",
259              "hello.alpha",
260          ];
261  
262          // Should fail because no mode is specified.
263          let err = CLI::try_parse_from(arg_vec).unwrap_err();
264          assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
265      }
266  
267      #[test]
268      fn clap_alphaos_deploy() -> Result<()> {
269          let arg_vec = &[
270              "alphaos",
271              "developer",
272              "deploy",
273              "--private-key=PRIVATE_KEY",
274              "--endpoint=ENDPOINT",
275              "--priority-fee=77",
276              "--dry-run",
277              "--record=RECORD",
278              "hello.alpha",
279          ];
280          // Use try parse here, as parse calls `exit()`.
281          let cli = CLI::try_parse_from(arg_vec)?;
282  
283          let Command::Developer(developer) = cli.command else {
284              bail!("Unexpected result of clap parsing!");
285          };
286          let DeveloperCommand::Deploy(deploy) = developer.command else {
287              bail!("Unexpected result of clap parsing!");
288          };
289  
290          assert_eq!(developer.network, 0);
291          assert_eq!(deploy.program_id, "hello.alpha");
292          assert_eq!(deploy.private_key, Some("PRIVATE_KEY".to_string()));
293          assert_eq!(deploy.private_key_file, None);
294          assert_eq!(deploy.endpoint, "ENDPOINT");
295          assert!(deploy.dry_run);
296          assert!(deploy.broadcast.is_none());
297          assert_eq!(deploy.store, None);
298          assert_eq!(deploy.priority_fee, 77);
299          assert_eq!(deploy.record, Some("RECORD".to_string()));
300  
301          Ok(())
302      }
303  
304      #[test]
305      fn clap_alphaos_deploy_broadcast() -> Result<()> {
306          let arg_vec = &[
307              "alphaos",
308              "developer",
309              "deploy",
310              "--private-key=PRIVATE_KEY",
311              "--endpoint=ENDPOINT",
312              "--priority-fee=77",
313              "--broadcast=ENDPOINT2",
314              "--record=RECORD",
315              "hello.alpha",
316          ];
317          // Use try parse here, as parse calls `exit()`.
318          let cli = CLI::try_parse_from(arg_vec)?;
319  
320          let Command::Developer(developer) = cli.command else {
321              bail!("Unexpected result of clap parsing!");
322          };
323          let DeveloperCommand::Deploy(deploy) = developer.command else {
324              bail!("Unexpected result of clap parsing!");
325          };
326  
327          assert_eq!(developer.network, 0);
328          assert_eq!(deploy.program_id, "hello.alpha");
329          assert_eq!(deploy.private_key, Some("PRIVATE_KEY".to_string()));
330          assert_eq!(deploy.private_key_file, None);
331          assert_eq!(deploy.endpoint, "ENDPOINT");
332          assert!(!deploy.dry_run);
333          // Check that the custom endpoint for broadcasting is used.
334          assert_eq!(Some(Some(Uri::try_from("ENDPOINT2").unwrap())), deploy.broadcast);
335          assert_eq!(deploy.store, None);
336          assert_eq!(deploy.priority_fee, 77);
337          assert_eq!(deploy.record, Some("RECORD".to_string()));
338  
339          Ok(())
340      }
341  }