1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
use std::convert::TryInto;
use std::env;
use std::fs::read_to_string;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;

use crate::{bitcoin_network_path, Error, Explorer};

use async_trait::async_trait;
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::{Block, BlockHash, Work};
use bitcoin::Transaction;
use bitcoin::blockdata::block::Header as BlockHeader;
use bitcoin::{Network, OutPoint};
use jsonrpc_async::error::Error::Rpc;
use jsonrpc_async::simple_http::SimpleHttpTransport;
use jsonrpc_async::Client;
use log::{self, error, info};
use serde;
use serde_json::{json, Value};
use tokio::sync::Mutex;
use url::Url;

use crate::convert::{BlockchainInfo, JsonResponse};

/// Async client for RPC to bitcoin core daemon
#[derive(Clone, Debug)]
pub struct BitcoindClient {
    rpc: Arc<Mutex<Client>>,
    url: Url,
}

/// BitcoindClient Error
pub type BitcoindClientResult<T> = Result<T, Error>;

impl BitcoindClient {
    /// Create a new BitcoindClient
    pub async fn new(url: Url) -> Self {
        let mut builder = SimpleHttpTransport::builder()
            .url(&url.to_string())
            .await
            .unwrap();
        if let Ok(timeout_secs_str) = env::var("BITCOIND_CLIENT_TIMEOUT_SECS") {
            let timeout_secs = timeout_secs_str
                .parse::<u64>()
                .expect("BITCOIND_CLIENT_TIMEOUT_SECS not valid");
            info!("using timeout of {} seconds", timeout_secs);
            builder = builder.timeout(Duration::from_secs(timeout_secs));
        };
        // sadly, SimpleHttpTransport doesn't grab the auth from the URL
        if !url.username().is_empty() {
            builder = builder.auth(url.username(), url.password());
        }
        let rpc = Client::with_transport(builder.build());
        let client = Self {
            rpc: Arc::new(Mutex::new(rpc)),
            url,
        };
        client
    }

    /// Make a getblockchaininfo RPC call
    pub async fn get_blockchain_info(&self) -> BitcoindClientResult<BlockchainInfo> {
        let result = self.call_into("getblockchaininfo", &[]).await;
        Ok(result?)
    }

    /// Perform an RPC call and deserialize the result
    pub async fn call<T: for<'a> serde::de::Deserialize<'a>>(
        &self,
        cmd: &str,
        args: &[Value],
    ) -> Result<T, Error> {
        let rpc = self.rpc.lock().await;
        let v_args: Vec<_> = args
            .iter()
            .map(serde_json::value::to_raw_value)
            .collect::<Result<_, serde_json::Error>>()?;
        let req = rpc.build_request(cmd, &v_args[..]);
        log::trace!("JSON-RPC request: {} {}", cmd, Value::from(args));

        let res = rpc.send_request(req).await;
        let resp = res.map_err(Error::from);
        if let Err(ref err) = resp {
            error!("{}: {} {}", cmd, self.url, err);
        }
        // log_response(cmd, &resp);
        Ok(resp?.result()?)
    }

    /// Perform an RPC call and deserialize a JSON response
    pub async fn call_into<T>(&self, cmd: &str, args: &[Value]) -> Result<T, Error>
    where
        JsonResponse: TryInto<T, Error = std::io::Error>,
    {
        let value: Value = self.call(cmd, args).await?;
        Ok(JsonResponse(value).try_into()?)
    }
}

/// BlockSource Error
pub type BlockSourceResult<T> = Result<T, Error>;

/// Abstract type for retrieving block headers and data.
#[async_trait]
pub trait BlockSource: Sync + Send {
    /// Returns the header for a given hash
    async fn get_header(&self, header_hash: &BlockHash) -> BlockSourceResult<BlockHeaderData>;

    /// Returns the block for a given hash.
    async fn get_block(&self, header_hash: &BlockHash) -> BlockSourceResult<Block>;

    /// Returns hash of block in best-block-chain at height provided.
    async fn get_block_hash(&self, height: u32) -> BlockSourceResult<Option<BlockHash>>;

