2020-05-07 15:14:05 +02:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
2020-05-07 17:36:45 +02:00
|
|
|
use futures::stream::{self, StreamExt, TryStreamExt};
|
|
|
|
|
2020-05-07 15:14:05 +02:00
|
|
|
#[allow(unused_imports)]
|
|
|
|
use log::{debug, error, info, trace};
|
|
|
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
2020-07-15 18:49:24 +02:00
|
|
|
use reqwest::{Client, StatusCode};
|
2020-05-07 15:14:05 +02:00
|
|
|
|
|
|
|
use bitcoin::consensus::{deserialize, serialize};
|
|
|
|
use bitcoin::hashes::hex::ToHex;
|
|
|
|
use bitcoin::hashes::{sha256, Hash};
|
|
|
|
use bitcoin::{Script, Transaction, Txid};
|
|
|
|
|
|
|
|
use self::utils::{ELSGetHistoryRes, ELSListUnspentRes, ElectrumLikeSync};
|
|
|
|
use super::*;
|
|
|
|
use crate::database::{BatchDatabase, DatabaseUtils};
|
|
|
|
use crate::error::Error;
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct UrlClient {
|
|
|
|
url: String,
|
2020-07-15 18:49:24 +02:00
|
|
|
// We use the async client instead of the blocking one because it automatically uses `fetch`
|
2020-07-20 15:51:57 +02:00
|
|
|
// when the target platform is wasm32.
|
2020-05-07 15:14:05 +02:00
|
|
|
client: Client,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct EsploraBlockchain(Option<UrlClient>);
|
|
|
|
|
|
|
|
impl std::convert::From<UrlClient> for EsploraBlockchain {
|
|
|
|
fn from(url_client: UrlClient) -> Self {
|
|
|
|
EsploraBlockchain(Some(url_client))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EsploraBlockchain {
|
|
|
|
pub fn new(base_url: &str) -> Self {
|
|
|
|
EsploraBlockchain(Some(UrlClient {
|
|
|
|
url: base_url.to_string(),
|
|
|
|
client: Client::new(),
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Blockchain for EsploraBlockchain {
|
|
|
|
fn offline() -> Self {
|
|
|
|
EsploraBlockchain(None)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_online(&self) -> bool {
|
|
|
|
self.0.is_some()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-20 15:51:57 +02:00
|
|
|
#[maybe_async]
|
2020-05-07 15:14:05 +02:00
|
|
|
impl OnlineBlockchain for EsploraBlockchain {
|
2020-07-15 18:49:24 +02:00
|
|
|
fn get_capabilities(&self) -> HashSet<Capability> {
|
2020-05-07 15:14:05 +02:00
|
|
|
vec![Capability::FullHistory, Capability::GetAnyTx]
|
|
|
|
.into_iter()
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2020-07-15 18:49:24 +02:00
|
|
|
fn setup<D: BatchDatabase + DatabaseUtils, P: Progress>(
|
2020-05-07 15:14:05 +02:00
|
|
|
&mut self,
|
|
|
|
stop_gap: Option<usize>,
|
|
|
|
database: &mut D,
|
|
|
|
progress_update: P,
|
|
|
|
) -> Result<(), Error> {
|
2020-07-20 15:51:57 +02:00
|
|
|
maybe_await!(self
|
|
|
|
.0
|
2020-05-07 15:14:05 +02:00
|
|
|
.as_mut()
|
|
|
|
.ok_or(Error::OfflineClient)?
|
2020-07-20 15:51:57 +02:00
|
|
|
.electrum_like_setup(stop_gap, database, progress_update))
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 18:49:24 +02:00
|
|
|
fn get_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
2020-07-20 15:51:57 +02:00
|
|
|
Ok(await_or_block!(self
|
|
|
|
.0
|
|
|
|
.as_mut()
|
|
|
|
.ok_or(Error::OfflineClient)?
|
|
|
|
._get_tx(txid))?)
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 18:49:24 +02:00
|
|
|
fn broadcast(&mut self, tx: &Transaction) -> Result<(), Error> {
|
2020-07-20 15:51:57 +02:00
|
|
|
Ok(await_or_block!(self
|
2020-05-07 15:14:05 +02:00
|
|
|
.0
|
|
|
|
.as_mut()
|
|
|
|
.ok_or(Error::OfflineClient)?
|
2020-07-20 15:51:57 +02:00
|
|
|
._broadcast(tx))?)
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 18:49:24 +02:00
|
|
|
fn get_height(&mut self) -> Result<usize, Error> {
|
2020-07-20 15:51:57 +02:00
|
|
|
Ok(await_or_block!(self
|
|
|
|
.0
|
|
|
|
.as_mut()
|
|
|
|
.ok_or(Error::OfflineClient)?
|
|
|
|
._get_height())?)
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl UrlClient {
|
|
|
|
fn script_to_scripthash(script: &Script) -> String {
|
|
|
|
sha256::Hash::hash(script.as_bytes()).into_inner().to_hex()
|
|
|
|
}
|
|
|
|
|
2020-07-20 15:51:57 +02:00
|
|
|
async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
|
|
|
let resp = self
|
|
|
|
.client
|
|
|
|
.get(&format!("{}/api/tx/{}/raw", self.url, txid))
|
|
|
|
.send()
|
|
|
|
.await?;
|
2020-05-07 15:14:05 +02:00
|
|
|
|
|
|
|
if let StatusCode::NOT_FOUND = resp.status() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
2020-07-20 15:51:57 +02:00
|
|
|
Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?))
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2020-07-20 15:51:57 +02:00
|
|
|
async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
|
|
|
|
self.client
|
|
|
|
.post(&format!("{}/api/tx", self.url))
|
|
|
|
.body(serialize(transaction).to_hex())
|
|
|
|
.send()
|
|
|
|
.await?
|
2020-05-07 15:14:05 +02:00
|
|
|
.error_for_status()?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-07-20 15:51:57 +02:00
|
|
|
async fn _get_height(&self) -> Result<usize, EsploraError> {
|
|
|
|
let req = self
|
|
|
|
.client
|
|
|
|
.get(&format!("{}/api/blocks/tip/height", self.url))
|
|
|
|
.send()
|
|
|
|
.await?;
|
2020-07-15 18:49:24 +02:00
|
|
|
|
2020-07-20 15:51:57 +02:00
|
|
|
Ok(req.error_for_status()?.text().await?.parse()?)
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2020-05-07 17:36:45 +02:00
|
|
|
async fn _script_get_history(
|
|
|
|
&self,
|
|
|
|
script: &Script,
|
|
|
|
) -> Result<Vec<ELSGetHistoryRes>, EsploraError> {
|
2020-05-07 15:14:05 +02:00
|
|
|
let mut result = Vec::new();
|
|
|
|
let scripthash = Self::script_to_scripthash(script);
|
|
|
|
|
|
|
|
// Add the unconfirmed transactions first
|
|
|
|
result.extend(
|
|
|
|
self.client
|
|
|
|
.get(&format!(
|
|
|
|
"{}/api/scripthash/{}/txs/mempool",
|
|
|
|
self.url, scripthash
|
|
|
|
))
|
2020-05-07 17:36:45 +02:00
|
|
|
.send()
|
|
|
|
.await?
|
2020-05-07 15:14:05 +02:00
|
|
|
.error_for_status()?
|
2020-05-07 17:36:45 +02:00
|
|
|
.json::<Vec<EsploraGetHistory>>()
|
|
|
|
.await?
|
2020-05-07 15:14:05 +02:00
|
|
|
.into_iter()
|
|
|
|
.map(|x| ELSGetHistoryRes {
|
|
|
|
tx_hash: x.txid,
|
|
|
|
height: x.status.block_height.unwrap_or(0) as i32,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
debug!(
|
|
|
|
"Found {} mempool txs for {} - {:?}",
|
|
|
|
result.len(),
|
|
|
|
scripthash,
|
|
|
|
script
|
|
|
|
);
|
|
|
|
|
|
|
|
// Then go through all the pages of confirmed transactions
|
|
|
|
let mut last_txid = String::new();
|
|
|
|
loop {
|
|
|
|
let response = self
|
|
|
|
.client
|
|
|
|
.get(&format!(
|
|
|
|
"{}/api/scripthash/{}/txs/chain/{}",
|
|
|
|
self.url, scripthash, last_txid
|
|
|
|
))
|
2020-05-07 17:36:45 +02:00
|
|
|
.send()
|
|
|
|
.await?
|
2020-05-07 15:14:05 +02:00
|
|
|
.error_for_status()?
|
2020-05-07 17:36:45 +02:00
|
|
|
.json::<Vec<EsploraGetHistory>>()
|
|
|
|
.await?;
|
2020-05-07 15:14:05 +02:00
|
|
|
let len = response.len();
|
|
|
|
if let Some(elem) = response.last() {
|
|
|
|
last_txid = elem.txid.to_hex();
|
|
|
|
}
|
|
|
|
|
|
|
|
debug!("... adding {} confirmed transactions", len);
|
|
|
|
|
|
|
|
result.extend(response.into_iter().map(|x| ELSGetHistoryRes {
|
|
|
|
tx_hash: x.txid,
|
|
|
|
height: x.status.block_height.unwrap_or(0) as i32,
|
|
|
|
}));
|
|
|
|
|
|
|
|
if len < 25 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
|
2020-05-07 17:36:45 +02:00
|
|
|
async fn _script_list_unspent(
|
2020-05-07 15:14:05 +02:00
|
|
|
&self,
|
|
|
|
script: &Script,
|
|
|
|
) -> Result<Vec<ELSListUnspentRes>, EsploraError> {
|
|
|
|
Ok(self
|
|
|
|
.client
|
|
|
|
.get(&format!(
|
|
|
|
"{}/api/scripthash/{}/utxo",
|
|
|
|
self.url,
|
|
|
|
Self::script_to_scripthash(script)
|
|
|
|
))
|
2020-05-07 17:36:45 +02:00
|
|
|
.send()
|
|
|
|
.await?
|
2020-05-07 15:14:05 +02:00
|
|
|
.error_for_status()?
|
2020-05-07 17:36:45 +02:00
|
|
|
.json::<Vec<EsploraListUnspent>>()
|
|
|
|
.await?
|
2020-05-07 15:14:05 +02:00
|
|
|
.into_iter()
|
|
|
|
.map(|x| ELSListUnspentRes {
|
|
|
|
tx_hash: x.txid,
|
|
|
|
height: x.status.block_height.unwrap_or(0),
|
|
|
|
tx_pos: x.vout,
|
|
|
|
})
|
|
|
|
.collect())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-20 15:51:57 +02:00
|
|
|
#[maybe_async]
|
2020-05-07 15:14:05 +02:00
|
|
|
impl ElectrumLikeSync for UrlClient {
|
2020-07-15 18:49:24 +02:00
|
|
|
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
|
2020-05-07 15:14:05 +02:00
|
|
|
&mut self,
|
|
|
|
scripts: I,
|
|
|
|
) -> Result<Vec<Vec<ELSGetHistoryRes>>, Error> {
|
2020-07-20 15:51:57 +02:00
|
|
|
let future = async {
|
2020-07-15 18:49:24 +02:00
|
|
|
Ok(stream::iter(scripts)
|
|
|
|
.then(|script| self._script_get_history(&script))
|
|
|
|
.try_collect()
|
|
|
|
.await?)
|
2020-07-20 15:51:57 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
await_or_block!(future)
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 18:49:24 +02:00
|
|
|
fn els_batch_script_list_unspent<'s, I: IntoIterator<Item = &'s Script>>(
|
2020-05-07 15:14:05 +02:00
|
|
|
&mut self,
|
|
|
|
scripts: I,
|
|
|
|
) -> Result<Vec<Vec<ELSListUnspentRes>>, Error> {
|
2020-07-20 15:51:57 +02:00
|
|
|
let future = async {
|
2020-07-15 18:49:24 +02:00
|
|
|
Ok(stream::iter(scripts)
|
|
|
|
.then(|script| self._script_list_unspent(&script))
|
|
|
|
.try_collect()
|
|
|
|
.await?)
|
2020-07-20 15:51:57 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
await_or_block!(future)
|
2020-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 18:49:24 +02:00
|
|
|
fn els_transaction_get(&mut self, txid: &Txid) -> Result<Transaction, Error> {
|
2020-07-20 15:51:57 +02:00
|
|
|
Ok(await_or_block!(self._get_tx(txid))?
|
2020-05-07 15:14:05 +02:00
|
|
|
.ok_or_else(|| EsploraError::TransactionNotFound(*txid))?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct EsploraGetHistoryStatus {
|
|
|
|
block_height: Option<usize>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct EsploraGetHistory {
|
|
|
|
txid: Txid,
|
|
|
|
status: EsploraGetHistoryStatus,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct EsploraListUnspent {
|
|
|
|
txid: Txid,
|
|
|
|
vout: usize,
|
|
|
|
status: EsploraGetHistoryStatus,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum EsploraError {
|
|
|
|
Reqwest(reqwest::Error),
|
|
|
|
Parsing(std::num::ParseIntError),
|
|
|
|
BitcoinEncoding(bitcoin::consensus::encode::Error),
|
|
|
|
|
|
|
|
TransactionNotFound(Txid),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<reqwest::Error> for EsploraError {
|
|
|
|
fn from(other: reqwest::Error) -> Self {
|
|
|
|
EsploraError::Reqwest(other)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<std::num::ParseIntError> for EsploraError {
|
|
|
|
fn from(other: std::num::ParseIntError) -> Self {
|
|
|
|
EsploraError::Parsing(other)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<bitcoin::consensus::encode::Error> for EsploraError {
|
|
|
|
fn from(other: bitcoin::consensus::encode::Error) -> Self {
|
|
|
|
EsploraError::BitcoinEncoding(other)
|
|
|
|
}
|
|
|
|
}
|