#[cfg(feature="softforks")]
use std::collections::HashMap;
pub use bytes;
pub use serde;
pub use reqwest;
pub use bitcoin;
use serde::{Deserialize, Serialize};
use bitcoin::hash_types::{BlockHash, Txid};
use bitcoin::blockdata::block::{Block, BlockHeader};
use bitcoin::blockdata::transaction::Transaction;
use bitcoin::consensus::Decodable;
pub const DEFAULT_ENDPOINT: &str = "http://localhost:8332/rest";
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Softfork {
#[serde(rename="type")]
pub type_: String,
pub active: bool,
#[serde(default)]
pub height: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChainInfo {
pub chain: String,
pub blocks: u32,
pub headers: u32,
pub bestblockhash: String,
pub difficulty: f64,
pub mediantime: u32,
pub verificationprogress: f64,
pub chainwork: String,
pub pruned: bool,
#[serde(default)]
pub pruneheight: u32,
#[cfg(feature="softforks")]
pub softforks: HashMap<String, Softfork>,
pub warnings: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ScriptPubKey {
pub asm: String,
pub hex: String,
#[serde(default)]
pub req_sigs: u32,
#[serde(rename="type")]
pub type_: String,
#[serde(default)]
pub addresses: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Utxo {
pub height: u32,
pub value: f64,
pub script_pub_key: ScriptPubKey,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UtxoData {
pub chain_height: u32,
pub chaintip_hash: String,
pub bitmap: String,
pub utxos: Vec<Utxo>,
}
#[derive(Debug)]
pub enum Error {
Reqwest(reqwest::Error),
BitcoinEncodeError(bitcoin::consensus::encode::Error)
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Self::Reqwest(err)
}
}
impl From<bitcoin::consensus::encode::Error> for Error {
fn from(err: bitcoin::consensus::encode::Error) -> Self {
Self::BitcoinEncodeError(err)
}
}
#[derive(Debug, Clone)]
pub struct Context {
endpoint: String,
client: reqwest::Client,
}
pub fn new(endpoint: &str) -> Context {
Context {
endpoint: endpoint.to_string(),
client: reqwest::Client::new(),
}
}
impl Context {
pub async fn call_json<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, reqwest::Error> {
let url = format!("{}/{}.json", &self.endpoint, path);
let result = self.client.get(url)
.send().await?
.json::<T>().await?;
Ok(result)
}
pub async fn call_bin(&self, path: &str) -> Result<bytes::Bytes, reqwest::Error> {
let url = format!("{}/{}.bin", &self.endpoint, path);
let result = self.client.get(url)
.send().await?
.bytes().await?;
Ok(result)
}
pub async fn call_hex(&self, path: &str) -> Result<String, reqwest::Error> {
let url = format!("{}/{}.hex", &self.endpoint, path);
let mut result = self.client.get(url)
.send().await?
.text().await?;
result.pop();
Ok(result)
}
pub async fn tx(&self, txhash: &Txid) -> Result<Transaction, Error> {
let result = self.call_bin(&["tx", &txhash.to_string()].join("/")).await?;
Ok(Transaction::consensus_decode(result.as_ref())?)
}
pub async fn block(&self, blockhash: &BlockHash) -> Result<Block, Error> {
let result = self.call_bin(&["block", &blockhash.to_string()].join("/")).await?;
Ok(Block::consensus_decode(result.as_ref())?)
}
pub async fn block_notxdetails(&self, blockhash: &BlockHash) -> Result<BlockHeader, Error> {
let result = self.call_bin(&["block", "notxdetails", &blockhash.to_string()].join("/")).await?;
Ok(BlockHeader::consensus_decode(result.as_ref())?)
}
pub async fn headers(&self, count: u32, blockhash: &BlockHash) -> Result<Vec<BlockHeader>, Error> {
let result = self.call_bin(&["headers", &count.to_string(), &blockhash.to_string()].join("/")).await?;
let mut ret = Vec::new();
const BLOCK_HEADER_SIZE: usize = 80usize;
let mut offset = 0;
while offset < result.len() {
ret.push(BlockHeader::consensus_decode(result[offset..(offset+BLOCK_HEADER_SIZE)].as_ref())?);
offset += BLOCK_HEADER_SIZE;
}
Ok(ret)
}
pub async fn blockhashbyheight(&self, height: u32) -> Result<BlockHash, Error> {
let result = self.call_bin(&["blockhashbyheight", &height.to_string()].join("/")).await?;
Ok(BlockHash::consensus_decode(result.as_ref())?)
}
pub async fn chaininfo(&self) -> Result<ChainInfo, Error> {
let result: ChainInfo = self.call_json("chaininfo").await?;
Ok(result)
}
pub async fn getutxos(&self, checkmempool: bool, txids: &[Txid]) -> Result<UtxoData, Error> {
let mut path = Vec::with_capacity(1 + if checkmempool { 0 } else { 1 } + txids.len());
path.push("getutxos".to_string());
if checkmempool {
path.push("checkmempool".to_string());
}
for (i, txid) in txids.iter().enumerate() {
path.push([txid.to_string(), i.to_string()].join("-"));
}
let result: UtxoData = self.call_json(&path.join("/")).await?;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[tokio::test]
async fn reqwest_fail() {
let rest = new("http://invalid-url");
assert!(rest.blockhashbyheight(0).await.is_err());
}
struct Fixture {
rest_env_name: &'static str,
genesis_block_hash: &'static str,
txid_coinbase_block1: &'static str,
}
async fn decode_fail(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
assert!(rest.blockhashbyheight(0xFFFFFFFF).await.is_err());
}
async fn tx(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
let tx = rest.tx(&Txid::from_str(f.txid_coinbase_block1).unwrap()).await.unwrap();
assert_eq!(tx.txid().to_string(), f.txid_coinbase_block1);
}
async fn block(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
let blockid = BlockHash::from_str(f.genesis_block_hash).unwrap();
let block = rest.block(&blockid).await.unwrap();
assert_eq!(block.block_hash().to_string(), f.genesis_block_hash);
}
async fn block_notxdetails(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
let blockid = BlockHash::from_str(f.genesis_block_hash).unwrap();
let blockheader = rest.block_notxdetails(&blockid).await.unwrap();
assert_eq!(blockheader.block_hash().to_string(), f.genesis_block_hash);
}
async fn headers(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
let blockid = BlockHash::from_str(f.genesis_block_hash).unwrap();
let headers = rest.headers(1, &blockid).await.unwrap();
assert_eq!(headers[0].block_hash().to_string(), f.genesis_block_hash);
}
async fn chaininfo(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
let chaininfo = rest.chaininfo().await.unwrap();
assert_eq!(chaininfo.chain, "main");
}
async fn blockhashbyheight(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
assert_eq!(rest.blockhashbyheight(0).await.unwrap().to_string(), f.genesis_block_hash);
}
async fn blockhashbyheight_hex(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
let blockhash_hex = rest.call_hex("blockhashbyheight/0").await.unwrap();
let blockhash = BlockHash::from_str(&blockhash_hex).unwrap();
assert_eq!(blockhash.to_string(), f.genesis_block_hash);
}
async fn utxos(f: &Fixture) {
let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
let rest = new(&test_endpoint);
let utxos = rest.getutxos(true, &vec![
Txid::from_str(f.txid_coinbase_block1).unwrap(),
]).await.unwrap();
assert!(utxos.chain_height > 0);
}
const BTC: Fixture = Fixture {
rest_env_name: "BITCOIN_REST_ENDPOINT",
genesis_block_hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
txid_coinbase_block1: "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
};
#[tokio::test] async fn btc_decode_fail () { decode_fail (&BTC).await; }
#[tokio::test] async fn btc_tx () { tx (&BTC).await; }
#[tokio::test] async fn btc_block () { block (&BTC).await; }
#[tokio::test] async fn btc_block_notxdetails () { block_notxdetails (&BTC).await; }
#[tokio::test] async fn btc_headers () { headers (&BTC).await; }
#[tokio::test] async fn btc_chaininfo () { chaininfo (&BTC).await; }
#[tokio::test] async fn btc_blockhashbyheight () { blockhashbyheight (&BTC).await; }
#[tokio::test] async fn btc_blockhashbyheight_hex() { blockhashbyheight_hex(&BTC).await; }
#[tokio::test] async fn btc_utxos () { utxos (&BTC).await; }
const MONA: Fixture = Fixture {
rest_env_name: "MONACOIN_REST_ENDPOINT",
genesis_block_hash: "ff9f1c0116d19de7c9963845e129f9ed1bfc0b376eb54fd7afa42e0d418c8bb6",
txid_coinbase_block1: "10067abeabcd96a1261bc542b16d686d083308304923d74cb8f3bab4209cc3b9",
};
#[tokio::test] async fn mona_decode_fail () { decode_fail (&MONA).await; }
#[tokio::test] async fn mona_tx () { tx (&MONA).await; }
#[tokio::test] async fn mona_block () { block (&MONA).await; }
#[tokio::test] async fn mona_block_notxdetails() { block_notxdetails(&MONA).await; }
#[tokio::test] async fn mona_headers () { headers (&MONA).await; }
#[cfg(not(feature="softforks"))]
#[tokio::test] async fn mona_chaininfo () { chaininfo (&MONA).await; }
#[tokio::test] async fn mona_utxos () { utxos (&MONA).await; }
}