aleo_agent/agent.rs
1//! The main Agent module. Contains the [Agent] types and all associated structures
2
3use crate::account::Account;
4use crate::builder::AgentBuilder;
5use crate::program::ProgramManager;
6use anyhow::{bail, ensure, Result};
7use snarkvm::circuit::prelude::num_traits::ToPrimitive;
8use std::fmt;
9use std::ops::Range;
10use std::str::FromStr;
11
12use crate::{
13 Address, CiphertextRecord, ConsensusStore, CurrentNetwork, Entry, Field, Identifier, Literal,
14 Plaintext, PlaintextRecord, ProgramID, Query, Transaction, Value, DEFAULT_BASE_URL,
15 DEFAULT_TESTNET, VM,
16};
17
18#[derive(Clone)]
19pub struct Agent {
20 client: ureq::Agent,
21 base_url: String,
22 network: String,
23 account: Account,
24}
25
26impl Default for Agent {
27 fn default() -> Agent {
28 Self {
29 client: ureq::Agent::new(),
30 account: Account::default(),
31 base_url: DEFAULT_BASE_URL.to_string(),
32 network: DEFAULT_TESTNET.to_string(),
33 }
34 }
35}
36
37impl Agent {
38 pub fn builder() -> AgentBuilder {
39 AgentBuilder::default()
40 }
41
42 pub fn new(base_url: String, network: String, account: Account) -> Agent {
43 Agent {
44 client: ureq::Agent::new(),
45 base_url,
46 network,
47 account,
48 }
49 }
50
51 pub fn program(&self, program_id: &str) -> Result<ProgramManager> {
52 let program_id = ProgramID::from_str(program_id)?;
53 Ok(ProgramManager::new(self, program_id))
54 }
55
56 pub fn account(&self) -> &Account {
57 &self.account
58 }
59
60 pub fn base_url(&self) -> &String {
61 &self.base_url
62 }
63
64 pub fn client(&self) -> &ureq::Agent {
65 &self.client
66 }
67
68 pub fn network(&self) -> &String {
69 &self.network
70 }
71
72 pub fn set_url(&mut self, url: &str) {
73 self.base_url = url.to_string();
74 }
75
76 pub fn set_network(&mut self, network: &str) {
77 self.network = network.to_string();
78 }
79
80 pub fn set_account(&mut self, account: Account) {
81 self.account = account;
82 }
83
84 pub fn local_testnet(&mut self, port: &str) {
85 self.network = DEFAULT_TESTNET.to_string();
86 self.base_url = format!("http://0.0.0.0:{}", port);
87 }
88}
89
90impl Agent {
91 /// Decrypts a ciphertext record to a plaintext record using the agent's view key.
92 ///
93 /// # Arguments
94 /// * `ciphertext_record` - The ciphertext record to decrypt.
95 ///
96 /// # Returns
97 /// A `Result` which is:
98 /// * a `PlaintextRecord` - The decrypted plaintext record.
99 /// * an `Error` - If there was an issue decrypting the record.
100 ///
101 /// # Example
102 /// ```ignore
103 /// use std::str::FromStr;
104 /// use aleo_agent::agent::Agent;
105 /// use aleo_agent::CiphertextRecord;
106 /// let agent = Agent::default();
107 /// let ciphertext_record = CiphertextRecord::from_str( "CIPHERTEXT RECORD").expect("Failed to parse ciphertext record");
108 /// let plaintext_record = agent.decrypt_ciphertext_record(&ciphertext_record);
109 /// ```
110 pub fn decrypt_ciphertext_record(
111 &self,
112 ciphertext_record: &CiphertextRecord,
113 ) -> Result<PlaintextRecord> {
114 let view_key = self.account().view_key();
115 ciphertext_record.decrypt(view_key)
116 }
117
118 /// Finds unspent records on chain.
119 ///
120 /// # Arguments
121 /// * `block_heights` - The range of block heights to search for unspent records.
122 /// * `max_gates` - The minimum threshold microcredits for the sum of balances collected from records
123 ///
124 /// # Returns
125 /// The `Ok` variant wraps the unspent records as a vector of tuples of `(Field, PlaintextRecord)`.
126 ///
127 /// # Example
128 /// ```
129 /// use aleo_agent::agent::Agent;
130 /// use aleo_agent::{MICROCREDITS, PlaintextRecord};
131 /// let agent = Agent::default();
132 /// let gate = 10 * MICROCREDITS;
133 ///
134 /// // Get unspent records with a minimum of 10 credits in the range of blocks 0 to 100
135 /// let res = agent.get_unspent_records(0..100, Some(gate)).expect("Failed to get unspent records");
136 /// let records = res
137 /// .iter().filter_map(|(_, record)| Some(record.cloned()) )
138 /// .collect::<Vec<PlaintextRecord>>();
139 /// ```
140 pub fn get_unspent_records(
141 &self,
142 block_heights: Range<u32>,
143 max_gates: Option<u64>, // microcredits
144 ) -> Result<Vec<(Field, PlaintextRecord)>> {
145 ensure!(
146 block_heights.start < block_heights.end,
147 "The start block height must be less than the end block height"
148 );
149
150 let private_key = self.account().private_key();
151 let view_key = self.account().view_key();
152 let address_x_coordinate = self.account().address().to_x_coordinate();
153
154 let step_size = 49;
155
156 // Initialize a vector for the records.
157 let mut records = vec![];
158
159 let mut total_gates = 0u64;
160 let mut end_height = block_heights.end;
161 let mut start_height = block_heights.end.saturating_sub(step_size);
162
163 for _ in (block_heights.start..block_heights.end).step_by(step_size as usize) {
164 println!(
165 "Searching blocks {} to {} for records...",
166 start_height, end_height
167 );
168 // Get blocks
169 let _records = self
170 .get_blocks_in_range(start_height, end_height)?
171 .into_iter()
172 .flat_map(|block| block.into_records())
173 .filter_map(|(commitment, record)| {
174 if record.is_owner_with_address_x_coordinate(view_key, &address_x_coordinate) {
175 let sn = PlaintextRecord::serial_number(*private_key, commitment).ok()?;
176 if self.find_transition_id_by_input_or_output_id(sn).is_err() {
177 if let Ok(record) = record.decrypt(view_key) {
178 total_gates += record.microcredits().unwrap_or(0);
179 return Some((commitment, record));
180 }
181 }
182 };
183 None
184 });
185
186 // Filter the records by the view key.
187 records.extend(_records);
188
189 // If a maximum number of gates is specified, stop searching when the total gates
190 // use the specified limit
191 if max_gates.is_some() && total_gates >= max_gates.unwrap() {
192 break;
193 }
194
195 // Search in reverse order from the latest block to the earliest block
196 end_height = start_height;
197 start_height = start_height.saturating_sub(step_size);
198 if start_height < block_heights.start {
199 start_height = block_heights.start
200 };
201 }
202 Ok(records)
203 }
204
205 /// Scans the chain for all records matching the address of agent.
206 ///
207 /// # Return
208 /// A `Result` which is:
209 /// * a `Vec<(Field, PlaintextRecord)>` - The records that match the view key.
210 /// * an `Error` - If there was an issue scanning the ledger.
211 ///
212 /// # Example
213 /// ```ignore
214 /// use aleo_agent::agent::Agent;
215 /// let agent = Agent::default();
216 /// let end_height = agent.get_latest_block_height().unwrap();
217 /// let start_height = end_height - 50; // You can arbitrarily specify the {start block}
218 /// let records = agent.scan_records(start_height..end_height, None);
219 /// match records {
220 /// Ok(records) => {
221 /// println!("Records:\n{records:#?}");
222 /// }
223 /// Err(e) => {
224 /// eprintln!("Failed to get records : {e}");
225 /// }
226 /// }
227 /// ```
228 pub fn scan_records(
229 &self,
230 block_heights: Range<u32>,
231 max_records: Option<usize>,
232 ) -> Result<Vec<(Field, PlaintextRecord)>> {
233 // Compute the x-coordinate of the address.
234 let address_x_coordinate = self.account().address().to_x_coordinate();
235
236 // Prepare the starting block height, by rounding down to the nearest step of 50.
237 let start_block_height = block_heights.start - (block_heights.start % 50);
238 // Prepare the ending block height, by rounding up to the nearest step of 50.
239 let end_block_height = block_heights.end + (50 - (block_heights.end % 50));
240
241 // Initialize a vector for the records.
242 let mut records = Vec::new();
243
244 for start_height in (start_block_height..end_block_height).step_by(50) {
245 println!(
246 "Searching blocks {} to {} for records...",
247 start_height, end_block_height
248 );
249 if start_height >= block_heights.end {
250 break;
251 }
252 let end = start_height + 50;
253 let end_height = if end > block_heights.end {
254 block_heights.end
255 } else {
256 end
257 };
258
259 let view_key = self.account().view_key();
260 // Filter the records by the view key.
261 let _records = self
262 .get_blocks_in_range(start_height, end_height)?
263 .into_iter()
264 .flat_map(|block| block.into_records())
265 .filter_map(|(commitment, record)| {
266 if record.is_owner_with_address_x_coordinate(view_key, &address_x_coordinate) {
267 Some((
268 commitment,
269 record.decrypt(view_key).expect("Failed to decrypt records"),
270 ))
271 } else {
272 None
273 }
274 })
275 .collect::<Vec<(Field, PlaintextRecord)>>();
276
277 records.extend(_records);
278
279 if records.len() >= max_records.unwrap_or(usize::MAX) {
280 break;
281 }
282 }
283
284 Ok(records)
285 }
286}
287
288impl Agent {
289 /// Fetch the public balance in microcredits associated with the address.
290 ///
291 /// # Returns
292 /// A `Result` which is:
293 /// * a `u64` - The public balance in microcredits associated with the address.
294 /// * an `Error` - If there was an issue fetching the public balance.
295 pub fn get_public_balance(&self) -> Result<u64> {
296 let credits = ProgramID::from_str("credits.aleo")?;
297 let account_mapping = Identifier::from_str("account")?;
298 let url = format!(
299 "{}/{}/program/{}/mapping/{}/{}",
300 self.base_url(),
301 self.network(),
302 credits,
303 account_mapping,
304 self.account().address()
305 );
306 let response = self.client().get(&url).call()?;
307 Ok(response
308 .into_json::<Option<Value>>()?
309 .and_then(|value| match value {
310 //Value::Plaintext(Plaintext::Literal(Literal::U64(amount), _))
311 Value::Plaintext(Plaintext::Literal(Literal::U64(amount), _)) => {
312 Some(amount.to_u64().unwrap())
313 }
314 _ => None,
315 })
316 .unwrap_or_default())
317 }
318
319 /// Fetches the transactions associated with the agent's account.
320 ///
321 /// # Returns
322 /// A `Result` which is:
323 /// * a `Vec<Transaction>` - The transactions associated with the agent's account.
324 /// * an `Error` - If there was an issue fetching the transactions.
325 pub fn get_transactions(&self) -> Result<Vec<Transaction>> {
326 let url = format!(
327 "{}/{}/address/{}",
328 self.base_url(),
329 self.network(),
330 self.account().address()
331 );
332 match self.client().get(&url).call()?.into_json() {
333 Ok(transaction) => Ok(transaction),
334 Err(error) => bail!("Failed to get account transactions : {error}"),
335 }
336 }
337
338 /// Executes a transfer to the specified recipient_address with the specified amount and fee.
339 ///
340 /// # Arguments
341 /// * `amount` - The amount to be transferred.
342 /// * `fee` - The fee for the transfer.
343 /// * `recipient_address` - The address of the recipient.
344 /// * `transfer_type` - The type of transfer.
345 /// * `amount_record` - An optional record of the amount.
346 /// * `fee_record` - An optional record of the fee.
347 ///
348 /// # Returns
349 /// A `Result` which is:
350 /// * a `String` - The transaction hash .
351 /// * an `Error` - If there was an issue executing the transfer.
352 /// Executes a transfer to the specified recipient_address with the specified amount and fee.
353 /// Specify 0 for no fee.
354 ///
355 /// # Example
356 /// ```ignore
357 /// use std::str::FromStr;
358 /// use aleo_agent::Address;
359 /// use aleo_agent::agent::{Agent, TransferArgs, TransferType};
360 /// let agent = Agent::default();
361 /// // just use for test
362 /// let recipient_address = Address::zero();
363 /// let amount = 100;
364 /// let priority_fee = 0;
365 ///
366 /// // Public transfer 100 microcredits to the recipient address with 0 priority fee
367 /// let transfer_args = TransferArgs::from(amount, recipient_address, priority_fee, None, TransferType::Public);
368 /// let transfer_result = agent.transfer(transfer_args);
369 /// ```
370 pub fn transfer(&self, args: TransferArgs) -> Result<String> {
371 match &(args.transfer_type) {
372 TransferType::Private(from_record) | TransferType::PrivateToPublic(from_record) => {
373 ensure!(
374 from_record.microcredits()? >= args.amount,
375 "Credits in amount record must greater than transfer amount specified"
376 );
377 }
378 _ => {}
379 }
380
381 if let Some(fee_record) = args.fee_record.as_ref() {
382 ensure!(
383 fee_record.microcredits()? >= args.priority_fee,
384 "Credits in fee record must greater than fee specified"
385 );
386 }
387
388 let inputs = args.to_inputs();
389 let transfer_function = args.transfer_type.to_string();
390 let rng = &mut rand::thread_rng();
391 // Initialize a VM
392 let store = ConsensusStore::open(None)?;
393 let vm = VM::from(store)?;
394 // Specify the network state query
395 let query = Query::from(self.base_url().clone());
396 // Create a new transaction.
397 let execution = vm.execute(
398 self.account().private_key(),
399 ("credits.aleo", transfer_function),
400 inputs.iter(),
401 args.fee_record,
402 args.priority_fee,
403 Some(query),
404 rng,
405 )?;
406 self.broadcast_transaction(&execution)
407 }
408}
409
410/// A trait providing convenient methods for accessing the amount of Aleo present in a record
411pub trait Credits {
412 /// Get the amount of credits in the record if the record possesses Aleo credits
413 fn credits(&self) -> Result<f64> {
414 Ok(self.microcredits()? as f64 / 1_000_000.0)
415 }
416
417 /// Get the amount of microcredits in the record if the record possesses Aleo credits
418 fn microcredits(&self) -> Result<u64>;
419}
420
421impl Credits for PlaintextRecord {
422 fn microcredits(&self) -> Result<u64> {
423 let amount = match self.find(&[Identifier::from_str("microcredits")?])? {
424 Entry::Private(Plaintext::Literal(Literal::<CurrentNetwork>::U64(amount), _)) => amount,
425 _ => bail!("The record provided does not contain a microcredits field"),
426 };
427 Ok(*amount)
428 }
429}
430
431#[derive(Clone, Debug)]
432pub enum TransferType {
433 // param: from record plaintext
434 Private(PlaintextRecord),
435 // param: from record plaintext
436 PrivateToPublic(PlaintextRecord),
437 Public,
438 PublicToPrivate,
439}
440
441impl fmt::Display for TransferType {
442 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
443 match *self {
444 TransferType::Private(_) => write!(f, "transfer_private"),
445 TransferType::PrivateToPublic(_) => write!(f, "transfer_private_to_public"),
446 TransferType::Public => write!(f, "transfer_public"),
447 TransferType::PublicToPrivate => write!(f, "transfer_public_to_private"),
448 }
449 }
450}
451
452/// Arguments for a transfer.
453/// amount, fee, recipient address, from record
454#[derive(Clone, Debug)]
455pub struct TransferArgs {
456 amount: u64, // microcredits
457 priority_fee: u64, // microcredits
458 recipient_address: Address,
459 transfer_type: TransferType,
460 fee_record: Option<PlaintextRecord>,
461}
462
463impl TransferArgs {
464 /// Create a new transfer argument.
465 ///
466 /// # Arguments
467 /// * `amount` - The amount to be transferred.
468 /// * `recipient_address` - The address of the recipient.
469 /// * `priority_fee` - The fee for the transfer.
470 /// * `fee_record` - An optional record of the fee.
471 /// * `transfer_type` - The type of transfer.
472 ///
473 /// # Returns
474 /// A `TransferArgs` - The transfer arguments.
475 ///
476 /// # Example
477 /// ```
478 /// use std::str::FromStr;
479 /// use aleo_agent::{Address, MICROCREDITS};
480 /// use aleo_agent::agent::{TransferArgs, TransferType};
481 /// let recipient_address = Address::zero();
482 /// let amount = 10 * MICROCREDITS; // 10 credit
483 /// let priority_fee = 0;
484 /// let transfer_args = TransferArgs::from(amount, recipient_address, priority_fee, None, TransferType::Public);
485 /// ```
486 pub fn from(
487 amount: u64,
488 recipient_address: Address,
489 priority_fee: u64,
490 fee_record: Option<PlaintextRecord>,
491 transfer_type: TransferType,
492 ) -> Self {
493 Self {
494 amount,
495 priority_fee,
496 recipient_address,
497 transfer_type,
498 fee_record,
499 }
500 }
501
502 /// Convert the transfer arguments to a vector of values.
503 ///
504 /// # Returns
505 /// A `Vec<Value>` - The transfer arguments as a vector of values.
506 pub fn to_inputs(&self) -> Vec<Value> {
507 match &(self.transfer_type) {
508 TransferType::Private(from_record) | TransferType::PrivateToPublic(from_record) => {
509 vec![
510 Value::Record(from_record.clone()),
511 Value::from_str(&self.recipient_address.to_string()).unwrap(),
512 Value::from_str(&format!("{}u64", self.amount)).unwrap(),
513 ]
514 }
515 _ => {
516 vec![
517 Value::from_str(&self.recipient_address.to_string()).unwrap(),
518 Value::from_str(&format!("{}u64", self.amount)).unwrap(),
519 ]
520 }
521 }
522 }
523}