use crate::http::{BinaryResponse, HttpClientError, JsonResponse};
#[cfg(feature = "rpc-client")]
use crate::rpc::RpcClientError;
use crate::utils::hex_to_work;
use crate::{BlockHeaderData, BlockSourceError};
use bitcoin::block::{Block, Header};
use bitcoin::consensus::encode;
use bitcoin::hash_types::{BlockHash, TxMerkleNode, Txid};
use bitcoin::hex::FromHex;
use bitcoin::Transaction;
use serde_json;
use bitcoin::hashes::Hash;
use std::convert::Infallible;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::io;
use std::str::FromStr;
impl TryInto<serde_json::Value> for JsonResponse {
type Error = Infallible;
fn try_into(self) -> Result<serde_json::Value, Infallible> {
Ok(self.0)
}
}
impl From<io::Error> for BlockSourceError {
fn from(e: io::Error) -> BlockSourceError {
match e.kind() {
io::ErrorKind::InvalidData => BlockSourceError::persistent(e),
io::ErrorKind::InvalidInput => BlockSourceError::persistent(e),
_ => BlockSourceError::transient(e),
}
}
}
impl From<HttpClientError> for BlockSourceError {
fn from(e: HttpClientError) -> BlockSourceError {
match e {
HttpClientError::Transport(err) => {
BlockSourceError::transient(HttpClientError::Transport(err))
},
HttpClientError::Http(http_err) => {
if (500..600).contains(&http_err.status_code) {
BlockSourceError::transient(HttpClientError::Http(http_err))
} else {
BlockSourceError::persistent(HttpClientError::Http(http_err))
}
},
HttpClientError::Parse(msg) => {
BlockSourceError::persistent(HttpClientError::Parse(msg))
},
}
}
}
#[cfg(feature = "rpc-client")]
impl From<RpcClientError> for BlockSourceError {
fn from(e: RpcClientError) -> BlockSourceError {
match e {
RpcClientError::Http(http_err) => match http_err {
HttpClientError::Transport(err) => BlockSourceError::transient(
RpcClientError::Http(HttpClientError::Transport(err)),
),
HttpClientError::Http(http) => {
if (500..600).contains(&http.status_code) {
BlockSourceError::transient(RpcClientError::Http(HttpClientError::Http(
http,
)))
} else {
BlockSourceError::persistent(RpcClientError::Http(HttpClientError::Http(
http,
)))
}
},
HttpClientError::Parse(msg) => {
BlockSourceError::persistent(RpcClientError::Http(HttpClientError::Parse(msg)))
},
},
RpcClientError::Rpc(rpc_err) => {
BlockSourceError::transient(RpcClientError::Rpc(rpc_err))
},
RpcClientError::InvalidData(msg) => {
BlockSourceError::persistent(RpcClientError::InvalidData(msg))
},
}
}
}
impl TryInto<Block> for BinaryResponse {
type Error = ();
fn try_into(self) -> Result<Block, ()> {
encode::deserialize(&self.0).map_err(|_| ())
}
}
impl TryInto<BlockHash> for BinaryResponse {
type Error = ();
fn try_into(self) -> Result<BlockHash, ()> {
BlockHash::from_slice(&self.0).map_err(|_| ())
}
}
impl TryInto<BlockHeaderData> for JsonResponse {
type Error = &'static str;
fn try_into(self) -> Result<BlockHeaderData, &'static str> {
let header = match self.0 {
serde_json::Value::Array(mut array) if !array.is_empty() => {
array.drain(..).next().unwrap()
},
serde_json::Value::Object(_) => self.0,
_ => return Err("unexpected JSON type"),
};
if !header.is_object() {
return Err("expected JSON object");
}
header.try_into().map_err(|_| "invalid header data")
}
}
impl TryFrom<serde_json::Value> for BlockHeaderData {
type Error = ();
fn try_from(response: serde_json::Value) -> Result<Self, ()> {
macro_rules! get_field {
($name: expr, $ty_access: tt) => {
response.get($name).ok_or(())?.$ty_access().ok_or(())?
};
}
Ok(BlockHeaderData {
header: Header {
version: bitcoin::block::Version::from_consensus(
get_field!("version", as_i64).try_into().map_err(|_| ())?,
),
prev_blockhash: if let Some(hash_str) = response.get("previousblockhash") {
BlockHash::from_str(hash_str.as_str().ok_or(())?).map_err(|_| ())?
} else {
BlockHash::all_zeros()
},
merkle_root: TxMerkleNode::from_str(get_field!("merkleroot", as_str))
.map_err(|_| ())?,
time: get_field!("time", as_u64).try_into().map_err(|_| ())?,
bits: bitcoin::CompactTarget::from_consensus(u32::from_be_bytes(
<[u8; 4]>::from_hex(get_field!("bits", as_str)).map_err(|_| ())?,
)),
nonce: get_field!("nonce", as_u64).try_into().map_err(|_| ())?,
},
chainwork: hex_to_work(get_field!("chainwork", as_str)).map_err(|_| ())?,
height: get_field!("height", as_u64).try_into().map_err(|_| ())?,
})
}
}
impl TryInto<Block> for JsonResponse {
type Error = &'static str;
fn try_into(self) -> Result<Block, &'static str> {
match self.0.as_str() {
None => Err("expected JSON string"),
Some(hex_data) => match Vec::<u8>::from_hex(hex_data) {
Err(_) => Err("invalid hex data"),
Ok(block_data) => match encode::deserialize(&block_data) {
Err(_) => Err("invalid block data"),
Ok(block) => Ok(block),
},
},
}
}
}
impl TryInto<(BlockHash, Option<u32>)> for JsonResponse {
type Error = &'static str;
fn try_into(self) -> Result<(BlockHash, Option<u32>), &'static str> {
if !self.0.is_object() {
return Err("expected JSON object");
}
let hash = match &self.0["bestblockhash"] {
serde_json::Value::String(hex_data) => match BlockHash::from_str(&hex_data) {
Err(_) => return Err("invalid hex data"),
Ok(block_hash) => block_hash,
},
_ => return Err("expected JSON string"),
};
let height = match &self.0["blocks"] {
serde_json::Value::Null => None,
serde_json::Value::Number(height) => match height.as_u64() {
None => return Err("invalid height"),
Some(height) => match height.try_into() {
Err(_) => return Err("invalid height"),
Ok(height) => Some(height),
},
},
_ => return Err("expected JSON number"),
};
Ok((hash, height))
}
}
impl TryInto<Txid> for JsonResponse {
type Error = String;
fn try_into(self) -> Result<Txid, String> {
let hex_data = self.0.as_str().ok_or_else(|| "expected JSON string".to_string())?;
Txid::from_str(hex_data).map_err(|err| err.to_string())
}
}
impl TryInto<Transaction> for JsonResponse {
type Error = String;
fn try_into(self) -> Result<Transaction, String> {
let hex_tx = if self.0.is_object() {
match &self.0["hex"] {
serde_json::Value::String(hex_data) => match self.0["complete"] {
serde_json::Value::Bool(x) => {
if x == false {
let reason = match &self.0["errors"][0]["error"] {
serde_json::Value::String(x) => x.as_str(),
_ => "Unknown error",
};
return Err(format!("transaction couldn't be signed. {}", reason));
} else {
hex_data
}
},
_ => hex_data,
},
_ => {
return Err("expected JSON string".to_string());
},
}
} else {
match self.0.as_str() {
Some(hex_tx) => hex_tx,
None => {
return Err("expected JSON string".to_string());
},
}
};
match Vec::<u8>::from_hex(hex_tx) {
Err(_) => Err("invalid hex data".to_string()),
Ok(tx_data) => match encode::deserialize(&tx_data) {
Err(_) => Err("invalid transaction".to_string()),
Ok(tx) => Ok(tx),
},
}
}
}
impl TryInto<BlockHash> for JsonResponse {
type Error = &'static str;
fn try_into(self) -> Result<BlockHash, &'static str> {
match self.0.as_str() {
None => Err("expected JSON string"),
Some(hex_data) if hex_data.len() != 64 => Err("invalid hash length"),
Some(hex_data) => BlockHash::from_str(hex_data).map_err(|_| "invalid hex data"),
}
}
}
#[cfg(feature = "rest-client")]
pub(crate) struct GetUtxosResponse {
pub(crate) hit_bitmap_nonempty: bool,
}
#[cfg(feature = "rest-client")]
impl TryInto<GetUtxosResponse> for JsonResponse {
type Error = &'static str;
fn try_into(self) -> Result<GetUtxosResponse, &'static str> {
let bitmap_str = self
.0
.as_object()
.ok_or("expected an object")?
.get("bitmap")
.ok_or("missing bitmap field")?
.as_str()
.ok_or("bitmap should be an str")?;
let mut hit_bitmap_nonempty = false;
for c in bitmap_str.chars() {
if c < '0' || c > '9' {
return Err("invalid byte");
}
if c > '0' {
hit_bitmap_nonempty = true;
}
}
Ok(GetUtxosResponse { hit_bitmap_nonempty })
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use bitcoin::constants::genesis_block;
use bitcoin::hashes::Hash;
use bitcoin::hex::DisplayHex;
use bitcoin::network::Network;
use serde_json::value::Number;
use serde_json::Value;
impl From<BlockHeaderData> for serde_json::Value {
fn from(data: BlockHeaderData) -> Self {
let BlockHeaderData { chainwork, height, header } = data;
serde_json::json!({
"chainwork": chainwork.to_be_bytes().as_hex().to_string(),
"height": height,
"version": header.version.to_consensus(),
"merkleroot": header.merkle_root.to_string(),
"time": header.time,
"nonce": header.nonce,
"bits": header.bits.to_consensus().to_be_bytes().as_hex().to_string(),
"previousblockhash": header.prev_blockhash.to_string(),
})
}
}
#[test]
fn into_block_header_from_json_response_with_unexpected_type() {
let response = JsonResponse(serde_json::json!(42));
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
assert_eq!(e, "unexpected JSON type");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_header_from_json_response_with_unexpected_header_type() {
let response = JsonResponse(serde_json::json!([42]));
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON object");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_header_from_json_response_with_invalid_header_response() {
let block = genesis_block(Network::Bitcoin);
let mut response = JsonResponse(
BlockHeaderData { chainwork: block.header.work(), height: 0, header: block.header }
.into(),
);
response.0["chainwork"].take();
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid header data");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_header_from_json_response_with_invalid_header_data() {
let block = genesis_block(Network::Bitcoin);
let mut response = JsonResponse(
BlockHeaderData { chainwork: block.header.work(), height: 0, header: block.header }
.into(),
);
response.0["chainwork"] = serde_json::json!("foobar");
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid header data");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_header_from_json_response_with_valid_header() {
let block = genesis_block(Network::Bitcoin);
let response = JsonResponse(
BlockHeaderData { chainwork: block.header.work(), height: 0, header: block.header }
.into(),
);
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(data) => {
assert_eq!(data.chainwork, block.header.work());
assert_eq!(data.height, 0);
assert_eq!(data.header, block.header);
},
}
}
#[test]
fn into_block_header_from_json_response_with_valid_header_array() {
let genesis_block = genesis_block(Network::Bitcoin);
let best_block_header =
Header { prev_blockhash: genesis_block.block_hash(), ..genesis_block.header };
let chainwork = genesis_block.header.work() + best_block_header.work();
let response = JsonResponse(serde_json::json!([
serde_json::Value::from(BlockHeaderData {
chainwork,
height: 1,
header: best_block_header,
}),
serde_json::Value::from(BlockHeaderData {
chainwork: genesis_block.header.work(),
height: 0,
header: genesis_block.header,
}),
]));
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(data) => {
assert_eq!(data.chainwork, chainwork);
assert_eq!(data.height, 1);
assert_eq!(data.header, best_block_header);
},
}
}
#[test]
fn into_block_header_from_json_response_without_previous_block_hash() {
let block = genesis_block(Network::Bitcoin);
let mut response = JsonResponse(
BlockHeaderData { chainwork: block.header.work(), height: 0, header: block.header }
.into(),
);
response.0.as_object_mut().unwrap().remove("previousblockhash");
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(BlockHeaderData { chainwork: _, height: _, header }) => {
assert_eq!(header, block.header);
},
}
}
#[test]
fn into_block_from_invalid_binary_response() {
let response = BinaryResponse(b"foo".to_vec());
match TryInto::<Block>::try_into(response) {
Err(_) => {},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_from_valid_binary_response() {
let genesis_block = genesis_block(Network::Bitcoin);
let response = BinaryResponse(encode::serialize(&genesis_block));
match TryInto::<Block>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(block) => assert_eq!(block, genesis_block),
}
}
#[test]
fn into_block_from_json_response_with_unexpected_type() {
let response = JsonResponse(serde_json::json!({ "result": "foo" }));
match TryInto::<Block>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_from_json_response_with_invalid_hex_data() {
let response = JsonResponse(serde_json::json!("foobar"));
match TryInto::<Block>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid hex data");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_from_json_response_with_invalid_block_data() {
let response = JsonResponse(serde_json::json!("abcd"));
match TryInto::<Block>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid block data");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_from_json_response_with_valid_block_data() {
let genesis_block = genesis_block(Network::Bitcoin);
let response = JsonResponse(serde_json::json!(encode::serialize_hex(&genesis_block)));
match TryInto::<Block>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(block) => assert_eq!(block, genesis_block),
}
}
#[test]
fn into_block_hash_from_json_response_with_unexpected_type() {
let response = JsonResponse(serde_json::json!("foo"));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON object");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_hash_from_json_response_with_unexpected_bestblockhash_type() {
let response = JsonResponse(serde_json::json!({ "bestblockhash": 42 }));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_hash_from_json_response_with_invalid_hex_data() {
let response = JsonResponse(serde_json::json!({ "bestblockhash": "foobar"} ));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid hex data");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_hash_from_json_response_without_height() {
let block = genesis_block(Network::Bitcoin);
let response = JsonResponse(serde_json::json!({
"bestblockhash": block.block_hash().to_string(),
}));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok((hash, height)) => {
assert_eq!(hash, block.block_hash());
assert!(height.is_none());
},
}
}
#[test]
fn into_block_hash_from_json_response_with_unexpected_blocks_type() {
let block = genesis_block(Network::Bitcoin);
let response = JsonResponse(serde_json::json!({
"bestblockhash": block.block_hash().to_string(),
"blocks": "foo",
}));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON number");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_hash_from_json_response_with_invalid_height() {
let block = genesis_block(Network::Bitcoin);
let response = JsonResponse(serde_json::json!({
"bestblockhash": block.block_hash().to_string(),
"blocks": std::u64::MAX,
}));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid height");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_block_hash_from_json_response_with_height() {
let block = genesis_block(Network::Bitcoin);
let response = JsonResponse(serde_json::json!({
"bestblockhash": block.block_hash().to_string(),
"blocks": 1,
}));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok((hash, height)) => {
assert_eq!(hash, block.block_hash());
assert_eq!(height.unwrap(), 1);
},
}
}
#[test]
fn into_txid_from_json_response_with_unexpected_type() {
let response = JsonResponse(serde_json::json!({ "result": "foo" }));
match TryInto::<Txid>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_txid_from_json_response_with_invalid_hex_data() {
let response = JsonResponse(serde_json::json!("foobar"));
match TryInto::<Txid>::try_into(response) {
Err(e) => {
assert_eq!(e, "failed to parse hex");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_txid_from_json_response_with_invalid_txid_data() {
let response = JsonResponse(serde_json::json!("abcd"));
match TryInto::<Txid>::try_into(response) {
Err(e) => {
assert_eq!(e, "failed to parse hex");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_txid_from_json_response_with_valid_txid_data() {
let target_txid = Txid::from_slice(&[1; 32]).unwrap();
let response = JsonResponse(serde_json::json!(encode::serialize_hex(&target_txid)));
match TryInto::<Txid>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(txid) => assert_eq!(txid, target_txid),
}
}
#[test]
fn into_txid_from_bitcoind_rpc_json_response() {
let mut rpc_response = serde_json::json!(
{"error": "", "id": "770", "result": "7934f775149929a8b742487129a7c3a535dfb612f0b726cc67bc10bc2628f906"}
);
let r: Result<Txid, String> =
JsonResponse(rpc_response.get_mut("result").unwrap().take()).try_into();
assert_eq!(
r.unwrap().to_string(),
"7934f775149929a8b742487129a7c3a535dfb612f0b726cc67bc10bc2628f906"
);
}
#[test]
fn into_tx_from_json_response_with_invalid_hex_data() {
let response = JsonResponse(serde_json::json!("foobar"));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid hex data");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_tx_from_json_response_with_invalid_data_type() {
let response = JsonResponse(Value::Number(Number::from_f64(1.0).unwrap()));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_tx_from_json_response_with_invalid_tx_data() {
let response = JsonResponse(serde_json::json!("abcd"));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
assert_eq!(e, "invalid transaction");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_tx_from_json_response_with_valid_tx_data_plain() {
let genesis_block = genesis_block(Network::Bitcoin);
let target_tx = genesis_block.txdata.get(0).unwrap();
let response = JsonResponse(serde_json::json!(encode::serialize_hex(&target_tx)));
match TryInto::<Transaction>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(tx) => assert_eq!(&tx, target_tx),
}
}
#[test]
fn into_tx_from_json_response_with_valid_tx_data_hex_field() {
let genesis_block = genesis_block(Network::Bitcoin);
let target_tx = genesis_block.txdata.get(0).unwrap();
let response =
JsonResponse(serde_json::json!({ "hex": encode::serialize_hex(&target_tx) }));
match TryInto::<Transaction>::try_into(response) {
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(tx) => assert_eq!(&tx, target_tx),
}
}
#[test]
fn into_tx_from_json_response_with_no_hex_field() {
let response = JsonResponse(serde_json::json!({ "error": "foo" }));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn into_tx_from_json_response_not_signed() {
let response = JsonResponse(serde_json::json!({ "hex": "foo", "complete": false }));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
assert!(e.contains("transaction couldn't be signed"));
},
Ok(_) => panic!("Expected error"),
}
}
}