snarkos_cli/commands/developer/
execute.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkOS 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
16use super::{DEFAULT_ENDPOINT, Developer};
17use crate::{
18    commands::StoreFormat,
19    helpers::args::{parse_private_key, prepare_endpoint},
20};
21
22use snarkvm::{
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
38use aleo_std::StorageMode;
39use anyhow::{Context, Result, bail};
40use clap::{Parser, builder::NonEmptyStringValueParser};
41use colored::Colorize;
42use std::{path::PathBuf, str::FromStr};
43use tracing::debug;
44use ureq::http::Uri;
45use 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)]
53pub 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    #[clap(short, long, alias="query", default_value=DEFAULT_ENDPOINT, verbatim_doc_comment)]
78    endpoint: Uri,
79    /// The priority fee in microcredits.
80    #[clap(long, default_value_t = 0)]
81    priority_fee: u64,
82    /// The record to spend the fee from.
83    #[clap(short, long)]
84    record: Option<String>,
85    /// Set the URL used to broadcast the transaction (if no value is given, the query endpoint is used).
86    ///
87    /// The given value is expected the full URL of the endpoint, not just the base URL, e.g., "http://mynode.com/testnet/transaction/broadcast".
88    #[clap(short, long, group = "mode", verbatim_doc_comment)]
89    broadcast: Option<Option<Uri>>,
90    /// Performs a dry-run of transaction generation.
91    #[clap(short, long, group = "mode")]
92    dry_run: bool,
93    /// Store generated deployment transaction to a local file.
94    #[clap(long, group = "mode")]
95    store: Option<String>,
96    /// If --store is specified, the format in which the transaction should be stored : string or
97    /// bytes, by default : bytes.
98    #[clap(long, value_enum, default_value_t = StoreFormat::Bytes, requires="store")]
99    store_format: StoreFormat,
100    /// Wait for the transaction to be accepted by the network. Requires --broadcast.
101    #[clap(long, requires = "broadcast")]
102    wait: bool,
103    /// Timeout in seconds when waiting for transaction confirmation. Default is 60 seconds.
104    #[clap(long, default_value_t = 60, requires = "wait")]
105    timeout: u64,
106    /// Specify the path to a directory containing the ledger. Overrides the default path.
107    #[clap(long = "storage_path")]
108    storage_path: Option<PathBuf>,
109}
110
111impl Drop for Execute {
112    /// Zeroize the private key when the `Execute` struct goes out of scope.
113    fn drop(&mut self) {
114        if let Some(mut pk) = self.private_key.take() {
115            pk.zeroize()
116        }
117    }
118}
119
120impl Execute {
121    /// Executes an Aleo program function with the provided inputs.
122    pub fn parse<N: Network>(self) -> Result<String> {
123        let endpoint = prepare_endpoint(self.endpoint.clone())?;
124
125        // Specify the query
126        let query = Query::<N, BlockMemory<N>>::from(endpoint.clone());
127
128        // TODO (kaimast): can this ever be true?
129        let is_static_query = matches!(query, Query::STATIC(_));
130
131        // Retrieve the private key.
132        let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?;
133
134        // Retrieve the program ID.
135        let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?;
136
137        // Retrieve the function.
138        let function = Identifier::from_str(&self.function).with_context(|| "Failed to parse function ID")?;
139
140        // Retrieve the inputs.
141        let inputs = self.inputs.iter().map(|input| Value::from_str(input)).collect::<Result<Vec<Value<N>>>>()?;
142
143        let locator = Locator::<N>::from_str(&format!("{program_id}/{function}"))?;
144        println!("📦 Creating execution transaction for '{}'...\n", &locator.to_string().bold());
145
146        // Generate the execution transaction.
147        let transaction = {
148            // Initialize an RNG.
149            let rng = &mut rand::thread_rng();
150
151            // Initialize the storage.
152            let storage_mode = match &self.storage_path {
153                Some(path) => StorageMode::Custom(path.clone()),
154                None => StorageMode::Production,
155            };
156            let store = ConsensusStore::<N, ConsensusMemory<N>>::open(storage_mode)?;
157
158            // Initialize the VM.
159            let vm = VM::from(store)?;
160
161            if !is_static_query && program_id != ProgramID::from_str("credits.aleo")? {
162                let height = query.current_block_height()?;
163                let version = N::CONSENSUS_VERSION(height)?;
164                debug!("At block height {height} and consensus {version:?}");
165                let edition = Developer::get_latest_edition(&endpoint, &program_id)
166                    .with_context(|| format!("Failed to get latest edition for program {program_id}"))?;
167
168                // Load the program and it's imports into the process.
169                load_program(&query, &mut vm.process().write(), &program_id, edition)?;
170            }
171
172            // Prepare the fee.
173            let fee_record = match &self.record {
174                Some(record_string) => Some(
175                    Developer::parse_record(&private_key, record_string).with_context(|| "Failed to parse record")?,
176                ),
177                None => None,
178            };
179
180            // Create a new transaction.
181            vm.execute(
182                &private_key,
183                (program_id, function),
184                inputs.iter(),
185                fee_record,
186                self.priority_fee,
187                Some(&query),
188                rng,
189            )
190            .with_context(|| "VM failed to execute transaction locally")?
191        };
192
193        // Check if the public balance is sufficient.
194        if self.record.is_none() && !is_static_query {
195            // Fetch the public balance.
196            let address = Address::try_from(&private_key)?;
197            let public_balance = Developer::get_public_balance::<N>(&endpoint, &address)
198                .with_context(|| "Failed to check for sufficient funds to send transaction")?;
199
200            // Check if the public balance is sufficient.
201            let storage_cost = transaction
202                .execution()
203                .with_context(|| "Failed to get execution cost of transaction")?
204                .size_in_bytes()?;
205
206            // Calculate the base fee.
207            // This fee is the minimum fee required to pay for the transaction,
208            // excluding any finalize fees that the execution may incur.
209            let base_fee = storage_cost.saturating_add(self.priority_fee);
210
211            // If the public balance is insufficient, return an error.
212            if public_balance < base_fee {
213                bail!(
214                    "The public balance of {} is insufficient to pay the base fee for `{}`",
215                    public_balance,
216                    locator.to_string().bold()
217                );
218            }
219        }
220
221        println!("✅ Created execution transaction for '{}'", locator.to_string().bold());
222
223        // Determine if the transaction should be broadcast, stored, or displayed to the user.
224        Developer::handle_transaction(
225            &endpoint,
226            &self.broadcast,
227            self.dry_run,
228            &self.store,
229            self.store_format,
230            self.wait,
231            self.timeout,
232            transaction,
233            locator.to_string(),
234        )
235    }
236}
237
238/// A helper function to recursively load the program and all of its imports into the process.
239fn load_program<N: Network>(
240    query: &Query<N, BlockMemory<N>>,
241    process: &mut Process<N>,
242    program_id: &ProgramID<N>,
243    edition: u16,
244) -> Result<()> {
245    // Fetch the program.
246    let program = query.get_program(program_id).with_context(|| "Failed to fetch program")?;
247
248    // Return early if the program is already loaded.
249    if process.contains_program(program.id()) {
250        return Ok(());
251    }
252
253    // Iterate through the program imports.
254    for import_program_id in program.imports().keys() {
255        // Add the imports to the process if does not exist yet.
256        if !process.contains_program(import_program_id) {
257            // Recursively load the program and its imports.
258            load_program(query, process, import_program_id, edition)
259                .with_context(|| format!("Failed to load imported program {import_program_id}"))?;
260        }
261    }
262
263    // Add the program to the process if it does not already exist.
264    if !process.contains_program(program.id()) {
265        process
266            .add_programs_with_editions(&[(program, edition)])
267            .with_context(|| format!("Failed to add program {program_id}"))?;
268    }
269
270    Ok(())
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::commands::{CLI, Command, DeveloperCommand};
277
278    #[test]
279    fn clap_snarkos_execute() -> Result<()> {
280        let arg_vec = &[
281            "snarkos",
282            "developer",
283            "execute",
284            "--private-key",
285            "PRIVATE_KEY",
286            "--endpoint=ENDPOINT",
287            "--priority-fee",
288            "77",
289            "--record",
290            "RECORD",
291            "--dry-run",
292            "hello.aleo",
293            "hello",
294            "1u32",
295            "2u32",
296        ];
297        let cli = CLI::try_parse_from(arg_vec)?;
298
299        let Command::Developer(developer) = cli.command else {
300            bail!("Unexpected result of clap parsing!");
301        };
302        let DeveloperCommand::Execute(execute) = developer.command else {
303            bail!("Unexpected result of clap parsing!");
304        };
305
306        assert_eq!(developer.network, 0);
307        assert_eq!(execute.private_key, Some("PRIVATE_KEY".to_string()));
308        assert_eq!(execute.endpoint, "ENDPOINT");
309        assert_eq!(execute.priority_fee, 77);
310        assert_eq!(execute.record, Some("RECORD".into()));
311        assert_eq!(execute.program_id, "hello.aleo".to_string());
312        assert_eq!(execute.function, "hello".to_string());
313        assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
314
315        Ok(())
316    }
317
318    #[test]
319    fn clap_snarkos_execute_pk_file() -> Result<()> {
320        let arg_vec = &[
321            "snarkos",
322            "developer",
323            "execute",
324            "--private-key-file",
325            "PRIVATE_KEY_FILE",
326            "--endpoint=ENDPOINT",
327            "--record",
328            "RECORD",
329            "--dry-run",
330            "hello.aleo",
331            "hello",
332            "1u32",
333            "2u32",
334        ];
335        let cli = CLI::try_parse_from(arg_vec)?;
336
337        let Command::Developer(developer) = cli.command else {
338            bail!("Unexpected result of clap parsing!");
339        };
340        let DeveloperCommand::Execute(execute) = developer.command else {
341            bail!("Unexpected result of clap parsing!");
342        };
343
344        assert_eq!(developer.network, 0);
345        assert_eq!(execute.private_key_file, Some("PRIVATE_KEY_FILE".to_string()));
346        assert_eq!(execute.endpoint, "ENDPOINT");
347        assert_eq!(execute.priority_fee, 0); // Default value.
348        assert_eq!(execute.record, Some("RECORD".into()));
349        assert_eq!(execute.program_id, "hello.aleo".to_string());
350        assert_eq!(execute.function, "hello".to_string());
351        assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
352
353        Ok(())
354    }
355
356    #[test]
357    fn clap_snarkos_execute_two_private_keys() {
358        let arg_vec = &[
359            "snarkos",
360            "developer",
361            "execute",
362            "--private-key",
363            "PRIVATE_KEY",
364            "--private-key-file",
365            "PRIVATE_KEY_FILE",
366            "--endpoint=ENDPOINT",
367            "--priority-fee",
368            "77",
369            "--record",
370            "RECORD",
371            "--dry-run",
372            "hello.aleo",
373            "hello",
374            "1u32",
375            "2u32",
376        ];
377
378        let err = CLI::try_parse_from(arg_vec).unwrap_err();
379        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
380    }
381
382    #[test]
383    fn clap_snarkos_execute_no_private_keys() {
384        let arg_vec = &[
385            "snarkos",
386            "developer",
387            "execute",
388            "--endpoint=ENDPOINT",
389            "--priority-fee",
390            "77",
391            "--record",
392            "RECORD",
393            "--dry-run",
394            "hello.aleo",
395            "hello",
396            "1u32",
397            "2u32",
398        ];
399
400        let err = CLI::try_parse_from(arg_vec).unwrap_err();
401        assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
402    }
403}