    /// Returns the hash of the best block and, optionally, its height.
    ///
    /// When polling a block source, [`Poll`] implementations may pass the height to [`get_header`]
    /// to allow for a more efficient lookup.
    ///
    /// [`get_header`]: Self::get_header
    async fn get_best_block(&self) -> BlockSourceResult<(BlockHash, u32)>;
}

/// A block header and some associated data. This information should be available from most block
/// sources (and, notably, is available in Bitcoin Core's RPC and REST interfaces).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BlockHeaderData {
    /// The block header itself.
    pub header: BlockHeader,

    /// The block height where the genesis block has height 0.
    pub height: u32,

    /// The total chain work in expected number of double-SHA256 hashes required to build a chain
    /// of equivalent weight.
    pub chainwork: Work,
}

#[async_trait]
impl BlockSource for BitcoindClient {
    async fn get_header(&self, header_hash: &BlockHash) -> BlockSourceResult<BlockHeaderData> {
        Ok(self
            .call_into("getblockheader", &[json!(header_hash.to_string())])
            .await?)
    }

    async fn get_block(&self, header_hash: &BlockHash) -> BlockSourceResult<Block> {
        Ok(self
            .call_into("getblock", &[json!(header_hash.to_string()), json!(0)])
            .await?)
    }

    async fn get_block_hash(&self, height: u32) -> BlockSourceResult<Option<BlockHash>> {
        let result = self.call_into("getblockhash", &[json!(height)]).await;
        match result {
            Ok(r) => Ok(r),
            Err(e) => match e {
                Error::JsonRpc(Rpc(ref rpce)) => {
                    if rpce.code == -8 {
                        Ok(None)
                    } else {
                        Err(e)
                    }
                }
                _ => Err(e),
            },
        }
    }

    async fn get_best_block(&self) -> BlockSourceResult<(BlockHash, u32)> {
        let info = self.get_blockchain_info().await?;
        Ok((info.latest_blockhash, info.latest_height as u32))
    }
}

#[async_trait]
impl Explorer for BitcoindClient {
    async fn get_utxo_confirmations(&self, txout: &OutPoint) -> BitcoindClientResult<Option<u64>> {
        let value: Value = self
            .call("gettxout", &[json!(txout.txid.to_string()), json!(txout.vout)])
            .await?;
        if value.is_null() {
            Ok(None)
        } else {
            let confirmations = value["confirmations"].as_u64().unwrap();
            Ok(Some(confirmations))
        }
    }

    async fn broadcast_transaction(&self, tx: &bitcoin::Transaction) -> BitcoindClientResult<()> {
        let tx_hex = serialize_hex(tx);
        let _: Value = self.call("sendrawtransaction", &[json!(tx_hex)]).await?;
        Ok(())
    }

    /// Get the transaction that spends the given outpoint, if exists in chain
    async fn get_utxo_spending_tx(&self, _txout: &OutPoint) -> Result<Option<Transaction>, Error> {
        unimplemented!("get_utxo_spending_tx")
    }
}

fn bitcoin_rpc_cookie(network: Network) -> (String, String) {
    let home = env::var("HOME").expect("cannot get cookie file if HOME is not set");
    let bitcoin_path = Path::new(&home).join(".bitcoin");
    let bitcoin_net_path = bitcoin_network_path(bitcoin_path, network);
    let cookie_path = bitcoin_net_path.join(".cookie");
    info!(
        "auth to bitcoind via cookie {}",
        cookie_path.to_string_lossy()
    );
    let cookie_contents = read_to_string(cookie_path).expect("cookie file read");
    let mut iter = cookie_contents.splitn(2, ":");
    (
        iter.next().expect("cookie user").to_string(),
        iter.next().expect("cookie pass").to_string(),
    )
}

/// Construct a client from an RPC URL and a network
pub async fn bitcoind_client_from_url(mut url: Url, network: Network) -> BitcoindClient {
    if url.username().is_empty() {
        // try to get from cookie file
        let (user, pass) = bitcoin_rpc_cookie(network);
        url.set_username(&user).expect("set user");
        url.set_password(Some(&pass)).expect("set pass");
    }
    BitcoindClient::new(url).await
}