/ adl / cli / commands / execute.rs
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  }