Skip to main content

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, anyhow, bail};
40use clap::{Parser, builder::NonEmptyStringValueParser};
41use colored::Colorize;
42use std::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    ///
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
113impl 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
122impl 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.aleo")? {
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.
241fn 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)]
280mod tests {
281    use super::*;
282    use crate::commands::{CLI, Command, DeveloperCommand};
283
284    #[test]
285    fn clap_snarkos_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.aleo",
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.aleo".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_snarkos_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.aleo",
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.aleo".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_snarkos_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.aleo",
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_snarkos_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.aleo",
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}