aleo_agent/
program.rs

1//! Tools for executing, and managing programs on the Aleo network
2//! Program management object for loading programs for building, execution, and deployment
3//!
4//! This object is meant to be a software abstraction that can be consumed by software like
5//! CLI tools, IDE plugins, Server-side stack components and other software that needs to
6//! interact with the Aleo network.
7
8use std::cmp::min;
9use std::ops::Range;
10use std::path::PathBuf;
11use std::str::FromStr;
12
13use crate::agent::Agent;
14use anyhow::{anyhow, bail, ensure, Error, Result};
15use indexmap::IndexMap;
16
17use super::*;
18
19#[derive(Clone)]
20pub struct ProgramManager<'agent> {
21    agent: &'agent Agent,
22    program_id: ProgramID,
23}
24
25impl<'agent> ProgramManager<'agent> {
26    /// Creates a new Program Manager with an agent and a particular ProgramID.
27    pub fn new(agent: &'agent Agent, program_id: ProgramID) -> Self {
28        Self { agent, program_id }
29    }
30
31    pub fn program_id(&self) -> &ProgramID {
32        &self.program_id
33    }
34
35    pub fn agent(&self) -> &Agent {
36        self.agent
37    }
38}
39
40// execution functions
41impl<'agent> ProgramManager<'agent> {
42    /// Execute a program function on the Aleo Network.
43    ///
44    /// To run this function successfully, the program must already be deployed on the Aleo Network
45    ///
46    /// # Arguments
47    /// * `function` - The function to execute
48    /// * `inputs` - The inputs to the function
49    /// * `priority_fee` - The priority fee to pay for the transaction
50    /// * `fee_record` - The plaintext record to pay for the transaction fee. If None, the fee will be paid through the account's public balance
51    ///
52    /// # Returns
53    /// The transaction ID of the execution transaction
54    ///
55    /// # Example
56    /// ```ignore
57    /// use aleo_agent::agent::Agent;
58    /// use aleo_agent::program::ProgramManager;
59    /// let pm = Agent::default().program("xxx.aleo");
60    ///
61    /// // Execute the main function of the xxx.aleo program with inputs 1, 2, 3; priority fee 100; and no fee record
62    /// // The fee will be paid through account's public balance
63    /// let tx_id = pm.execute_program("main", vec![1, 2, 3].into_iter(), 100, None).expect("Failed to execute program");
64    /// ```
65    pub fn execute_program(
66        &self,
67        function: &str,
68        inputs: impl ExactSizeIterator<Item = impl TryInto<Value>>,
69        priority_fee: u64,
70        fee_record: Option<PlaintextRecord>,
71    ) -> Result<String> {
72        // Check program and function have valid names
73        let function_id: Identifier =
74            Identifier::from_str(function).map_err(|_| anyhow!("Invalid function name"))?;
75
76        // Get the program from chain, error if it doesn't exist
77        let program = Self::get_program_from_chain(self.program_id())?;
78
79        // Initialize an RNG and query object for the transaction
80        let rng = &mut rand::thread_rng();
81        let query = Query::from(self.agent().base_url());
82
83        let vm = Self::initialize_vm(&program)?;
84
85        let transaction = vm.execute(
86            self.agent().account().private_key(),
87            (program.id(), function_id),
88            inputs,
89            fee_record,
90            priority_fee,
91            Some(query),
92            rng,
93        )?;
94
95        // Broadcast the execution transaction to the network
96        self.agent().broadcast_transaction(&transaction)
97    }
98
99    /// Execute a program function on the Aleo Network with a priority fee and no fee record
100    ///
101    /// # Arguments
102    /// * `block_heights` - The range of block heights to search for records
103    /// * `unspent_only` - Whether to return only unspent records : true for unspent records, false for all records
104    ///
105    /// # Returns
106    /// A vector of records that match the search criteria
107    ///
108    /// # Example
109    /// ```ignore
110    /// use aleo_agent::agent::Agent;
111    /// use aleo_agent::program::ProgramManager;
112    /// let pm = Agent::default().program("xxx.aleo");
113    ///
114    /// // Get the unspent records of the first 100 blocks for the program
115    /// let records = pm.get_program_records(0..100, true).expect("Failed to get program records");
116    /// ```
117    pub fn get_program_records(
118        &self,
119        block_heights: Range<u32>,
120        unspent_only: bool,
121    ) -> Result<Vec<(Field, CiphertextRecord)>> {
122        let private_key = self.agent().account().private_key();
123        // Prepare the view key.
124        let view_key = self.agent().account().view_key();
125        // Compute the x-coordinate of the address.
126        let address_x_coordinate = view_key.to_address().to_x_coordinate();
127
128        // Prepare the starting block height, by rounding down to the nearest step of 50.
129        let start_block_height = block_heights.start - (block_heights.start % 50);
130        // Prepare the ending block height, by rounding up to the nearest step of 50.
131        let end_block_height = block_heights.end + (50 - (block_heights.end % 50));
132
133        // Initialize a vector for the records.
134        let mut records = Vec::new();
135
136        for start_height in (start_block_height..end_block_height).step_by(50) {
137            if start_height >= block_heights.end {
138                break;
139            }
140            let end_height = min(start_height + 50, block_heights.end);
141
142            let _records = self
143                .agent()
144                .get_blocks_in_range(start_height, end_height)?
145                .into_iter()
146                .flat_map(|block| block.into_transitions())
147                .filter(|transition| transition.program_id().eq(self.program_id()))
148                .flat_map(|transition| transition.into_records())
149                .filter_map(|(commitment, record)| {
150                    if record.is_owner_with_address_x_coordinate(view_key, &address_x_coordinate) {
151                        if unspent_only {
152                            let sn =
153                                CiphertextRecord::serial_number(*private_key, commitment).ok()?;
154                            if self
155                                .agent()
156                                .find_transition_id_by_input_or_output_id(sn)
157                                .is_err()
158                            {
159                                return Some((commitment, record));
160                            }
161                        } else {
162                            return Some((commitment, record));
163                        }
164                    };
165                    None
166                });
167            records.extend(_records);
168        }
169
170        Ok(records)
171    }
172
173    /// Get the current value of a mapping given a specific program, mapping name, and mapping key
174    ///
175    /// # Arguments
176    /// * `mapping_name` - The name of the mapping to query
177    /// * `key` - The key to query the mapping with
178    ///
179    /// # Returns
180    /// The value of the mapping at the given key
181    pub fn get_mapping_value(
182        &self,
183        mapping_name: impl TryInto<Identifier>,
184        key: impl TryInto<Plaintext>,
185    ) -> Result<Value> {
186        // Prepare the mapping name.
187        let mapping_name = mapping_name
188            .try_into()
189            .map_err(|_| anyhow!("Invalid mapping name"))?;
190        // Prepare the key.
191        let key = key.try_into().map_err(|_| anyhow!("Invalid key"))?;
192        let program_id = self.program_id();
193        // Perform the request.
194        let url = format!(
195            "{}/{}/program/{}/mapping/{mapping_name}/{key}",
196            self.agent().base_url(),
197            self.agent().network(),
198            program_id.name(),
199        );
200        match self.agent().client().get(&url).call()?.into_json() {
201            Ok(transition_id) => Ok(transition_id),
202            Err(error) => bail!("Failed to parse transition ID: {error}"),
203        }
204    }
205
206    /// Get all mappings associated with a program.
207    pub fn get_program_mappings(&self) -> Result<Vec<Identifier>> {
208        // Prepare the program ID.
209        let program_id = self.program_id();
210        // Perform the request.
211        let url = format!(
212            "{}/{}/program/{}/mappings",
213            self.agent().base_url(),
214            self.agent().network(),
215            program_id.name()
216        );
217        match self.agent().client().get(&url).call()?.into_json() {
218            Ok(program_mappings) => Ok(program_mappings),
219            Err(error) => bail!("Failed to parse program {program_id}: {error}"),
220        }
221    }
222}
223
224// program associated functions
225impl<'agent> ProgramManager<'agent> {
226    /// Get a program from the network by its ID. This method will return an error if it does not exist.
227    pub fn get_program_from_chain(program_id: &ProgramID) -> Result<Program> {
228        let client = ureq::Agent::new();
229        // Perform the request.
230        let url = format!(
231            "{}/{}/program/{}",
232            DEFAULT_BASE_URL, DEFAULT_TESTNET, program_id.name()
233        );
234        match client.get(&url).call()?.into_json() {
235            Ok(program) => Ok(program),
236            Err(error) => bail!("Failed to parse program {program_id}: {error}"),
237        }
238    }
239
240    /// Resolve imports of a program in a depth-first-search order from program source code
241    ///
242    /// # Arguments
243    /// * `program` - The program to resolve imports for
244    ///
245    /// # Returns
246    /// A map of program IDs to programs
247    pub fn get_import_programs_from_chain(
248        program: &Program,
249    ) -> Result<IndexMap<ProgramID, Program>> {
250        let mut found_imports = IndexMap::new();
251        for (import_id, _) in program.imports().iter() {
252            let imported_program = Self::get_program_from_chain(import_id)?;
253            let nested_imports = Self::get_import_programs_from_chain(&imported_program)?;
254            for (id, import) in nested_imports.into_iter() {
255                found_imports
256                    .contains_key(&id)
257                    .then(|| anyhow!("Circular dependency discovered in program imports"));
258                found_imports.insert(id, import);
259            }
260            found_imports
261                .contains_key(import_id)
262                .then(|| anyhow!("Circular dependency discovered in program imports"));
263            found_imports.insert(*import_id, imported_program);
264        }
265        Ok(found_imports)
266    }
267
268    /// Load a program from a file path
269    ///
270    /// # Arguments
271    /// * path - The path refers to the folder containing the program.json and *.aleo files,
272    /// which are generated by `leo build` in the Leo project.
273    pub fn load_program_from_path<P: Into<PathBuf>>(path: P) -> Result<Program> {
274        let path = path.into();
275        ensure!(path.exists(), "The program directory does not exist");
276        let package = Package::open(&path)?;
277        let program_name = package.program().id().name();
278        ensure!(
279            !Program::is_reserved_keyword(program_name),
280            "Program name is invalid (reserved): {}",
281            program_name
282        );
283        // Load the main program.
284        Ok(package.program().clone())
285    }
286
287    /// Initialize a SnarkVM instance with a program and its imports
288    fn initialize_vm(program: &Program) -> Result<VM> {
289        // Create an ephemeral SnarkVM to store the programs
290        // Initialize an RNG and query object for the transaction
291        let store = ConsensusStore::open(None)?;
292        let vm = VM::from(store)?;
293
294        // Resolve imports
295        let credits_id = ProgramID::from_str("credits.aleo")?;
296        Self::get_import_programs_from_chain(program)?
297            .iter()
298            .try_for_each(|(_, import)| {
299                if import.id() != &credits_id {
300                    vm.process().write().add_program(import)?
301                }
302                Ok::<_, Error>(())
303            })?;
304
305        // If the initialization is for an execution, add the program. Otherwise, don't add it as
306        // it will be added during the deployment process
307        vm.process().write().add_program(program)?;
308        Ok(vm)
309    }
310}