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