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}