aleo_rust/api/
blocking.rs

1// Copyright (C) 2019-2023 Aleo Systems Inc.
2// This file is part of the Aleo SDK library.
3
4// The Aleo SDK library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Aleo SDK library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18
19#[cfg(not(feature = "async"))]
20#[allow(clippy::type_complexity)]
21impl<N: Network> AleoAPIClient<N> {
22    /// Get the latest block height
23    pub fn latest_height(&self) -> Result<u32> {
24        let url = format!("{}/{}/latest/height", self.base_url, self.network_id);
25        match self.client.get(&url).call()?.into_json() {
26            Ok(height) => Ok(height),
27            Err(error) => bail!("Failed to parse the latest block height: {error}"),
28        }
29    }
30
31    /// Get the latest block hash
32    pub fn latest_hash(&self) -> Result<N::BlockHash> {
33        let url = format!("{}/{}/latest/hash", self.base_url, self.network_id);
34        match self.client.get(&url).call()?.into_json() {
35            Ok(hash) => Ok(hash),
36            Err(error) => bail!("Failed to parse the latest block hash: {error}"),
37        }
38    }
39
40    /// Get the latest block
41    pub fn latest_block(&self) -> Result<Block<N>> {
42        let url = format!("{}/{}/latest/block", self.base_url, self.network_id);
43        match self.client.get(&url).call()?.into_json() {
44            Ok(block) => Ok(block),
45            Err(error) => bail!("Failed to parse the latest block: {error}"),
46        }
47    }
48
49    /// Get the block matching the specific height from the network
50    pub fn get_block(&self, height: u32) -> Result<Block<N>> {
51        let url = format!("{}/{}/block/{height}", self.base_url, self.network_id);
52        match self.client.get(&url).call()?.into_json() {
53            Ok(block) => Ok(block),
54            Err(error) => bail!("Failed to parse block {height}: {error}"),
55        }
56    }
57
58    /// Get a range of blocks from the network (limited 50 blocks at a time)
59    pub fn get_blocks(&self, start_height: u32, end_height: u32) -> Result<Vec<Block<N>>> {
60        if start_height >= end_height {
61            bail!("Start height must be less than end height");
62        } else if end_height - start_height > 50 {
63            bail!("Cannot request more than 50 blocks at a time");
64        }
65
66        let url = format!("{}/{}/blocks?start={start_height}&end={end_height}", self.base_url, self.network_id);
67        match self.client.get(&url).call()?.into_json() {
68            Ok(blocks) => Ok(blocks),
69            Err(error) => {
70                bail!("Failed to parse blocks {start_height} (inclusive) to {end_height} (exclusive): {error}")
71            }
72        }
73    }
74
75    /// Retrieve a transaction by via its transaction id
76    pub fn get_transaction(&self, transaction_id: &str) -> Result<Transaction<N>> {
77        let url = format!("{}/{}/transaction/{transaction_id}", self.base_url, self.network_id).replace('"', "");
78        match self.client.get(&url).call()?.into_json() {
79            Ok(transaction) => Ok(transaction),
80            Err(error) => bail!("Failed to parse transaction '{transaction_id}': {error}"),
81        }
82    }
83
84    /// Get pending transactions currently in the mempool.
85    pub fn get_memory_pool_transactions(&self) -> Result<Vec<Transaction<N>>> {
86        let url = format!("{}/{}/memoryPool/transactions", self.base_url, self.network_id);
87        match self.client.get(&url).call()?.into_json() {
88            Ok(transactions) => Ok(transactions),
89            Err(error) => bail!("Failed to parse memory pool transactions: {error}"),
90        }
91    }
92
93    /// Get a program from the network by its ID. This method will return an error if it does not exist.
94    pub fn get_program(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<Program<N>> {
95        // Prepare the program ID.
96        let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
97        // Perform the request.
98        let url = format!("{}/{}/program/{program_id}", self.base_url, self.network_id);
99        match self.client.get(&url).call()?.into_json() {
100            Ok(program) => Ok(program),
101            Err(error) => bail!("Failed to parse program {program_id}: {error}"),
102        }
103    }
104
105    /// Resolve imports of a program in a depth-first-search order from a program id
106    pub fn get_program_imports(
107        &self,
108        program_id: impl TryInto<ProgramID<N>>,
109    ) -> Result<IndexMap<ProgramID<N>, Program<N>>> {
110        let program = self.get_program(program_id)?;
111        self.get_program_imports_from_source(&program)
112    }
113
114    /// Resolve imports of a program in a depth-first-search order from program source code
115    pub fn get_program_imports_from_source(&self, program: &Program<N>) -> Result<IndexMap<ProgramID<N>, Program<N>>> {
116        let mut found_imports = IndexMap::new();
117        for (import_id, _) in program.imports().iter() {
118            let imported_program = self.get_program(import_id)?;
119            let nested_imports = self.get_program_imports_from_source(&imported_program)?;
120            for (id, import) in nested_imports.into_iter() {
121                found_imports.contains_key(&id).then(|| anyhow!("Circular dependency discovered in program imports"));
122                found_imports.insert(id, import);
123            }
124            found_imports.contains_key(import_id).then(|| anyhow!("Circular dependency discovered in program imports"));
125            found_imports.insert(*import_id, imported_program);
126        }
127        Ok(found_imports)
128    }
129
130    /// Get all mappings associated with a program.
131    pub fn get_program_mappings(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<Vec<Identifier<N>>> {
132        // Prepare the program ID.
133        let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
134        // Perform the request.
135        let url = format!("{}/{}/program/{program_id}/mappings", self.base_url, self.network_id);
136        match self.client.get(&url).call()?.into_json() {
137            Ok(program_mappings) => Ok(program_mappings),
138            Err(error) => bail!("Failed to parse program {program_id}: {error}"),
139        }
140    }
141
142    /// Get the current value of a mapping given a specific program, mapping name, and mapping key
143    pub fn get_mapping_value(
144        &self,
145        program_id: impl TryInto<ProgramID<N>>,
146        mapping_name: impl TryInto<Identifier<N>>,
147        key: impl TryInto<Plaintext<N>>,
148    ) -> Result<Value<N>> {
149        // Prepare the program ID.
150        let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
151        // Prepare the mapping name.
152        let mapping_name = mapping_name.try_into().map_err(|_| anyhow!("Invalid mapping name"))?;
153        // Prepare the key.
154        let key = key.try_into().map_err(|_| anyhow!("Invalid key"))?;
155        // Perform the request.
156        let url = format!("{}/{}/program/{program_id}/mapping/{mapping_name}/{key}", self.base_url, self.network_id);
157        match self.client.get(&url).call()?.into_json() {
158            Ok(transition_id) => Ok(transition_id),
159            Err(error) => bail!("Failed to parse transition ID: {error}"),
160        }
161    }
162
163    pub fn find_block_hash(&self, transaction_id: N::TransactionID) -> Result<N::BlockHash> {
164        let url = format!("{}/{}/find/blockHash/{transaction_id}", self.base_url, self.network_id);
165        match self.client.get(&url).call()?.into_json() {
166            Ok(hash) => Ok(hash),
167            Err(error) => bail!("Failed to parse block hash: {error}"),
168        }
169    }
170
171    /// Returns the transition ID that contains the given `input ID` or `output ID`.
172    pub fn find_transition_id(&self, input_or_output_id: Field<N>) -> Result<N::TransitionID> {
173        let url = format!("{}/{}/find/transitionID/{input_or_output_id}", self.base_url, self.network_id);
174        match self.client.get(&url).call()?.into_json() {
175            Ok(transition_id) => Ok(transition_id),
176            Err(error) => bail!("Failed to parse transition ID: {error}"),
177        }
178    }
179
180    /// Scans the ledger for records that match the given view key.
181    pub fn scan(
182        &self,
183        view_key: impl TryInto<ViewKey<N>>,
184        block_heights: Range<u32>,
185        max_records: Option<usize>,
186    ) -> Result<Vec<(Field<N>, Record<N, Ciphertext<N>>)>> {
187        // Prepare the view key.
188        let view_key = view_key.try_into().map_err(|_| anyhow!("Invalid view key"))?;
189        // Compute the x-coordinate of the address.
190        let address_x_coordinate = view_key.to_address().to_x_coordinate();
191
192        // Prepare the starting block height, by rounding down to the nearest step of 50.
193        let start_block_height = block_heights.start - (block_heights.start % 50);
194        // Prepare the ending block height, by rounding up to the nearest step of 50.
195        let end_block_height = block_heights.end + (50 - (block_heights.end % 50));
196
197        // Initialize a vector for the records.
198        let mut records = Vec::new();
199
200        for start_height in (start_block_height..end_block_height).step_by(50) {
201            println!("Searching blocks {} to {} for records...", start_height, end_block_height);
202            if start_height >= block_heights.end {
203                break;
204            }
205            let end = start_height + 50;
206            let end_height = if end > block_heights.end { block_heights.end } else { end };
207
208            // Prepare the URL.
209            let records_iter =
210                self.get_blocks(start_height, end_height)?.into_iter().flat_map(|block| block.into_records());
211
212            // Filter the records by the view key.
213            records.extend(records_iter.filter_map(|(commitment, record)| {
214                match record.is_owner_with_address_x_coordinate(&view_key, &address_x_coordinate) {
215                    true => Some((commitment, record)),
216                    false => None,
217                }
218            }));
219
220            if records.len() >= max_records.unwrap_or(usize::MAX) {
221                break;
222            }
223        }
224
225        Ok(records)
226    }
227
228    /// Search for records that belong to a specific program
229    pub fn get_program_records(
230        &self,
231        private_key: &PrivateKey<N>,
232        program_id: impl TryInto<ProgramID<N>>,
233        block_heights: Range<u32>,
234        unspent_only: bool,
235        max_records: Option<usize>,
236    ) -> Result<Vec<(Field<N>, Record<N, Ciphertext<N>>)>> {
237        // Prepare the view key.
238        let view_key = ViewKey::try_from(private_key)?;
239        // Compute the x-coordinate of the address.
240        let address_x_coordinate = view_key.to_address().to_x_coordinate();
241
242        // Prepare the starting block height, by rounding down to the nearest step of 50.
243        let start_block_height = block_heights.start - (block_heights.start % 50);
244        // Prepare the ending block height, by rounding up to the nearest step of 50.
245        let end_block_height = block_heights.end + (50 - (block_heights.end % 50));
246
247        // Initialize a vector for the records.
248        let mut records = Vec::new();
249
250        let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid Program ID"))?;
251
252        for start_height in (start_block_height..end_block_height).step_by(50) {
253            println!("Searching blocks {} to {} for records...", start_height, end_block_height);
254            if start_height >= block_heights.end {
255                break;
256            }
257
258            let end = start_height + 50;
259            let end_height = if end > block_heights.end { block_heights.end } else { end };
260
261            // Prepare the URL.
262            records.extend(
263                self.get_blocks(start_height, end_height)?
264                    .into_iter()
265                    .flat_map(|block| block.into_transitions())
266                    .filter(|transition| transition.program_id() == &program_id)
267                    .flat_map(|transition| transition.into_records())
268                    .filter_map(|(commitment, record)| {
269                        match record.is_owner_with_address_x_coordinate(&view_key, &address_x_coordinate) {
270                            true => {
271                                let sn = Record::<N, Ciphertext<N>>::serial_number(*private_key, commitment).ok()?;
272                                if unspent_only {
273                                    if self.find_transition_id(sn).is_err() { Some((commitment, record)) } else { None }
274                                } else {
275                                    Some((commitment, record))
276                                }
277                            }
278                            false => None,
279                        }
280                    }),
281            );
282
283            if let Some(max_records) = max_records {
284                if records.len() >= max_records {
285                    break;
286                }
287            }
288        }
289
290        Ok(records)
291    }
292
293    /// Search for unspent records in the ledger
294    pub fn get_unspent_records(
295        &self,
296        private_key: &PrivateKey<N>,
297        block_heights: Range<u32>,
298        max_gates: Option<u64>,
299        specified_amounts: Option<&Vec<u64>>,
300    ) -> Result<Vec<(Field<N>, Record<N, Plaintext<N>>)>> {
301        let view_key = ViewKey::try_from(private_key)?;
302        let address_x_coordinate = view_key.to_address().to_x_coordinate();
303
304        let step_size = 49;
305        let required_amounts = if let Some(amounts) = specified_amounts {
306            ensure!(!amounts.is_empty(), "If specific amounts are specified, there must be one amount specified");
307            let mut required_amounts = amounts.clone();
308            required_amounts.sort_by(|a, b| b.cmp(a));
309            required_amounts
310        } else {
311            vec![]
312        };
313
314        ensure!(
315            block_heights.start < block_heights.end,
316            "The start block height must be less than the end block height"
317        );
318
319        // Initialize a vector for the records.
320        let mut records = vec![];
321
322        let mut total_gates = 0u64;
323        let mut end_height = block_heights.end;
324        let mut start_height = block_heights.end.saturating_sub(step_size);
325
326        for _ in (block_heights.start..block_heights.end).step_by(step_size as usize) {
327            println!("Searching blocks {} to {} for records...", start_height, end_height);
328            // Get blocks
329            let records_iter =
330                self.get_blocks(start_height, end_height)?.into_iter().flat_map(|block| block.into_records());
331
332            // Search in reverse order from the latest block to the earliest block
333            end_height = start_height;
334            start_height = start_height.saturating_sub(step_size);
335            if start_height < block_heights.start {
336                start_height = block_heights.start
337            };
338            // Filter the records by the view key.
339            records.extend(records_iter.filter_map(|(commitment, record)| {
340                match record.is_owner_with_address_x_coordinate(&view_key, &address_x_coordinate) {
341                    true => {
342                        let sn = Record::<N, Ciphertext<N>>::serial_number(*private_key, commitment).ok()?;
343                        if self.find_transition_id(sn).is_err() {
344                            let record = record.decrypt(&view_key);
345                            if let Ok(record) = record {
346                                total_gates += record.microcredits().unwrap_or(0);
347                                Some((commitment, record))
348                            } else {
349                                None
350                            }
351                        } else {
352                            None
353                        }
354                    }
355                    false => None,
356                }
357            }));
358            // If a maximum number of gates is specified, stop searching when the total gates
359            // exceeds the specified limit
360            if max_gates.is_some() && total_gates >= max_gates.unwrap() {
361                break;
362            }
363            // If a list of specified amounts is specified, stop searching when records matching
364            // those amounts are found
365            if !required_amounts.is_empty() {
366                records.sort_by(|(_, first), (_, second)| {
367                    second.microcredits().unwrap_or(0).cmp(&first.microcredits().unwrap_or(0))
368                });
369                let mut found_indices = std::collections::HashSet::<usize>::new();
370                required_amounts.iter().for_each(|amount| {
371                    for (pos, (_, found_record)) in records.iter().enumerate() {
372                        let found_amount = found_record.microcredits().unwrap_or(0);
373                        if !found_indices.contains(&pos) && found_amount >= *amount {
374                            found_indices.insert(pos);
375                        }
376                    }
377                });
378                if found_indices.len() >= required_amounts.len() {
379                    let found_records = records[0..required_amounts.len()].to_vec();
380                    return Ok(found_records);
381                }
382            }
383        }
384        if !required_amounts.is_empty() {
385            bail!(
386                "Could not find enough records with the specified amounts, consider splitting records into smaller amounts"
387            );
388        }
389        Ok(records)
390    }
391
392    /// Broadcast a deploy or execute transaction to the Aleo network
393    pub fn transaction_broadcast(&self, transaction: Transaction<N>) -> Result<String> {
394        let url = format!("{}/{}/transaction/broadcast", self.base_url, self.network_id);
395        match self.client.post(&url).send_json(&transaction) {
396            Ok(response) => match response.into_string() {
397                Ok(success_response) => Ok(success_response),
398                Err(error) => bail!("❌ Transaction response was malformed {}", error),
399            },
400            Err(error) => {
401                let error_message = match error {
402                    ureq::Error::Status(code, response) => {
403                        format!("(status code {code}: {:?})", response.into_string()?)
404                    }
405                    ureq::Error::Transport(err) => format!("({err})"),
406                };
407
408                match transaction {
409                    Transaction::Deploy(..) => {
410                        bail!("❌ Failed to deploy program to {}: {}", &url, error_message)
411                    }
412                    Transaction::Execute(..) => {
413                        bail!("❌ Failed to broadcast execution to {}: {}", &url, error_message)
414                    }
415                    Transaction::Fee(..) => {
416                        bail!("❌ Failed to broadcast fee execution to {}: {}", &url, error_message)
417                    }
418                }
419            }
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_api_get_blocks() {
430        let client = AleoAPIClient::<Testnet3>::testnet3();
431        let blocks = client.get_blocks(0, 3).unwrap();
432
433        // Check height matches
434        assert_eq!(blocks[0].height(), 0);
435        assert_eq!(blocks[1].height(), 1);
436        assert_eq!(blocks[2].height(), 2);
437
438        // Check block hashes
439        assert_eq!(blocks[1].previous_hash(), blocks[0].hash());
440        assert_eq!(blocks[2].previous_hash(), blocks[1].hash());
441    }
442
443    #[test]
444    fn test_mappings_query() {
445        let client = AleoAPIClient::<Testnet3>::testnet3();
446        let mappings = client.get_program_mappings("credits.aleo").unwrap();
447        // Assert there's only one mapping in credits.aleo
448        assert_eq!(mappings.len(), 4);
449
450        let identifier = mappings[0];
451        // Assert the identifier is "account"
452        assert_eq!(identifier.to_string(), "committee");
453    }
454
455    #[test]
456    fn test_import_resolution() {
457        let client = AleoAPIClient::<Testnet3>::testnet3();
458        let imports = client.get_program_imports("imported_add_mul.aleo").unwrap();
459        let id1 = ProgramID::<Testnet3>::from_str("multiply_test.aleo").unwrap();
460        let id2 = ProgramID::<Testnet3>::from_str("double_test.aleo").unwrap();
461        let id3 = ProgramID::<Testnet3>::from_str("addition_test.aleo").unwrap();
462
463        let keys = imports.keys();
464        println!("Imports: {keys:?}");
465        assert!(imports.contains_key(&id1));
466        assert!(imports.contains_key(&id2));
467        assert!(imports.contains_key(&id3));
468        assert_eq!(keys.len(), 3);
469    }
470}