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::Developer;
17use snarkvm::{
18    console::network::{CanaryV0, MainnetV0, Network, TestnetV0},
19    prelude::{
20        Address,
21        Identifier,
22        Locator,
23        PrivateKey,
24        Process,
25        ProgramID,
26        VM,
27        Value,
28        query::Query,
29        store::{ConsensusStore, helpers::memory::ConsensusMemory},
30    },
31};
32
33use aleo_std::StorageMode;
34use anyhow::{Result, anyhow, bail};
35use clap::Parser;
36use colored::Colorize;
37use std::{path::PathBuf, str::FromStr};
38use zeroize::Zeroize;
39
40/// Executes an Aleo program function.
41#[derive(Debug, Parser)]
42pub struct Execute {
43    /// The program identifier.
44    program_id: String,
45    /// The function name.
46    function: String,
47    /// The function inputs.
48    inputs: Vec<String>,
49    /// Specify the network to create an execution for.
50    #[clap(default_value = "0", long = "network")]
51    pub network: u16,
52    /// The private key used to generate the execution.
53    #[clap(short, long)]
54    private_key: Option<String>,
55    /// Specify the path to a file containing the account private key of the node
56    #[clap(long)]
57    private_key_file: Option<String>,
58    /// The endpoint to query node state from.
59    #[clap(short, long)]
60    query: String,
61    /// The priority fee in microcredits.
62    #[clap(long)]
63    priority_fee: Option<u64>,
64    /// The record to spend the fee from.
65    #[clap(short, long)]
66    record: Option<String>,
67    /// The endpoint used to broadcast the generated transaction.
68    #[clap(short, long, conflicts_with = "dry_run")]
69    broadcast: Option<String>,
70    /// Performs a dry-run of transaction generation.
71    #[clap(short, long, conflicts_with = "broadcast")]
72    dry_run: bool,
73    /// Store generated deployment transaction to a local file.
74    #[clap(long)]
75    store: Option<String>,
76    /// Specify the path to a directory containing the ledger. Overrides the default path (also for
77    /// dev).
78    #[clap(long = "storage_path")]
79    pub storage_path: Option<PathBuf>,
80}
81
82impl Drop for Execute {
83    /// Zeroize the private key when the `Execute` struct goes out of scope.
84    fn drop(&mut self) {
85        if let Some(mut pk) = self.private_key.take() {
86            pk.zeroize()
87        }
88    }
89}
90
91impl Execute {
92    /// Executes an Aleo program function with the provided inputs.
93    #[allow(clippy::format_in_format_args)]
94    pub fn parse(self) -> Result<String> {
95        // Ensure that the user has specified an action.
96        if !self.dry_run && self.broadcast.is_none() && self.store.is_none() {
97            bail!("❌ Please specify one of the following actions: --broadcast, --dry-run, --store");
98        }
99
100        // Construct the execution for the specified network.
101        match self.network {
102            MainnetV0::ID => self.construct_execution::<MainnetV0>(),
103            TestnetV0::ID => self.construct_execution::<TestnetV0>(),
104            CanaryV0::ID => self.construct_execution::<CanaryV0>(),
105            unknown_id => bail!("Unknown network ID ({unknown_id})"),
106        }
107    }
108
109    /// Construct and process the execution transaction.
110    fn construct_execution<N: Network>(&self) -> Result<String> {
111        // Specify the query
112        let query = Query::from(&self.query);
113
114        // Retrieve the private key.
115        let key_str = match (self.private_key.as_ref(), self.private_key_file.as_ref()) {
116            (Some(private_key), None) => private_key.to_owned(),
117            (None, Some(private_key_file)) => {
118                let path = private_key_file.parse::<PathBuf>().map_err(|e| anyhow!("Invalid path - {e}"))?;
119                std::fs::read_to_string(path)?.trim().to_string()
120            }
121            (None, None) => bail!("Missing the '--private-key' or '--private-key-file' argument"),
122            (Some(_), Some(_)) => {
123                bail!("Cannot specify both the '--private-key' and '--private-key-file' flags")
124            }
125        };
126        let private_key = PrivateKey::from_str(&key_str)?;
127
128        // Retrieve the program ID.
129        let program_id = ProgramID::from_str(&self.program_id)?;
130
131        // Retrieve the function.
132        let function = Identifier::from_str(&self.function)?;
133
134        // Retrieve the inputs.
135        let inputs = self.inputs.iter().map(|input| Value::from_str(input)).collect::<Result<Vec<Value<N>>>>()?;
136
137        let locator = Locator::<N>::from_str(&format!("{}/{}", program_id, function))?;
138        println!("📦 Creating execution transaction for '{}'...\n", &locator.to_string().bold());
139
140        // Generate the execution transaction.
141        let transaction = {
142            // Initialize an RNG.
143            let rng = &mut rand::thread_rng();
144
145            // Initialize the storage.
146            let storage_mode = match &self.storage_path {
147                Some(path) => StorageMode::Custom(path.clone()),
148                None => StorageMode::Production,
149            };
150            let store = ConsensusStore::<N, ConsensusMemory<N>>::open(storage_mode)?;
151
152            // Initialize the VM.
153            let vm = VM::from(store)?;
154
155            // Load the program and it's imports into the process.
156            load_program(&self.query, &mut vm.process().write(), &program_id)?;
157
158            // Prepare the fee.
159            let fee_record = match &self.record {
160                Some(record_string) => Some(Developer::parse_record(&private_key, record_string)?),
161                None => None,
162            };
163            let priority_fee = self.priority_fee.unwrap_or(0);
164
165            // Create a new transaction.
166            vm.execute(&private_key, (program_id, function), inputs.iter(), fee_record, priority_fee, Some(query), rng)?
167        };
168
169        // Check if the public balance is sufficient.
170        if self.record.is_none() {
171            // Fetch the public balance.
172            let address = Address::try_from(&private_key)?;
173            let public_balance = Developer::get_public_balance(&address, &self.query)?;
174
175            // Check if the public balance is sufficient.
176            let storage_cost = transaction
177                .execution()
178                .ok_or_else(|| anyhow!("The transaction does not contain an execution"))?
179                .size_in_bytes()?;
180
181            // Calculate the base fee.
182            // This fee is the minimum fee required to pay for the transaction,
183            // excluding any finalize fees that the execution may incur.
184            let base_fee = storage_cost.saturating_add(self.priority_fee.unwrap_or(0));
185
186            // If the public balance is insufficient, return an error.
187            if public_balance < base_fee {
188                bail!(
189                    "❌ The public balance of {} is insufficient to pay the base fee for `{}`",
190                    public_balance,
191                    locator.to_string().bold()
192                );
193            }
194        }
195
196        println!("✅ Created execution transaction for '{}'", locator.to_string().bold());
197
198        // Determine if the transaction should be broadcast, stored, or displayed to the user.
199        Developer::handle_transaction(&self.broadcast, self.dry_run, &self.store, transaction, locator.to_string())
200    }
201}
202
203/// A helper function to recursively load the program and all of its imports into the process.
204fn load_program<N: Network>(endpoint: &str, process: &mut Process<N>, program_id: &ProgramID<N>) -> Result<()> {
205    // Fetch the program.
206    let program = Developer::fetch_program(program_id, endpoint)?;
207
208    // Return early if the program is already loaded.
209    if process.contains_program(program.id()) {
210        return Ok(());
211    }
212
213    // Iterate through the program imports.
214    for import_program_id in program.imports().keys() {
215        // Add the imports to the process if does not exist yet.
216        if !process.contains_program(import_program_id) {
217            // Recursively load the program and its imports.
218            load_program(endpoint, process, import_program_id)?;
219        }
220    }
221
222    // Add the program to the process if it does not already exist.
223    if !process.contains_program(program.id()) {
224        process.add_program(&program)?;
225    }
226
227    Ok(())
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::commands::{CLI, Command};
234
235    #[test]
236    fn clap_snarkos_execute() {
237        let arg_vec = vec![
238            "snarkos",
239            "developer",
240            "execute",
241            "--private-key",
242            "PRIVATE_KEY",
243            "--query",
244            "QUERY",
245            "--priority-fee",
246            "77",
247            "--record",
248            "RECORD",
249            "hello.aleo",
250            "hello",
251            "1u32",
252            "2u32",
253        ];
254        let cli = CLI::parse_from(arg_vec);
255
256        if let Command::Developer(Developer::Execute(execute)) = cli.command {
257            assert_eq!(execute.network, 0);
258            assert_eq!(execute.private_key, Some("PRIVATE_KEY".to_string()));
259            assert_eq!(execute.query, "QUERY");
260            assert_eq!(execute.priority_fee, Some(77));
261            assert_eq!(execute.record, Some("RECORD".into()));
262            assert_eq!(execute.program_id, "hello.aleo".to_string());
263            assert_eq!(execute.function, "hello".to_string());
264            assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
265        } else {
266            panic!("Unexpected result of clap parsing!");
267        }
268    }
269
270    #[test]
271    fn clap_snarkos_execute_pk_file() {
272        let arg_vec = vec![
273            "snarkos",
274            "developer",
275            "execute",
276            "--private-key-file",
277            "PRIVATE_KEY_FILE",
278            "--query",
279            "QUERY",
280            "--priority-fee",
281            "77",
282            "--record",
283            "RECORD",
284            "hello.aleo",
285            "hello",
286            "1u32",
287            "2u32",
288        ];
289        let cli = CLI::parse_from(arg_vec);
290
291        if let Command::Developer(Developer::Execute(execute)) = cli.command {
292            assert_eq!(execute.network, 0);
293            assert_eq!(execute.private_key_file, Some("PRIVATE_KEY_FILE".to_string()));
294            assert_eq!(execute.query, "QUERY");
295            assert_eq!(execute.priority_fee, Some(77));
296            assert_eq!(execute.record, Some("RECORD".into()));
297            assert_eq!(execute.program_id, "hello.aleo".to_string());
298            assert_eq!(execute.function, "hello".to_string());
299            assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
300        } else {
301            panic!("Unexpected result of clap parsing!");
302        }
303    }
304}