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