bitcoind_client/
esplora_client.rsuse crate::{Error, Explorer};
use async_trait::async_trait;
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::OutPoint;
use log::info;
use bitcoin::Transaction;
use bitcoin::Txid;
use reqwest::{Client, Response};
use std::sync::Arc;
use tokio::sync::Mutex;
use url::Url;
#[derive(Clone, Debug)]
pub struct EsploraClient {
rpc: Arc<Mutex<Client>>,
url: Url,
}
impl EsploraClient {
pub async fn new(url: Url) -> Self {
let builder = Client::builder();
let rpc = builder.build().unwrap();
let client = Self {
rpc: Arc::new(Mutex::new(rpc)),
url,
};
client
}
pub async fn get<T: for<'a> serde::de::Deserialize<'a>>(&self, path: &str) -> Result<T, Error> {
let rpc = self.rpc.lock().await;
let url = format!("{}/{}", self.url, path);
let res = rpc
.get(&url)
.send()
.await
.map_err(|e| Error::Esplora(e.to_string()))?;
let res = Self::handle_error(res).await?;
let res = res
.json::<T>()
.await
.map_err(|e| Error::Esplora(e.to_string()))?;
Ok(res)
}
pub async fn get_raw(&self, path: &str) -> Result<String, Error> {
let rpc = self.rpc.lock().await;
let url = format!("{}/{}", self.url, path);
let res = rpc
.get(&url)
.send()
.await
.map_err(|e| Error::Esplora(e.to_string()))?;
let res = Self::handle_error(res).await?;
let res = res
.text()
.await
.map_err(|e| Error::Esplora(e.to_string()))?;
Ok(res)
}
pub async fn post<T: for<'a> serde::de::Deserialize<'a>>(
&self,
path: &str,
body: String,
) -> Result<T, Error> {
let rpc = self.rpc.lock().await;
let res = rpc
.post(&format!("{}/{}", self.url, path))
.body(body)
.send()
.await
.map_err(|e| Error::Esplora(e.to_string()))?;
let res = Self::handle_error(res).await?;
let res = res
.json::<T>()
.await
.map_err(|e| Error::Esplora(e.to_string()))?;
Ok(res)
}
pub async fn post_returning_body(&self, path: &str, body: String) -> Result<String, Error> {
let rpc = self.rpc.lock().await;
let res = rpc
.post(&format!("{}/{}", self.url, path))
.body(body)
.send()
.await
.map_err(|e| Error::Esplora(e.to_string()))?;
let res = Self::handle_error(res).await?;
Ok(res
.text()
.await
.map_err(|e| Error::Esplora(e.to_string()))?)
}
async fn handle_error(res: Response) -> Result<Response, Error> {
if res.status().is_server_error() || res.status().is_client_error() {
Err(Error::Esplora(format!(
"server error: {} {}",
res.status(),
res.text().await.unwrap()
)))
} else {
Ok(res)
}
}
}
#[derive(serde::Deserialize, Debug)]
struct TxOutResponse {
spent: bool,
txid: Option<Txid>,
}
#[derive(serde::Deserialize, Debug)]
struct TxResponse {
confirmed: bool,
block_height: Option<u64>,
}
#[async_trait]
impl Explorer for EsploraClient {
async fn get_utxo_confirmations(&self, txout: &OutPoint) -> Result<Option<u64>, Error> {
let txout_res: TxOutResponse = self
.get(&format!("/tx/{}/outspend/{}", txout.txid, txout.vout))
.await?;
if txout_res.spent {
Ok(None)
} else {
let tx_res: TxResponse = self.get(&format!("/tx/{}/status", txout.txid)).await?;
if tx_res.confirmed {
let chain_height: u64 = self.get("/blocks/tip/height").await?;
Ok(Some(chain_height - tx_res.block_height.unwrap() + 1))
} else {
Ok(None)
}
}
}
async fn broadcast_transaction(&self, tx: &Transaction) -> Result<(), Error> {
let txid: String = self
.post_returning_body("/tx", serialize_hex(tx))
.await?;
info!("broadcasted txid: {}", txid);
Ok(())
}
async fn get_utxo_spending_tx(&self, txout: &OutPoint) -> Result<Option<Transaction>, Error> {
let txout_res: TxOutResponse = self
.get(&format!("/tx/{}/outspend/{}", txout.txid, txout.vout))
.await?;
if let Some(txid) = txout_res.txid {
let tx_hex = self.get_raw(&format!("/tx/{}/hex", txid)).await?;
let tx =
bitcoin::consensus::deserialize(&hex::decode(&tx_hex).expect("bad hex")).unwrap();
Ok(Some(tx))
} else {
Ok(None)
}
}
}