snarkos_cli/commands/developer/
execute.rs

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