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};
#[derive(Clone, Debug)]
pub struct BitcoindClient {
rpc: Arc<Mutex<Client>>,
url: Url,
}
pub type BitcoindClientResult<T> = Result<T, Error>;
impl 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));
};
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
}
pub async fn get_blockchain_info(&self) -> BitcoindClientResult<BlockchainInfo> {
let result = self.call_into("getblockchaininfo", &[]).await;
Ok(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);
}
Ok(resp?.result()?)
}
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()?)
}
}
pub type BlockSourceResult<T> = Result<T, Error>;
#[async_trait]
pub trait BlockSource: Sync + Send {
async fn get_header(&self, header_hash: &BlockHash) -> BlockSourceResult<BlockHeaderData>;
async fn get_block(&self, header_hash: &BlockHash) -> BlockSourceResult<Block>;
async fn get_block_hash(&self, height: u32) -> BlockSourceResult<Option<BlockHash>>;
async fn get_best_block(&self) -> BlockSourceResult<(BlockHash, u32)>;
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BlockHeaderData {
pub header: BlockHeader,
pub height: u32,
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(())
}
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(),
)
}
pub async fn bitcoind_client_from_url(mut url: Url, network: Network) -> BitcoindClient {
if url.username().is_empty() {
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
}