1use super::*;
18
19#[cfg(not(feature = "async"))]
20#[allow(clippy::type_complexity)]
21impl<N: Network> AleoAPIClient<N> {
22 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 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 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 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 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 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 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 pub fn get_program(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<Program<N>> {
95 let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
97 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 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 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 pub fn get_program_mappings(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<Vec<Identifier<N>>> {
132 let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
134 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 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 let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
151 let mapping_name = mapping_name.try_into().map_err(|_| anyhow!("Invalid mapping name"))?;
153 let key = key.try_into().map_err(|_| anyhow!("Invalid key"))?;
155 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 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 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 let view_key = view_key.try_into().map_err(|_| anyhow!("Invalid view key"))?;
189 let address_x_coordinate = view_key.to_address().to_x_coordinate();
191
192 let start_block_height = block_heights.start - (block_heights.start % 50);
194 let end_block_height = block_heights.end + (50 - (block_heights.end % 50));
196
197 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 let records_iter =
210 self.get_blocks(start_height, end_height)?.into_iter().flat_map(|block| block.into_records());
211
212 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 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 let view_key = ViewKey::try_from(private_key)?;
239 let address_x_coordinate = view_key.to_address().to_x_coordinate();
241
242 let start_block_height = block_heights.start - (block_heights.start % 50);
244 let end_block_height = block_heights.end + (50 - (block_heights.end % 50));
246
247 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 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 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 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 let records_iter =
330 self.get_blocks(start_height, end_height)?.into_iter().flat_map(|block| block.into_records());
331
332 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 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 max_gates.is_some() && total_gates >= max_gates.unwrap() {
361 break;
362 }
363 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 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 assert_eq!(blocks[0].height(), 0);
435 assert_eq!(blocks[1].height(), 1);
436 assert_eq!(blocks[2].height(), 2);
437
438 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_eq!(mappings.len(), 4);
449
450 let identifier = mappings[0];
451 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}