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