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