bitcoin_rest/
lib.rs

1//! bitcoin_rest - A Bitcoin Core REST API wrapper library for Rust.
2//! 
3//! This library calls the [Bitcoin Core's REST API endpoint](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md) and
4//! converts them to [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin) objects.
5//! 
6//! For details, please see [Context](./struct.Context.html).
7
8#[cfg(feature="softforks")]
9use std::collections::HashMap;
10pub use bytes;
11pub use serde;
12pub use reqwest;
13pub use bitcoin;
14use serde::{Deserialize, Serialize};
15use bitcoin::hash_types::{BlockHash, Txid};
16use bitcoin::blockdata::block::{Block, BlockHeader};
17use bitcoin::blockdata::transaction::Transaction;
18use bitcoin::consensus::Decodable;
19
20pub const DEFAULT_ENDPOINT: &str = "http://localhost:8332/rest";
21
22#[derive(Debug, Clone, Deserialize, Serialize)]
23pub struct Softfork {
24    #[serde(rename="type")]
25    pub type_: String,
26    pub active: bool,
27    #[serde(default)]
28    pub height: u32,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
32pub struct ChainInfo {
33    pub chain: String,
34    pub blocks: u32,
35    pub headers: u32,
36    pub bestblockhash: String,
37    pub difficulty: f64,
38    pub mediantime: u32,
39    pub verificationprogress: f64,
40    pub chainwork: String,
41    pub pruned: bool,
42    #[serde(default)]
43    pub pruneheight: u32,
44    #[cfg(feature="softforks")]
45    pub softforks: HashMap<String, Softfork>,
46    pub warnings: String,
47}
48
49#[derive(Debug, Clone, Deserialize, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct ScriptPubKey {
52    pub asm: String,
53    pub hex: String,
54    #[serde(default)]
55    pub req_sigs: u32,
56    #[serde(rename="type")]
57    pub type_: String,
58    #[serde(default)]
59    pub addresses: Vec<String>,
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct Utxo {
65    pub height: u32,
66    pub value: f64,
67    pub script_pub_key: ScriptPubKey,
68}
69
70#[derive(Debug, Clone, Deserialize, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct UtxoData {
73    pub chain_height: u32,
74    pub chaintip_hash: String,
75    pub bitmap: String,
76    pub utxos: Vec<Utxo>,
77}
78
79#[derive(Debug)]
80pub enum Error {
81    Reqwest(reqwest::Error),
82    BitcoinEncodeError(bitcoin::consensus::encode::Error)
83}
84
85impl From<reqwest::Error> for Error {
86    fn from(err: reqwest::Error) -> Self {
87        Self::Reqwest(err)
88    }
89}
90
91impl From<bitcoin::consensus::encode::Error> for Error {
92    fn from(err: bitcoin::consensus::encode::Error) -> Self {
93        Self::BitcoinEncodeError(err)
94    }
95}
96
97/// `bitcoin_rest` context.
98#[derive(Debug, Clone)]
99pub struct Context {
100    endpoint: String,
101    client: reqwest::Client,
102}
103
104/// Create a new `bitcoin_rest` context.
105///
106/// The `endpoint` will be the string like "http://localhost:8332/rest"
107/// (Note: this string is available via `bitcoin_rest::DEFAULT_ENDPOINT`).
108pub fn new(endpoint: &str) -> Context {
109    Context {
110        endpoint: endpoint.to_string(),
111        client: reqwest::Client::new(),
112    }
113}
114
115impl Context {
116    /// Call the REST endpoint and parse it as a JSON.
117    pub async fn call_json<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, reqwest::Error> {
118        let url = format!("{}/{}.json", &self.endpoint, path);
119        let result = self.client.get(url)
120            .send().await?
121            .json::<T>().await?;
122        Ok(result)
123    }
124    /// Call the REST endpoint (binary).
125    pub async fn call_bin(&self, path: &str) -> Result<bytes::Bytes, reqwest::Error> {
126        let url = format!("{}/{}.bin", &self.endpoint, path);
127        let result = self.client.get(url)
128            .send().await?
129            .bytes().await?;
130        Ok(result)
131    }
132    /// Call the REST endpoint (hex).
133    pub async fn call_hex(&self, path: &str) -> Result<String, reqwest::Error> {
134        let url = format!("{}/{}.hex", &self.endpoint, path);
135        let mut result = self.client.get(url)
136            .send().await?
137            .text().await?;
138        // Trim last '\n'.
139        result.pop();
140        Ok(result)
141    }
142    /// Call the [/tx](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md#transactions) endpoint.
143    pub async fn tx(&self, txhash: &Txid) -> Result<Transaction, Error> {
144        let result = self.call_bin(&["tx", &txhash.to_string()].join("/")).await?;
145        Ok(Transaction::consensus_decode(result.as_ref())?)
146    }
147    /// Call the [/block](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md#blocks) endpoint.
148    pub async fn block(&self, blockhash: &BlockHash) -> Result<Block, Error> {
149        let result = self.call_bin(&["block", &blockhash.to_string()].join("/")).await?;
150        Ok(Block::consensus_decode(result.as_ref())?)
151    }
152    /// Call the [/block/notxdetails](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md#blocks) endpoint.
153    pub async fn block_notxdetails(&self, blockhash: &BlockHash) -> Result<BlockHeader, Error> {
154        let result = self.call_bin(&["block", "notxdetails", &blockhash.to_string()].join("/")).await?;
155        Ok(BlockHeader::consensus_decode(result.as_ref())?)
156    }
157    /// Call the [/headers](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md#blockheaders) endpoint.
158    pub async fn headers(&self, count: u32, blockhash: &BlockHash) -> Result<Vec<BlockHeader>, Error> {
159        let result = self.call_bin(&["headers", &count.to_string(), &blockhash.to_string()].join("/")).await?;
160        let mut ret = Vec::new();
161        const BLOCK_HEADER_SIZE: usize = 80usize;
162        let mut offset = 0;
163        while offset < result.len() {
164            ret.push(BlockHeader::consensus_decode(result[offset..(offset+BLOCK_HEADER_SIZE)].as_ref())?);
165            offset += BLOCK_HEADER_SIZE;
166        }
167        Ok(ret)
168    }
169    /// Call the [/blockhashbyheight](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md#blockhash-by-height) endpoint.
170    pub async fn blockhashbyheight(&self, height: u32) -> Result<BlockHash, Error> {
171        let result = self.call_bin(&["blockhashbyheight", &height.to_string()].join("/")).await?;
172        Ok(BlockHash::consensus_decode(result.as_ref())?)
173    }
174    /// Call the [/chaininfo](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md#chaininfo) endpoint.
175    pub async fn chaininfo(&self) -> Result<ChainInfo, Error> {
176        let result: ChainInfo = self.call_json("chaininfo").await?;
177        Ok(result)
178    }
179    /// Call the [/getutxos](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md#query-utxo-set) endpoint.
180    pub async fn getutxos(&self, checkmempool: bool, txids: &[Txid]) -> Result<UtxoData, Error> {
181        let mut path = Vec::with_capacity(1 + if checkmempool { 0 } else { 1 } + txids.len());
182        path.push("getutxos".to_string());
183        if checkmempool {
184            path.push("checkmempool".to_string());
185        }
186        for (i, txid) in txids.iter().enumerate() {
187            path.push([txid.to_string(), i.to_string()].join("-"));
188        }
189        let result: UtxoData = self.call_json(&path.join("/")).await?;
190        Ok(result)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use std::str::FromStr;
197    use super::*;
198    #[tokio::test]
199    async fn reqwest_fail() {
200        let rest = new("http://invalid-url");
201        assert!(rest.blockhashbyheight(0).await.is_err());
202    }
203    struct Fixture {
204        rest_env_name: &'static str,
205        genesis_block_hash: &'static str,
206        txid_coinbase_block1: &'static str,
207    }
208    async fn decode_fail(f: &Fixture) {
209        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
210        let rest = new(&test_endpoint);
211        assert!(rest.blockhashbyheight(0xFFFFFFFF).await.is_err());
212    }
213    async fn tx(f: &Fixture) {
214        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
215        let rest = new(&test_endpoint);
216        let tx = rest.tx(&Txid::from_str(f.txid_coinbase_block1).unwrap()).await.unwrap();
217        assert_eq!(tx.txid().to_string(), f.txid_coinbase_block1);
218    }
219    async fn block(f: &Fixture) {
220        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
221        let rest = new(&test_endpoint);
222        let blockid = BlockHash::from_str(f.genesis_block_hash).unwrap();
223        let block = rest.block(&blockid).await.unwrap();
224        assert_eq!(block.block_hash().to_string(), f.genesis_block_hash);
225    }
226    async fn block_notxdetails(f: &Fixture) {
227        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
228        let rest = new(&test_endpoint);
229        let blockid = BlockHash::from_str(f.genesis_block_hash).unwrap();
230        let blockheader = rest.block_notxdetails(&blockid).await.unwrap();
231        assert_eq!(blockheader.block_hash().to_string(), f.genesis_block_hash);
232    }
233    async fn headers(f: &Fixture) {
234        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
235        let rest = new(&test_endpoint);
236        let blockid = BlockHash::from_str(f.genesis_block_hash).unwrap();
237        let headers = rest.headers(1, &blockid).await.unwrap();
238        assert_eq!(headers[0].block_hash().to_string(), f.genesis_block_hash);
239    }
240    async fn chaininfo(f: &Fixture) {
241        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
242        let rest = new(&test_endpoint);
243        let chaininfo = rest.chaininfo().await.unwrap();
244        assert_eq!(chaininfo.chain, "main");
245    }
246    async fn blockhashbyheight(f: &Fixture) {
247        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
248        let rest = new(&test_endpoint);
249        assert_eq!(rest.blockhashbyheight(0).await.unwrap().to_string(), f.genesis_block_hash);
250    }
251    async fn blockhashbyheight_hex(f: &Fixture) {
252        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
253        let rest = new(&test_endpoint);
254        let blockhash_hex = rest.call_hex("blockhashbyheight/0").await.unwrap();
255        let blockhash = BlockHash::from_str(&blockhash_hex).unwrap();
256        assert_eq!(blockhash.to_string(), f.genesis_block_hash);
257    }
258    async fn utxos(f: &Fixture) {
259        let test_endpoint = std::env::var(f.rest_env_name).unwrap_or(DEFAULT_ENDPOINT.to_string());
260        let rest = new(&test_endpoint);
261        let utxos = rest.getutxos(true, &vec![
262            Txid::from_str(f.txid_coinbase_block1).unwrap(),
263        ]).await.unwrap();
264        assert!(utxos.chain_height > 0);
265    }
266    const BTC: Fixture = Fixture {
267        rest_env_name: "BITCOIN_REST_ENDPOINT",
268        genesis_block_hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
269        txid_coinbase_block1: "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
270    };
271    #[tokio::test] async fn btc_decode_fail          () { decode_fail          (&BTC).await; }
272    #[tokio::test] async fn btc_tx                   () { tx                   (&BTC).await; }
273    #[tokio::test] async fn btc_block                () { block                (&BTC).await; }
274    #[tokio::test] async fn btc_block_notxdetails    () { block_notxdetails    (&BTC).await; }
275    #[tokio::test] async fn btc_headers              () { headers              (&BTC).await; }
276    #[tokio::test] async fn btc_chaininfo            () { chaininfo            (&BTC).await; }
277    #[tokio::test] async fn btc_blockhashbyheight    () { blockhashbyheight    (&BTC).await; }
278    #[tokio::test] async fn btc_blockhashbyheight_hex() { blockhashbyheight_hex(&BTC).await; }
279    #[tokio::test] async fn btc_utxos                () { utxos                (&BTC).await; }
280    const MONA: Fixture = Fixture {
281        rest_env_name: "MONACOIN_REST_ENDPOINT",
282        genesis_block_hash: "ff9f1c0116d19de7c9963845e129f9ed1bfc0b376eb54fd7afa42e0d418c8bb6",
283        txid_coinbase_block1: "10067abeabcd96a1261bc542b16d686d083308304923d74cb8f3bab4209cc3b9",
284    };
285    #[tokio::test] async fn mona_decode_fail      () { decode_fail      (&MONA).await; }
286    #[tokio::test] async fn mona_tx               () { tx               (&MONA).await; }
287    #[tokio::test] async fn mona_block            () { block            (&MONA).await; }
288    #[tokio::test] async fn mona_block_notxdetails() { block_notxdetails(&MONA).await; }
289    #[tokio::test] async fn mona_headers          () { headers          (&MONA).await; }
290    #[cfg(not(feature="softforks"))]
291    #[tokio::test] async fn mona_chaininfo        () { chaininfo        (&MONA).await; }
292    //#[tokio::test] async fn mona_blockhashbyheight() { blockhashbyheight(&MONA).await; }
293    #[tokio::test] async fn mona_utxos            () { utxos            (&MONA).await; }
294}