use std::{io::ErrorKind, time::Duration};
use bitcoin::{
block::Header,
consensus::{deserialize, Decodable},
BlockHash,
};
use log::*;
use crate::index;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("request failed: {0}")]
Http(#[from] ureq::Error),
#[error("reading response failed: {0}")]
Io(#[from] std::io::Error),
#[error("decoding failed: {0}")]
Decoding(#[from] bitcoin::consensus::encode::Error),
}
pub struct Client {
agent: ureq::Agent,
url: String,
}
impl Client {
pub fn new<T: Into<String>>(agent: ureq::Agent, url: T) -> Self {
Self {
agent,
url: url.into(),
}
}
fn get_bytes(&self, url: &str) -> Result<Vec<u8>, Error> {
let mut iter = 0;
let err = loop {
iter += 1;
let req = self.agent.get(url);
debug!("=> {:?}", req);
let res = req.call();
debug!("<= {:?}", res);
let err = match res {
Ok(resp) => return Ok(resp.into_body().read_to_vec()?),
Err(err) => err,
};
if iter > 100 {
break err;
}
match &err {
ureq::Error::StatusCode(503) => (),
ureq::Error::Io(e) if e.kind() == ErrorKind::ConnectionRefused => (),
_ => break err, }
warn!("unavailable {}: {:?}", url, err);
std::thread::sleep(Duration::from_secs(1));
};
error!("GET {} failed: {:?}", url, err);
Err(Error::Http(err))
}
pub fn get_blockhash_by_height(&self, height: usize) -> Result<BlockHash, Error> {
let url = format!("{}/rest/blockhashbyheight/{}.bin", self.url, height);
let data = self.get_bytes(&url)?;
Ok(deserialize(&data)?)
}
pub fn get_headers(&self, hash: BlockHash, limit: usize) -> Result<Vec<Header>, Error> {
let url = format!("{}/rest/headers/{}/{}.bin", self.url, limit + 1, hash);
let data = self.get_bytes(&url)?;
assert_eq!(data.len() % Header::SIZE, 0);
let count = data.len() / Header::SIZE;
let mut headers = Vec::with_capacity(count);
let mut r = bitcoin::io::Cursor::new(data);
for _ in 0..count {
let header = Header::consensus_decode_from_finite_reader(&mut r)?;
headers.push(header);
}
Ok(headers)
}
pub fn get_block_bytes(&self, hash: BlockHash) -> Result<index::BlockBytes, Error> {
let url = format!("{}/rest/block/{}.bin", self.url, hash);
let data = self.get_bytes(&url)?;
Ok(index::BlockBytes::new(data))
}
pub fn get_spent_bytes(&self, hash: BlockHash) -> Result<index::SpentBytes, Error> {
let url = format!("{}/rest/spenttxouts/{}.bin", self.url, hash);
let data = self.get_bytes(&url)?;
Ok(index::SpentBytes::new(data))
}
pub fn get_block_part(
&self,
hash: BlockHash,
txpos: index::TxBlockPos,
) -> Result<Vec<u8>, Error> {
let url = format!(
"{}/rest/blockpart/{}.bin?offset={}&size={}",
self.url, hash, txpos.offset, txpos.size
);
self.get_bytes(&url)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::{consensus::serialize, hashes::Hash};
use corepc_node::{exe_path, Conf, Node};
use ureq::Agent;
#[test]
fn test_client_bitcoind() {
let mut conf = Conf::default();
conf.args.push("-rest");
const BLOCKS: usize = 10;
let node = Node::with_conf(exe_path().unwrap(), &conf).unwrap();
let addr = node.client.new_address().unwrap();
node.client.generate_to_address(BLOCKS, &addr).unwrap();
let client = Client::new(Agent::new_with_defaults(), node.rpc_url());
for i in 0..=BLOCKS {
let hash = client.get_blockhash_by_height(i).unwrap();
let block = client.get_block_bytes(hash).unwrap();
let _spent = client.get_spent_bytes(hash).unwrap();
let block_size = block.len().try_into().unwrap();
let block_bytes = client
.get_block_part(
hash,
index::TxBlockPos {
offset: 0,
size: block_size,
},
)
.unwrap();
assert_eq!(block, index::BlockBytes::new(block_bytes));
assert!(client
.get_block_part(
hash,
index::TxBlockPos {
offset: 0,
size: block_size + 1
}
)
.is_err());
assert!(client
.get_block_part(
BlockHash::all_zeros(),
index::TxBlockPos {
offset: 0,
size: block_size
}
)
.is_err());
let header_bytes = client
.get_block_part(
hash,
index::TxBlockPos {
offset: 0,
size: 80,
},
)
.unwrap();
let headers = client.get_headers(hash, 0).unwrap();
let expected = node
.client
.get_block_header(&hash)
.unwrap()
.block_header()
.unwrap();
assert_eq!(headers, vec![expected]);
assert_eq!(serialize(&expected), header_bytes);
}
assert!(client.get_block_bytes(BlockHash::all_zeros()).is_err());
assert!(client.get_spent_bytes(BlockHash::all_zeros()).is_err());
}
}