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}