execute.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 console::network::Network, 27 ledger::{query::QueryTrait, store::helpers::memory::BlockMemory}, 28 prelude::{ 29 query::Query, 30 store::{helpers::memory::ConsensusMemory, ConsensusStore}, 31 Address, 32 Identifier, 33 Locator, 34 Process, 35 ProgramID, 36 Value, 37 VM, 38 }, 39 }; 40 41 use alphastd::StorageMode; 42 use anyhow::{anyhow, bail, Context, Result}; 43 use clap::{builder::NonEmptyStringValueParser, Parser}; 44 use colored::Colorize; 45 use std::str::FromStr; 46 use tracing::debug; 47 use ureq::http::Uri; 48 use zeroize::Zeroize; 49 50 /// Executes an Alpha program function. 51 #[derive(Debug, Parser)] 52 #[command( 53 group(clap::ArgGroup::new("mode").required(true).multiple(false)), 54 group(clap::ArgGroup::new("key").required(true).multiple(false)) 55 )] 56 pub struct Execute { 57 /// The program identifier. 58 #[clap(value_parser=NonEmptyStringValueParser::default())] 59 program_id: String, 60 /// The function name. 61 #[clap(value_parser=NonEmptyStringValueParser::default())] 62 function: String, 63 /// The function inputs. 64 inputs: Vec<String>, 65 /// The private key used to generate the execution. 66 #[clap(short = 'p', long, group = "key", value_parser=NonEmptyStringValueParser::default())] 67 private_key: Option<String>, 68 /// Specify the path to a file containing the account private key of the node 69 #[clap(long, group = "key", value_parser=NonEmptyStringValueParser::default())] 70 private_key_file: Option<String>, 71 /// Use a developer validator key to generate the execution 72 #[clap(long, group = "key")] 73 dev_key: Option<u16>, 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 /// 81 /// The given value may also be a JSON serialized `StaticQuery` struct. 82 #[clap(short, long, alias="query", default_value=DEFAULT_ENDPOINT, verbatim_doc_comment)] 83 endpoint: Uri, 84 /// The priority fee in microcredits. 85 #[clap(long, default_value_t = 0)] 86 priority_fee: u64, 87 /// The record to spend the fee from. 88 #[clap(short, long)] 89 record: Option<String>, 90 /// Set the URL used to broadcast the transaction (if no value is given, the query endpoint is used). 91 /// 92 /// The given value is expected the full URL of the endpoint, not just the base URL, e.g., "http://mynode.com/testnet/transaction/broadcast". 93 #[clap(short, long, group = "mode", verbatim_doc_comment)] 94 broadcast: Option<Option<Uri>>, 95 /// Performs a dry-run of transaction generation. 96 #[clap(short, long, group = "mode")] 97 dry_run: bool, 98 /// Store generated deployment transaction to a local file. 99 #[clap(long, group = "mode")] 100 store: Option<String>, 101 /// If --store is specified, the format in which the transaction should be stored : string or 102 /// bytes, by default : bytes. 103 #[clap(long, value_enum, default_value_t = StoreFormat::Bytes, requires="store")] 104 store_format: StoreFormat, 105 /// Wait for the transaction to be accepted by the network. Requires --broadcast. 106 #[clap(long, requires = "broadcast")] 107 wait: bool, 108 /// Timeout in seconds when waiting for transaction confirmation. Default is 60 seconds. 109 #[clap(long, default_value_t = 60, requires = "wait")] 110 timeout: u64, 111 /// Send the transaction without checking if sufficient funds are available (intended for testing purposes only). 112 #[clap(long, hide = true)] 113 skip_funds_check: bool, 114 } 115 116 impl Drop for Execute { 117 /// Zeroize the private key when the `Execute` struct goes out of scope. 118 fn drop(&mut self) { 119 if let Some(mut pk) = self.private_key.take() { 120 pk.zeroize() 121 } 122 } 123 } 124 125 impl Execute { 126 /// Executes an Alpha program function with the provided inputs. 127 pub fn parse<N: Network>(self) -> Result<String> { 128 let endpoint = prepare_endpoint(self.endpoint.clone())?; 129 130 // Specify the query 131 let query = Query::<N, BlockMemory<N>>::from(endpoint.clone()); 132 133 // Check if the query is a static query. 134 let is_static_query = matches!(query, Query::STATIC(_)); 135 136 // Retrieve the private key. 137 let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?; 138 139 // Retrieve the program ID. 140 let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?; 141 142 // Retrieve the function. 143 let function = Identifier::from_str(&self.function).with_context(|| "Failed to parse function ID")?; 144 145 // Retrieve the inputs. 146 let inputs = self.inputs.iter().map(|input| Value::from_str(input)).collect::<Result<Vec<Value<N>>>>()?; 147 148 let locator = Locator::<N>::from_str(&format!("{program_id}/{function}"))?; 149 println!("📦 Creating execution transaction for '{}'...\n", &locator.to_string().bold()); 150 151 // Generate the execution transaction. 152 let transaction = { 153 // Initialize an RNG. 154 let rng = &mut rand::thread_rng(); 155 156 // Initialize the storage. 157 let store = ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?; 158 159 // Initialize the VM. 160 let vm = VM::from(store)?; 161 162 if !is_static_query && program_id != ProgramID::from_str("credits.alpha")? { 163 let height = query.current_block_height().with_context(|| "Failed to retrieve current block height")?; 164 let version = N::CONSENSUS_VERSION(height)?; 165 debug!("At block height {height} and consensus {version:?}"); 166 167 // Load the program and it's imports into the process. 168 load_program(&query, &mut vm.process().write(), &program_id, &endpoint)?; 169 } 170 171 // Prepare the fee. 172 let fee_record = match &self.record { 173 Some(record_string) => Some( 174 Developer::parse_record(&private_key, record_string).with_context(|| "Failed to parse record")?, 175 ), 176 None => None, 177 }; 178 179 // Create a new transaction. 180 vm.execute( 181 &private_key, 182 (program_id, function), 183 inputs.iter(), 184 fee_record, 185 self.priority_fee, 186 Some(&query), 187 rng, 188 ) 189 .with_context(|| "VM failed to execute transaction locally")? 190 }; 191 192 // Check if the public balance is sufficient. 193 if self.record.is_none() && !is_static_query && !self.skip_funds_check { 194 // Fetch the public balance. 195 let address = Address::try_from(&private_key)?; 196 let public_balance = Developer::get_public_balance::<N>(&endpoint, &address) 197 .with_context(|| "Failed to check for sufficient funds to send transaction")? 198 .ok_or_else(|| { 199 anyhow!( 200 "No public balance found for sending account `{}`. It may not exist.", 201 address.to_string().bold() 202 ) 203 })?; 204 205 // Check if the public balance is sufficient. 206 let storage_cost = transaction 207 .execution() 208 .with_context(|| "Failed to get execution cost of transaction")? 209 .size_in_bytes()?; 210 211 // Calculate the base fee. 212 // This fee is the minimum fee required to pay for the transaction, 213 // excluding any finalize fees that the execution may incur. 214 let base_fee = storage_cost.saturating_add(self.priority_fee); 215 216 // If the public balance is insufficient, return an error. 217 if public_balance < base_fee { 218 bail!( 219 "The public balance of {} is insufficient to pay the base fee for `{}`", 220 public_balance, 221 locator.to_string().bold() 222 ); 223 } 224 } 225 226 println!("✅ Created execution transaction for '{}'", locator.to_string().bold()); 227 228 // Determine if the transaction should be broadcast, stored, or displayed to the user. 229 Developer::handle_transaction( 230 &endpoint, 231 &self.broadcast, 232 self.dry_run, 233 &self.store, 234 self.store_format, 235 self.wait, 236 self.timeout, 237 transaction, 238 locator.to_string(), 239 ) 240 } 241 } 242 243 /// A helper function to recursively load the program and all of its imports into the process. 244 fn load_program<N: Network>( 245 query: &Query<N, BlockMemory<N>>, 246 process: &mut Process<N>, 247 program_id: &ProgramID<N>, 248 endpoint: &Uri, 249 ) -> Result<()> { 250 // Fetch the program. 251 let program = query.get_program(program_id).with_context(|| "Failed to fetch program")?; 252 // Fetch the latest edition of the program. 253 let edition = Developer::get_latest_edition(endpoint, program_id) 254 .with_context(|| format!("Failed to get latest edition for program {program_id}"))?; 255 256 // Return early if the program is already loaded. 257 if process.contains_program(program.id()) { 258 return Ok(()); 259 } 260 261 // Iterate through the program imports. 262 for import_program_id in program.imports().keys() { 263 // Add the imports to the process if does not exist yet. 264 if !process.contains_program(import_program_id) { 265 // Recursively load the program and its imports. 266 load_program(query, process, import_program_id, endpoint) 267 .with_context(|| format!("Failed to load imported program {import_program_id}"))?; 268 } 269 } 270 271 // Add the program to the process if it does not already exist. 272 if !process.contains_program(program.id()) { 273 debug!("Adding program {program_id} with edition {edition}"); 274 process 275 .add_programs_with_editions(&[(program, edition)]) 276 .with_context(|| format!("Failed to add program {program_id}"))?; 277 } 278 279 Ok(()) 280 } 281 282 #[cfg(test)] 283 mod tests { 284 use super::*; 285 use crate::commands::{Command, DeveloperCommand, CLI}; 286 287 #[test] 288 fn clap_alphaos_execute() -> Result<()> { 289 let arg_vec = &[ 290 "alphaos", 291 "developer", 292 "execute", 293 "--private-key", 294 "PRIVATE_KEY", 295 "--endpoint=ENDPOINT", 296 "--priority-fee", 297 "77", 298 "--record", 299 "RECORD", 300 "--dry-run", 301 "hello.alpha", 302 "hello", 303 "1u32", 304 "2u32", 305 ]; 306 let cli = CLI::try_parse_from(arg_vec)?; 307 308 let Command::Developer(developer) = cli.command else { 309 bail!("Unexpected result of clap parsing!"); 310 }; 311 let DeveloperCommand::Execute(execute) = developer.command else { 312 bail!("Unexpected result of clap parsing!"); 313 }; 314 315 assert_eq!(developer.network, 0); 316 assert_eq!(execute.private_key, Some("PRIVATE_KEY".to_string())); 317 assert_eq!(execute.endpoint, "ENDPOINT"); 318 assert_eq!(execute.priority_fee, 77); 319 assert_eq!(execute.record, Some("RECORD".into())); 320 assert_eq!(execute.program_id, "hello.alpha".to_string()); 321 assert_eq!(execute.function, "hello".to_string()); 322 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]); 323 324 Ok(()) 325 } 326 327 #[test] 328 fn clap_alphaos_execute_pk_file() -> Result<()> { 329 let arg_vec = &[ 330 "alphaos", 331 "developer", 332 "execute", 333 "--private-key-file", 334 "PRIVATE_KEY_FILE", 335 "--endpoint=ENDPOINT", 336 "--record", 337 "RECORD", 338 "--dry-run", 339 "hello.alpha", 340 "hello", 341 "1u32", 342 "2u32", 343 ]; 344 let cli = CLI::try_parse_from(arg_vec)?; 345 346 let Command::Developer(developer) = cli.command else { 347 bail!("Unexpected result of clap parsing!"); 348 }; 349 let DeveloperCommand::Execute(execute) = developer.command else { 350 bail!("Unexpected result of clap parsing!"); 351 }; 352 353 assert_eq!(developer.network, 0); 354 assert_eq!(execute.private_key_file, Some("PRIVATE_KEY_FILE".to_string())); 355 assert_eq!(execute.endpoint, "ENDPOINT"); 356 assert_eq!(execute.priority_fee, 0); // Default value. 357 assert_eq!(execute.record, Some("RECORD".into())); 358 assert_eq!(execute.program_id, "hello.alpha".to_string()); 359 assert_eq!(execute.function, "hello".to_string()); 360 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]); 361 362 Ok(()) 363 } 364 365 #[test] 366 fn clap_alphaos_execute_two_private_keys() { 367 let arg_vec = &[ 368 "alphaos", 369 "developer", 370 "execute", 371 "--private-key", 372 "PRIVATE_KEY", 373 "--private-key-file", 374 "PRIVATE_KEY_FILE", 375 "--endpoint=ENDPOINT", 376 "--priority-fee", 377 "77", 378 "--record", 379 "RECORD", 380 "--dry-run", 381 "hello.alpha", 382 "hello", 383 "1u32", 384 "2u32", 385 ]; 386 387 let err = CLI::try_parse_from(arg_vec).unwrap_err(); 388 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); 389 } 390 391 #[test] 392 fn clap_alphaos_execute_no_private_keys() { 393 let arg_vec = &[ 394 "alphaos", 395 "developer", 396 "execute", 397 "--endpoint=ENDPOINT", 398 "--priority-fee", 399 "77", 400 "--record", 401 "RECORD", 402 "--dry-run", 403 "hello.alpha", 404 "hello", 405 "1u32", 406 "2u32", 407 ]; 408 409 let err = CLI::try_parse_from(arg_vec).unwrap_err(); 410 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument); 411 } 412 }