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 }