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