execute.rs
1 // Copyright (C) 2019-2025 Alpha-Delta Network Inc. 2 // This file is part of the ADL library. 3 4 // The ADL 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 ADL 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 ADL library. If not, see <https://www.gnu.org/licenses/>. 16 17 use super::*; 18 19 use adl_ast::NetworkName; 20 use adl_package::{Package, ProgramData, fetch_program_from_network}; 21 use check_transaction::TransactionStatus; 22 23 use alphastd::StorageMode; 24 use alphavm::prelude::{Execution, Itertools, Network, Program, execution_cost}; 25 26 use clap::Parser; 27 use colored::*; 28 use std::{convert::TryFrom, path::PathBuf}; 29 30 #[cfg(not(feature = "only_testnet"))] 31 use alphavm::circuit::{AlphaCanaryV0, AlphaV0}; 32 use alphavm::{ 33 circuit::{Alpha, AlphaTestnetV0}, 34 prelude::{ 35 ConsensusVersion, Identifier, ProgramID, VM, 36 query::Query as SnarkVMQuery, 37 store::{ 38 ConsensusStore, 39 helpers::memory::{BlockMemory, ConsensusMemory}, 40 }, 41 }, 42 }; 43 44 /// Build, Prove and Run Adl program with inputs 45 #[derive(Parser, Debug)] 46 pub struct AdlExecute { 47 #[clap( 48 name = "NAME", 49 help = "The name of the function to execute, e.g `helloworld.alpha/main` or `main`.", 50 default_value = "main" 51 )] 52 name: String, 53 #[clap( 54 name = "INPUTS", 55 help = "The program inputs e.g. `1u32`, `record1...` (record ciphertext), or `{ owner: ...}` " 56 )] 57 inputs: Vec<String>, 58 #[clap(flatten)] 59 pub(crate) fee_options: FeeOptions, 60 #[clap(flatten)] 61 pub(crate) action: TransactionAction, 62 #[clap(flatten)] 63 pub(crate) env_override: EnvOptions, 64 #[clap(flatten)] 65 pub(crate) extra: ExtraOptions, 66 #[clap(flatten)] 67 build_options: BuildOptions, 68 } 69 70 impl Command for AdlExecute { 71 type Input = Option<Package>; 72 type Output = (); 73 74 fn log_span(&self) -> Span { 75 tracing::span!(tracing::Level::INFO, "Adl") 76 } 77 78 fn prelude(&self, context: Context) -> Result<Self::Input> { 79 // Get the path to the current directory. 80 let path = context.dir()?; 81 // Get the path to the home directory. 82 let home_path = context.home()?; 83 // Get the network, accounting for overrides. 84 let network = get_network(&self.env_override.network)?; 85 // Get the endpoint, accounting for overrides. 86 let endpoint = get_endpoint(&self.env_override.endpoint)?; 87 // If the current directory is a valid Adl package, then build it. 88 if Package::from_directory_no_graph(path, home_path, Some(network), Some(&endpoint)).is_ok() { 89 let package = AdlBuild { 90 env_override: self.env_override.clone(), 91 options: { 92 let mut options = self.build_options.clone(); 93 options.no_cache = true; 94 options 95 }, 96 } 97 .execute(context)?; 98 // Return the package. 99 Ok(Some(package)) 100 } else { 101 Ok(None) 102 } 103 } 104 105 fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> { 106 // Get the network, accounting for overrides. 107 let network = get_network(&self.env_override.network)?; 108 // Handle each network with the appropriate parameterization. 109 match network { 110 NetworkName::TestnetV0 => handle_execute::<AlphaTestnetV0>(self, context, network, input), 111 NetworkName::MainnetV0 => { 112 #[cfg(feature = "only_testnet")] 113 panic!("Mainnet chosen with only_testnet feature"); 114 #[cfg(not(feature = "only_testnet"))] 115 handle_execute::<AlphaV0>(self, context, network, input) 116 } 117 NetworkName::CanaryV0 => { 118 #[cfg(feature = "only_testnet")] 119 panic!("Canary chosen with only_testnet feature"); 120 #[cfg(not(feature = "only_testnet"))] 121 handle_execute::<AlphaCanaryV0>(self, context, network, input) 122 } 123 } 124 } 125 } 126 127 // A helper function to handle the `execute` command. 128 fn handle_execute<A: Alpha>( 129 command: AdlExecute, 130 context: Context, 131 network: NetworkName, 132 package: Option<Package>, 133 ) -> Result<<AdlExecute as Command>::Output> { 134 // Get the target chain. 135 let chain = context.chain(); 136 let ext = chain.extension_no_dot(); 137 138 // Get the private key and associated address, accounting for overrides. 139 let private_key = get_private_key(&command.env_override.private_key)?; 140 let address = Address::<A::Network>::try_from(&private_key) 141 .map_err(|e| CliError::custom(format!("Failed to parse address: {e}")))?; 142 143 // Get the endpoint, accounting for overrides. 144 let endpoint = get_endpoint(&command.env_override.endpoint)?; 145 146 // Get whether the network is a devnet, accounting for overrides. 147 let is_devnet = get_is_devnet(command.env_override.devnet); 148 149 // If the consensus heights are provided, use them; otherwise, use the default heights for the network. 150 let consensus_heights = 151 command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet)); 152 // Validate the provided consensus heights. 153 validate_consensus_heights(&consensus_heights) 154 .map_err(|e| CliError::custom(format!("Invalid consensus heights: {e}")))?; 155 // Print the consensus heights being used. 156 let consensus_heights_string = consensus_heights.iter().format(",").to_string(); 157 println!( 158 "\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" 159 ); 160 161 // Set the consensus heights in the environment. 162 #[allow(unsafe_code)] 163 unsafe { 164 // SAFETY: 165 // - `CONSENSUS_VERSION_HEIGHTS` is only set once and is only read in `alphavm::prelude::load_consensus_heights`. 166 // - There are no concurrent threads running at this point in the execution. 167 // WHY: 168 // - This is needed because there is no way to set the desired consensus heights for a particular `VM` instance 169 // without using the environment variable `CONSENSUS_VERSION_HEIGHTS`. Which is itself read once, and stored in a `OnceLock`. 170 std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string); 171 } 172 173 // Parse the <NAME> into an optional program name and a function name. 174 // If only a function name is provided, then use the program name from the package. 175 let (program_name, function_name) = match command.name.split_once('/') { 176 Some((program_name, function_name)) => (program_name.to_string(), function_name.to_string()), 177 None => match &package { 178 Some(package) => ( 179 format!( 180 "{}.{}", 181 package.programs.last().expect("There must be at least one program in a Adl package").name, 182 ext 183 ), 184 command.name, 185 ), 186 None => { 187 return Err(CliError::custom(format!( 188 "Running `adl execute {} ...`, without an explicit program name requires that your current working directory is a valid Adl project.", 189 command.name 190 )).into()); 191 } 192 }, 193 }; 194 195 // Parse the program name as a `ProgramID`. 196 let program_id = ProgramID::<A::Network>::from_str(&program_name) 197 .map_err(|e| CliError::custom(format!("Failed to parse program name: {e}")))?; 198 // Parse the function name as an `Identifier`. 199 let function_id = Identifier::<A::Network>::from_str(&function_name) 200 .map_err(|e| CliError::custom(format!("Failed to parse function name: {e}")))?; 201 202 // Get all the dependencies in the package if it exists. 203 // Get the programs and optional manifests for all programs. 204 let programs = if let Some(package) = &package { 205 // Get the package directories. 206 let build_directory = package.build_directory(); 207 let imports_directory = package.imports_directory(); 208 let source_directory = package.source_directory(); 209 // Get the program names and their bytecode. 210 package 211 .programs 212 .iter() 213 .clone() 214 .map(|program| { 215 let program_id = ProgramID::<A::Network>::from_str(&format!("{}.{}", program.name, ext)) 216 .map_err(|e| CliError::custom(format!("Failed to parse program ID: {e}")))?; 217 match &program.data { 218 ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string(), program.edition)), 219 ProgramData::SourcePath { source, .. } => { 220 // Get the path to the built bytecode. 221 let bytecode_path = if source.as_path() == source_directory.join("main.adl") { 222 build_directory.join(format!("main.{}", ext)) 223 } else { 224 imports_directory.join(format!("{}.{}", program.name, ext)) 225 }; 226 // Fetch the bytecode. 227 let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| { 228 CliError::custom(format!("Failed to read bytecode at {}: {e}", bytecode_path.display())) 229 })?; 230 // Return the bytecode and the manifest. 231 Ok((program_id, bytecode, program.edition)) 232 } 233 } 234 }) 235 .collect::<Result<Vec<_>>>()? 236 } else { 237 Vec::new() 238 }; 239 240 // Parse the program strings into AVM programs. 241 let mut programs = programs 242 .into_iter() 243 .map(|(_, bytecode, edition)| { 244 // Parse the program. 245 let program = alphavm::prelude::Program::<A::Network>::from_str(&bytecode) 246 .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?; 247 // Return the program and its name. 248 Ok((program, edition)) 249 }) 250 .collect::<Result<Vec<_>>>()?; 251 252 // Determine whether the program is local or remote. 253 let is_local = programs.iter().any(|(program, _)| program.id() == &program_id); 254 255 // If the program is local, then check that the function exists. 256 if is_local { 257 let program = &programs 258 .iter() 259 .find(|(program, _)| program.id() == &program_id) 260 .expect("Program should exist since it is local") 261 .0; 262 if !program.contains_function(&function_id) { 263 return Err(CliError::custom(format!( 264 "Function `{function_name}` does not exist in program `{program_name}`." 265 )) 266 .into()); 267 } 268 } 269 270 let inputs = 271 command.inputs.into_iter().map(|string| parse_input(&string, &private_key)).collect::<Result<Vec<_>>>()?; 272 273 // Get the first fee option. 274 let (_, priority_fee, record) = 275 parse_fee_options(&private_key, &command.fee_options, 1)?.into_iter().next().unwrap_or((None, None, None)); 276 277 // Get the consensus version. 278 let consensus_version = 279 get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?; 280 281 // Print the execution plan. 282 print_execution_plan::<A::Network>( 283 &private_key, 284 &address, 285 &endpoint, 286 &network, 287 &program_name, 288 &function_name, 289 is_local, 290 priority_fee.unwrap_or(0), 291 record.is_some(), 292 &command.action, 293 consensus_version, 294 &check_task_for_warnings(&endpoint, network, &programs, consensus_version), 295 ); 296 297 // Prompt the user to confirm the plan. 298 if !confirm("Do you want to proceed with execution?", command.extra.yes)? { 299 println!("β Execution aborted."); 300 return Ok(()); 301 } 302 303 // Initialize an RNG. 304 let rng = &mut rand::thread_rng(); 305 306 // Initialize a new VM. 307 let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?; 308 309 // Remove version suffixes from the endpoint. 310 let re = regex::Regex::new(r"v\d+$").unwrap(); 311 let query_endpoint = re.replace(&endpoint, "").to_string(); 312 313 // Specify the query. 314 let query = SnarkVMQuery::<A::Network, BlockMemory<A::Network>>::from( 315 query_endpoint 316 .parse::<Uri>() 317 .map_err(|e| CliError::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?, 318 ); 319 320 // If the program is not local, then download it and its dependencies for the network. 321 // Note: The dependencies are downloaded in "post-order" (child before parent). 322 if !is_local { 323 println!("β¬οΈ Downloading {program_name} and its dependencies from {endpoint}..."); 324 programs = load_latest_programs_from_network(&context, program_id, network, &endpoint)?; 325 }; 326 327 // Add the programs to the VM. 328 println!("\nβAdding programs to the VM in the following order:"); 329 let programs_and_editions = programs 330 .into_iter() 331 .map(|(program, edition)| { 332 print_program_source(&program.id().to_string(), edition); 333 let edition = edition.unwrap_or(LOCAL_PROGRAM_DEFAULT_EDITION); 334 (program, edition) 335 }) 336 .collect::<Vec<_>>(); 337 vm.process().write().add_programs_with_editions(&programs_and_editions)?; 338 339 // Execute the program and produce a transaction. 340 println!("\nβοΈ Executing {program_name}/{function_name}..."); 341 let (transaction, response) = vm.execute_with_response( 342 &private_key, 343 (&program_name, &function_name), 344 inputs.iter(), 345 record, 346 priority_fee.unwrap_or(0), 347 Some(&query), 348 rng, 349 )?; 350 351 // Print the execution stats. 352 print_execution_stats::<A::Network>( 353 &vm, 354 &program_name, 355 transaction.execution().expect("Expected execution"), 356 priority_fee, 357 consensus_version, 358 )?; 359 360 // Print the transaction. 361 // If the `print` option is set, print the execution transaction to the console. 362 // The transaction is printed in JSON format. 363 if command.action.print { 364 let transaction_json = serde_json::to_string_pretty(&transaction) 365 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?; 366 println!("π¨οΈ Printing execution for {program_name}\n{transaction_json}"); 367 } 368 369 // If the `save` option is set, save the execution transaction to a file in the specified directory. 370 // The file format is `program_name.execution.json`. 371 // The directory is created if it doesn't exist. 372 if let Some(path) = &command.action.save { 373 // Create the directory if it doesn't exist. 374 std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?; 375 // Save the transaction to a file. 376 let file_path = PathBuf::from(path).join(format!("{program_name}.execution.json")); 377 println!("πΎ Saving execution for {program_name} at {}", file_path.display()); 378 let transaction_json = serde_json::to_string_pretty(&transaction) 379 .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?; 380 std::fs::write(file_path, transaction_json) 381 .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?; 382 } 383 384 match response.outputs().len() { 385 0 => (), 386 1 => println!("\nβ‘οΈ Output\n"), 387 _ => println!("\nβ‘οΈ Outputs\n"), 388 }; 389 for output in response.outputs() { 390 println!(" β’ {output}"); 391 } 392 println!(); 393 394 // If the `broadcast` option is set, broadcast each deployment transaction to the network. 395 if command.action.broadcast { 396 println!("π‘ Broadcasting execution for {program_name}..."); 397 // Get and confirm the fee with the user. 398 let mut fee_id = None; 399 if let Some(fee) = transaction.fee_transition() { 400 // Most transactions will have fees, but some, like credits.alpha/upgrade executions, may not. 401 if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? { 402 println!("β Execution aborted."); 403 return Ok(()); 404 } 405 fee_id = Some(fee.id().to_string()); 406 } 407 let id = transaction.id().to_string(); 408 let height_before = check_transaction::current_height(&endpoint, network)?; 409 // Broadcast the transaction to the network. 410 let (message, status) = 411 handle_broadcast(&format!("{endpoint}/{network}/transaction/broadcast"), &transaction, &program_name)?; 412 413 let fail = |msg| { 414 println!("β Failed to broadcast execution: {msg}."); 415 Ok(()) 416 }; 417 418 match status { 419 200..=299 => { 420 let status = check_transaction::check_transaction_with_message( 421 &id, 422 fee_id.as_deref(), 423 &endpoint, 424 network, 425 height_before + 1, 426 command.extra.max_wait, 427 command.extra.blocks_to_check, 428 )?; 429 if status == Some(TransactionStatus::Accepted) { 430 println!("β Execution confirmed!"); 431 } 432 } 433 _ => { 434 return fail(&message); 435 } 436 } 437 } 438 439 Ok(()) 440 } 441 442 /// Check the execution task for warnings. 443 /// The following properties are checked: 444 /// - The component programs exist on the network and match the local ones. 445 fn check_task_for_warnings<N: Network>( 446 endpoint: &str, 447 network: NetworkName, 448 programs: &[(Program<N>, Option<u16>)], 449 consensus_version: ConsensusVersion, 450 ) -> Vec<String> { 451 let mut warnings = Vec::new(); 452 for (program, _) in programs { 453 // Check if the program exists on the network. 454 if let Ok(remote_program) = fetch_program_from_network(&program.id().to_string(), endpoint, network) { 455 // Parse the program. 456 let remote_program = match Program::<N>::from_str(&remote_program) { 457 Ok(program) => program, 458 Err(e) => { 459 warnings.push(format!("Could not parse '{}' from the network. Error: {e}", program.id())); 460 continue; 461 } 462 }; 463 // Check if the program matches the local one. 464 if remote_program != *program { 465 warnings.push(format!( 466 "The program '{}' on the network does not match the local copy. If you have a local dependency, you may use the `--no-local` flag to use the network version instead.", 467 program.id() 468 )); 469 } 470 } else { 471 warnings.push(format!( 472 "The program '{}' does not exist on the network. You may use `adl deploy --broadcast` to deploy it.", 473 program.id() 474 )); 475 } 476 } 477 // Check for a consensus version mismatch. 478 if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) { 479 warnings.push(format!("{e}. In some cases, the execution may fail")); 480 } 481 warnings 482 } 483 484 /// Pretty-print the execution plan in a readable format. 485 #[allow(clippy::too_many_arguments)] 486 fn print_execution_plan<N: Network>( 487 private_key: &PrivateKey<N>, 488 address: &Address<N>, 489 endpoint: &str, 490 network: &NetworkName, 491 program_name: &str, 492 function_name: &str, 493 is_local: bool, 494 priority_fee: u64, 495 fee_record: bool, 496 action: &TransactionAction, 497 consensus_version: ConsensusVersion, 498 warnings: &[String], 499 ) { 500 println!("\n{}", "π Execution Plan Summary".bold().underline()); 501 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed()); 502 503 println!("{}", "π§ Configuration:".bold()); 504 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow()); 505 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow()); 506 println!(" {:20}{}", "Endpoint:", endpoint.yellow()); 507 println!(" {:20}{}", "Network:", network.to_string().yellow()); 508 println!(" {:20}{}", "Consensus Version:", (consensus_version as u8).to_string().yellow()); 509 510 println!("\n{}", "π― Execution Target:".bold()); 511 println!(" {:16}{}", "Program:", program_name.cyan()); 512 println!(" {:16}{}", "Function:", function_name.cyan()); 513 println!(" {:16}{}", "Source:", if is_local { "local" } else { "remote" }); 514 515 println!("\n{}", "πΈ Fee Info:".bold()); 516 println!(" {:16}{}", "Priority Fee:", format!("{priority_fee} ΞΌcredits").green()); 517 println!(" {:16}{}", "Fee Record:", if fee_record { "yes" } else { "no (public fee)" }); 518 519 println!("\n{}", "βοΈ Actions:".bold()); 520 if !is_local { 521 println!(" - Program and its dependencies will be downloaded from the network."); 522 } 523 if action.print { 524 println!(" - Transaction will be printed to the console."); 525 } else { 526 println!(" - Transaction will NOT be printed to the console."); 527 } 528 if let Some(path) = &action.save { 529 println!(" - Transaction will be saved to {}", path.bold()); 530 } else { 531 println!(" - Transaction will NOT be saved to a file."); 532 } 533 if action.broadcast { 534 println!(" - Transaction will be broadcast to {}", endpoint.bold()); 535 } else { 536 println!(" - Transaction will NOT be broadcast to the network."); 537 } 538 539 // ββ Warnings βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 540 if !warnings.is_empty() { 541 println!("\n{}", "β οΈ Warnings:".bold().red()); 542 for warning in warnings { 543 println!(" β’ {}", warning.dimmed()); 544 } 545 } 546 547 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ\n".dimmed()); 548 } 549 550 /// Prettyβprint execution statistics without a table, using the same UI 551 /// conventions as `print_deployment_plan`. 552 fn print_execution_stats<N: Network>( 553 vm: &VM<N, ConsensusMemory<N>>, 554 program_name: &str, 555 execution: &Execution<N>, 556 priority_fee: Option<u64>, 557 consensus_version: ConsensusVersion, 558 ) -> Result<()> { 559 use colored::*; 560 561 // ββ Gather cost components ββββββββββββββββββββββββββββββββββββββββββββ 562 let (base_fee, (storage_cost, execution_cost)) = 563 execution_cost(&vm.process().read(), execution, consensus_version)?; 564 565 let base_cr = base_fee as f64 / 1_000_000.0; 566 let prio_cr = priority_fee.unwrap_or(0) as f64 / 1_000_000.0; 567 let total_cr = base_cr + prio_cr; 568 569 // ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 570 println!("\n{} {}", "π Execution Summary for".bold(), program_name.bold()); 571 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed()); 572 573 // ββ Cost breakdown ββββββββββββββββββββββββββββββββββββββββββββββββββββ 574 println!("{}", "π° Cost Breakdown (credits)".bold()); 575 println!(" {:22}{}{:.6}", "Transaction Storage:".cyan(), "".yellow(), storage_cost as f64 / 1_000_000.0); 576 println!(" {:22}{}{:.6}", "Onβchain Execution:".cyan(), "".yellow(), execution_cost as f64 / 1_000_000.0); 577 println!(" {:22}{}{:.6}", "Priority Fee:".cyan(), "".yellow(), prio_cr); 578 println!(" {:22}{}{:.6}", "Total Fee:".cyan(), "".yellow(), total_cr); 579 580 // ββ Footer rule βββββββββββββββββββββββββββββββββββββββββββββββββββββββ 581 println!("{}", "ββββββββββββββββββββββββββββββββββββββββββββββ".dimmed()); 582 Ok(()) 583 }