use super::*;
#[cfg(not(feature = "async"))]
#[allow(clippy::type_complexity)]
impl<N: Network> AleoAPIClient<N> {
pub fn latest_height(&self) -> Result<u32> {
let url = format!("{}/{}/latest/height", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(height) => Ok(height),
Err(error) => bail!("Failed to parse the latest block height: {error}"),
}
}
pub fn latest_hash(&self) -> Result<N::BlockHash> {
let url = format!("{}/{}/latest/hash", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(hash) => Ok(hash),
Err(error) => bail!("Failed to parse the latest block hash: {error}"),
}
}
pub fn latest_block(&self) -> Result<Block<N>> {
let url = format!("{}/{}/latest/block", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(block) => Ok(block),
Err(error) => bail!("Failed to parse the latest block: {error}"),
}
}
pub fn get_block(&self, height: u32) -> Result<Block<N>> {
let url = format!("{}/{}/block/{height}", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(block) => Ok(block),
Err(error) => bail!("Failed to parse block {height}: {error}"),
}
}
pub fn get_blocks(&self, start_height: u32, end_height: u32) -> Result<Vec<Block<N>>> {
if start_height >= end_height {
bail!("Start height must be less than end height");
} else if end_height - start_height > 50 {
bail!("Cannot request more than 50 blocks at a time");
}
let url = format!("{}/{}/blocks?start={start_height}&end={end_height}", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(blocks) => Ok(blocks),
Err(error) => {
bail!("Failed to parse blocks {start_height} (inclusive) to {end_height} (exclusive): {error}")
}
}
}
pub fn get_transaction(&self, transaction_id: N::TransactionID) -> Result<Transaction<N>> {
let url = format!("{}/{}/transaction/{transaction_id}", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(transaction) => Ok(transaction),
Err(error) => bail!("Failed to parse transaction '{transaction_id}': {error}"),
}
}
pub fn get_memory_pool_transactions(&self) -> Result<Vec<Transaction<N>>> {
let url = format!("{}/{}/memoryPool/transactions", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(transactions) => Ok(transactions),
Err(error) => bail!("Failed to parse memory pool transactions: {error}"),
}
}
pub fn get_program(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<Program<N>> {
let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
let url = format!("{}/{}/program/{program_id}", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(program) => Ok(program),
Err(error) => bail!("Failed to parse program {program_id}: {error}"),
}
}
pub fn find_block_hash(&self, transaction_id: N::TransactionID) -> Result<N::BlockHash> {
let url = format!("{}/{}/find/blockHash/{transaction_id}", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(hash) => Ok(hash),
Err(error) => bail!("Failed to parse block hash: {error}"),
}
}
pub fn find_transition_id(&self, input_or_output_id: Field<N>) -> Result<N::TransitionID> {
let url = format!("{}/{}/find/transitionID/{input_or_output_id}", self.base_url, self.network_id);
match self.client.get(&url).call()?.into_json() {
Ok(transition_id) => Ok(transition_id),
Err(error) => bail!("Failed to parse transition ID: {error}"),
}
}
pub fn scan(
&self,
view_key: impl TryInto<ViewKey<N>>,
block_heights: Range<u32>,
max_records: Option<usize>,
) -> Result<Vec<(Field<N>, Record<N, Ciphertext<N>>)>> {
let view_key = view_key.try_into().map_err(|_| anyhow!("Invalid view key"))?;
let address_x_coordinate = view_key.to_address().to_x_coordinate();
let start_block_height = block_heights.start - (block_heights.start % 50);
let end_block_height = block_heights.end + (50 - (block_heights.end % 50));
let mut records = Vec::new();
for start_height in (start_block_height..end_block_height).step_by(50) {
println!("Searching blocks {} to {} for records...", start_height, end_block_height);
if start_height >= block_heights.end {
break;
}
let end = start_height + 50;
let end_height = if end > block_heights.end { block_heights.end } else { end };
let records_iter =
self.get_blocks(start_height, end_height)?.into_iter().flat_map(|block| block.into_records());
records.extend(records_iter.filter_map(|(commitment, record)| {
match record.is_owner_with_address_x_coordinate(&view_key, &address_x_coordinate) {
true => Some((commitment, record)),
false => None,
}
}));
if records.len() >= max_records.unwrap_or(usize::MAX) {
break;
}
}
Ok(records)
}
pub fn get_unspent_records(
&self,
private_key: &PrivateKey<N>,
block_heights: Range<u32>,
max_gates: Option<u64>,
specified_amounts: Option<&Vec<u64>>,
) -> Result<Vec<(Field<N>, Record<N, Ciphertext<N>>)>> {
let view_key = ViewKey::try_from(private_key)?;
let address_x_coordinate = view_key.to_address().to_x_coordinate();
let step_size = 49;
ensure!(
block_heights.start < block_heights.end,
"The start block height must be less than the end block height"
);
let mut records = Vec::new();
let mut total_gates = 0u64;
let mut end_height = block_heights.end;
let mut start_height = block_heights.end.saturating_sub(step_size);
for _ in (block_heights.start..block_heights.end).step_by(step_size as usize) {
println!("Searching blocks {} to {} for records...", start_height, end_height);
let records_iter =
self.get_blocks(start_height, end_height)?.into_iter().flat_map(|block| block.into_records());
end_height = start_height;
start_height = start_height.saturating_sub(step_size);
if start_height < block_heights.start {
start_height = block_heights.start
};
records.extend(records_iter.filter_map(|(commitment, record)| {
match record.is_owner_with_address_x_coordinate(&view_key, &address_x_coordinate) {
true => {
let sn = Record::<N, Ciphertext<N>>::serial_number(*private_key, commitment).ok()?;
if self.find_transition_id(sn).is_err() {
if max_gates.is_some() {
let _ = record
.decrypt(&view_key)
.map(|record| {
total_gates += record.microcredits().unwrap_or(0);
record
})
.ok();
}
Some((commitment, record))
} else {
None
}
}
false => None,
}
}));
if max_gates.is_some() && total_gates > max_gates.unwrap() {
break;
}
if let Some(specified_amounts) = specified_amounts {
let found_records = specified_amounts
.iter()
.filter_map(|amount| {
let position = records.iter().position(|(_, record)| {
if let Ok(decrypted_record) = record.decrypt(&view_key) {
decrypted_record.microcredits().unwrap_or(0) > *amount
} else {
false
}
});
position.map(|index| records.remove(index))
})
.collect::<Vec<_>>();
if found_records.len() >= specified_amounts.len() {
return Ok(found_records);
}
}
}
Ok(records)
}
pub fn transaction_broadcast(&self, transaction: Transaction<N>) -> Result<String> {
let url = format!("{}/{}/transaction/broadcast", self.base_url, self.network_id);
match self.client.post(&url).send_json(&transaction) {
Ok(response) => match response.into_string() {
Ok(success_response) => Ok(success_response),
Err(error) => bail!("❌ Transaction response was malformed {}", error),
},
Err(error) => {
let error_message = match error {
ureq::Error::Status(code, response) => {
format!("(status code {code}: {:?})", response.into_string()?)
}
ureq::Error::Transport(err) => format!("({err})"),
};
match transaction {
Transaction::Deploy(..) => {
bail!("❌ Failed to deploy program to {}: {}", &url, error_message)
}
Transaction::Execute(..) => {
bail!("❌ Failed to broadcast execution to {}: {}", &url, error_message)
}
Transaction::Fee(..) => {
bail!("❌ Failed to broadcast fee execution to {}: {}", &url, error_message)
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_get_blocks() {
let client = AleoAPIClient::<Testnet3>::testnet3();
let blocks = client.get_blocks(0, 3).unwrap();
assert_eq!(blocks[0].height(), 0);
assert_eq!(blocks[1].height(), 1);
assert_eq!(blocks[2].height(), 2);
assert_eq!(blocks[1].previous_hash(), blocks[0].hash());
assert_eq!(blocks[2].previous_hash(), blocks[1].hash());
}
}