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