Invert dependencies in electrum sync
Blockchain calls sync logic rather than the other way around. Sync logic is captured in script_sync.rs.
This commit is contained in:
117
src/blockchain/esplora/api.rs
Normal file
117
src/blockchain/esplora/api.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
//! structs from the esplora API
|
||||
//!
|
||||
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
|
||||
use crate::ConfirmationTime;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid};
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct PrevOut {
|
||||
pub value: u64,
|
||||
pub scriptpubkey: Script,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct Vin {
|
||||
pub txid: Txid,
|
||||
pub vout: u32,
|
||||
// None if coinbase
|
||||
pub prevout: Option<PrevOut>,
|
||||
pub scriptsig: Script,
|
||||
#[serde(deserialize_with = "deserialize_witness")]
|
||||
pub witness: Vec<Vec<u8>>,
|
||||
pub sequence: u32,
|
||||
pub is_coinbase: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct Vout {
|
||||
pub value: u64,
|
||||
pub scriptpubkey: Script,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct TxStatus {
|
||||
pub confirmed: bool,
|
||||
pub block_height: Option<u32>,
|
||||
pub block_time: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct Tx {
|
||||
pub txid: Txid,
|
||||
pub version: i32,
|
||||
pub locktime: u32,
|
||||
pub vin: Vec<Vin>,
|
||||
pub vout: Vec<Vout>,
|
||||
pub status: TxStatus,
|
||||
pub fee: u64,
|
||||
}
|
||||
|
||||
impl Tx {
|
||||
pub fn to_tx(&self) -> Transaction {
|
||||
Transaction {
|
||||
version: self.version,
|
||||
lock_time: self.locktime,
|
||||
input: self
|
||||
.vin
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|vin| TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
script_sig: vin.scriptsig,
|
||||
sequence: vin.sequence,
|
||||
witness: vin.witness,
|
||||
})
|
||||
.collect(),
|
||||
output: self
|
||||
.vout
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|vout| TxOut {
|
||||
value: vout.value,
|
||||
script_pubkey: vout.scriptpubkey,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn confirmation_time(&self) -> Option<ConfirmationTime> {
|
||||
match self.status {
|
||||
TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(height),
|
||||
block_time: Some(timestamp),
|
||||
} => Some(ConfirmationTime { timestamp, height }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_outputs(&self) -> Vec<Option<TxOut>> {
|
||||
self.vin
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|vin| {
|
||||
vin.prevout.map(|po| TxOut {
|
||||
script_pubkey: po.scriptpubkey,
|
||||
value: po.value,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_witness<'de, D>(d: D) -> Result<Vec<Vec<u8>>, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
use crate::serde::Deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
let list = Vec::<String>::deserialize(d)?;
|
||||
list.into_iter()
|
||||
.map(|hex_str| Vec::<u8>::from_hex(&hex_str))
|
||||
.collect::<Result<Vec<Vec<u8>>, _>>()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
@@ -21,8 +21,6 @@ use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use bitcoin::consensus;
|
||||
use bitcoin::{BlockHash, Txid};
|
||||
|
||||
@@ -41,6 +39,8 @@ mod ureq;
|
||||
#[cfg(feature = "ureq")]
|
||||
pub use self::ureq::*;
|
||||
|
||||
mod api;
|
||||
|
||||
fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRate, Error> {
|
||||
let fee_val = estimates
|
||||
.into_iter()
|
||||
@@ -56,18 +56,6 @@ fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRa
|
||||
Ok(FeeRate::from_sat_per_vb(fee_val as f32))
|
||||
}
|
||||
|
||||
/// Data type used when fetching transaction history from Esplora.
|
||||
#[derive(Deserialize)]
|
||||
pub struct EsploraGetHistory {
|
||||
txid: Txid,
|
||||
status: EsploraGetHistoryStatus,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EsploraGetHistoryStatus {
|
||||
block_height: Option<usize>,
|
||||
}
|
||||
|
||||
/// Errors that can happen during a sync with [`EsploraBlockchain`]
|
||||
#[derive(Debug)]
|
||||
pub enum EsploraError {
|
||||
@@ -107,10 +95,50 @@ impl fmt::Display for EsploraError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for an [`EsploraBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
pub struct EsploraBlockchainConfig {
|
||||
/// Base URL of the esplora service
|
||||
///
|
||||
/// eg. `https://blockstream.info/api/`
|
||||
pub base_url: String,
|
||||
/// Optional URL of the proxy to use to make requests to the Esplora server
|
||||
///
|
||||
/// The string should be formatted as: `<protocol>://<user>:<password>@host:<port>`.
|
||||
///
|
||||
/// Note that the format of this value and the supported protocols change slightly between the
|
||||
/// sync version of esplora (using `ureq`) and the async version (using `reqwest`). For more
|
||||
/// details check with the documentation of the two crates. Both of them are compiled with
|
||||
/// the `socks` feature enabled.
|
||||
///
|
||||
/// The proxy is ignored when targeting `wasm32`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub proxy: Option<String>,
|
||||
/// Number of parallel requests sent to the esplora service (default: 4)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub concurrency: Option<u8>,
|
||||
/// Stop searching addresses for transactions after finding an unused gap of this length.
|
||||
pub stop_gap: usize,
|
||||
/// Socket timeout.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl EsploraBlockchainConfig {
|
||||
/// create a config with default values given the base url and stop gap
|
||||
pub fn new(base_url: String) -> Self {
|
||||
Self {
|
||||
base_url,
|
||||
proxy: None,
|
||||
timeout: None,
|
||||
stop_gap: 20,
|
||||
concurrency: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EsploraError {}
|
||||
|
||||
#[cfg(feature = "ureq")]
|
||||
impl_error!(::ureq::Error, Ureq, EsploraError);
|
||||
#[cfg(feature = "ureq")]
|
||||
impl_error!(::ureq::Transport, UreqTransport, EsploraError);
|
||||
#[cfg(feature = "reqwest")]
|
||||
@@ -127,3 +155,5 @@ crate::bdk_blockchain_tests! {
|
||||
EsploraBlockchain::new(&format!("http://{}",test_client.electrsd.esplora_url.as_ref().unwrap()), 20)
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
|
||||
|
||||
@@ -21,20 +21,16 @@ use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt};
|
||||
|
||||
use ::reqwest::{Client, StatusCode};
|
||||
use futures::stream::{FuturesOrdered, TryStreamExt};
|
||||
|
||||
use crate::blockchain::esplora::{EsploraError, EsploraGetHistory};
|
||||
use crate::blockchain::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
||||
use super::api::Tx;
|
||||
use crate::blockchain::esplora::EsploraError;
|
||||
use crate::blockchain::*;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
use crate::wallet::utils::ChunksIterator;
|
||||
use crate::FeeRate;
|
||||
|
||||
const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UrlClient {
|
||||
url: String,
|
||||
@@ -70,7 +66,7 @@ impl EsploraBlockchain {
|
||||
url_client: UrlClient {
|
||||
url: base_url.to_string(),
|
||||
client: Client::new(),
|
||||
concurrency: DEFAULT_CONCURRENT_REQUESTS,
|
||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||
},
|
||||
stop_gap,
|
||||
}
|
||||
@@ -98,11 +94,91 @@ impl Blockchain for EsploraBlockchain {
|
||||
fn setup<D: BatchDatabase, P: Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
_progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(self
|
||||
.url_client
|
||||
.electrum_like_setup(self.stop_gap, database, progress_update))
|
||||
use crate::blockchain::script_sync::Request;
|
||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||
let mut tx_index: HashMap<Txid, Tx> = HashMap::new();
|
||||
|
||||
let batch_update = loop {
|
||||
request = match request {
|
||||
Request::Script(script_req) => {
|
||||
let futures: FuturesOrdered<_> = script_req
|
||||
.request()
|
||||
.take(self.url_client.concurrency as usize)
|
||||
.map(|script| async move {
|
||||
let mut related_txs: Vec<Tx> =
|
||||
self.url_client._scripthash_txs(script, None).await?;
|
||||
|
||||
let n_confirmed =
|
||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||
// esplora pages on 25 confirmed transactions. If there's more than
|
||||
// 25 we need to keep requesting.
|
||||
if n_confirmed >= 25 {
|
||||
loop {
|
||||
let new_related_txs: Vec<Tx> = self
|
||||
.url_client
|
||||
._scripthash_txs(
|
||||
script,
|
||||
Some(related_txs.last().unwrap().txid),
|
||||
)
|
||||
.await?;
|
||||
let n = new_related_txs.len();
|
||||
related_txs.extend(new_related_txs);
|
||||
// we've reached the end
|
||||
if n < 25 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Result::<_, Error>::Ok(related_txs)
|
||||
})
|
||||
.collect();
|
||||
let txs_per_script: Vec<Vec<Tx>> = await_or_block!(futures.try_collect())?;
|
||||
let mut satisfaction = vec![];
|
||||
|
||||
for txs in txs_per_script {
|
||||
satisfaction.push(
|
||||
txs.iter()
|
||||
.map(|tx| (tx.txid, tx.status.block_height))
|
||||
.collect(),
|
||||
);
|
||||
for tx in txs {
|
||||
tx_index.insert(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
|
||||
script_req.satisfy(satisfaction)?
|
||||
}
|
||||
Request::Conftime(conftimereq) => {
|
||||
let conftimes = conftimereq
|
||||
.request()
|
||||
.map(|txid| {
|
||||
tx_index
|
||||
.get(txid)
|
||||
.expect("must be in index")
|
||||
.confirmation_time()
|
||||
})
|
||||
.collect();
|
||||
conftimereq.satisfy(conftimes)?
|
||||
}
|
||||
Request::Tx(txreq) => {
|
||||
let full_txs = txreq
|
||||
.request()
|
||||
.map(|txid| {
|
||||
let tx = tx_index.get(txid).expect("must be in index");
|
||||
(tx.confirmation_time(), tx.previous_outputs(), tx.to_tx())
|
||||
})
|
||||
.collect();
|
||||
txreq.satisfy(full_txs)?
|
||||
}
|
||||
Request::Finish(batch_update) => break batch_update,
|
||||
}
|
||||
};
|
||||
|
||||
database.commit_batch(batch_update)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
@@ -124,10 +200,6 @@ impl Blockchain for EsploraBlockchain {
|
||||
}
|
||||
|
||||
impl UrlClient {
|
||||
fn script_to_scripthash(script: &Script) -> String {
|
||||
sha256::Hash::hash(script.as_bytes()).into_inner().to_hex()
|
||||
}
|
||||
|
||||
async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
||||
let resp = self
|
||||
.client
|
||||
@@ -196,71 +268,27 @@ impl UrlClient {
|
||||
Ok(req.error_for_status()?.text().await?.parse()?)
|
||||
}
|
||||
|
||||
async fn _script_get_history(
|
||||
async fn _scripthash_txs(
|
||||
&self,
|
||||
script: &Script,
|
||||
) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
|
||||
let mut result = Vec::new();
|
||||
let scripthash = Self::script_to_scripthash(script);
|
||||
|
||||
// Add the unconfirmed transactions first
|
||||
result.extend(
|
||||
self.client
|
||||
.get(&format!(
|
||||
"{}/scripthash/{}/txs/mempool",
|
||||
self.url, scripthash
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<Vec<EsploraGetHistory>>()
|
||||
.await?
|
||||
.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!(
|
||||
"{}/scripthash/{}/txs/chain/{}",
|
||||
self.url, scripthash, last_txid
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<Vec<EsploraGetHistory>>()
|
||||
.await?;
|
||||
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)
|
||||
last_seen: Option<Txid>,
|
||||
) -> Result<Vec<Tx>, EsploraError> {
|
||||
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
|
||||
let url = match last_seen {
|
||||
Some(last_seen) => format!(
|
||||
"{}/scripthash/{}/txs/chain/{}",
|
||||
self.url, script_hash, last_seen
|
||||
),
|
||||
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
|
||||
};
|
||||
Ok(self
|
||||
.client
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<Vec<Tx>>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
|
||||
@@ -275,83 +303,8 @@ impl UrlClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl ElectrumLikeSync for UrlClient {
|
||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
|
||||
&self,
|
||||
scripts: I,
|
||||
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
||||
let mut results = vec![];
|
||||
for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) {
|
||||
let mut futs = FuturesOrdered::new();
|
||||
for script in chunk {
|
||||
futs.push(self._script_get_history(script));
|
||||
}
|
||||
let partial_results: Vec<Vec<ElsGetHistoryRes>> = await_or_block!(futs.try_collect())?;
|
||||
results.extend(partial_results);
|
||||
}
|
||||
Ok(await_or_block!(stream::iter(results).collect()))
|
||||
}
|
||||
|
||||
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
|
||||
&self,
|
||||
txids: I,
|
||||
) -> Result<Vec<Transaction>, Error> {
|
||||
let mut results = vec![];
|
||||
for chunk in ChunksIterator::new(txids.into_iter(), self.concurrency as usize) {
|
||||
let mut futs = FuturesOrdered::new();
|
||||
for txid in chunk {
|
||||
futs.push(self._get_tx_no_opt(txid));
|
||||
}
|
||||
let partial_results: Vec<Transaction> = await_or_block!(futs.try_collect())?;
|
||||
results.extend(partial_results);
|
||||
}
|
||||
Ok(await_or_block!(stream::iter(results).collect()))
|
||||
}
|
||||
|
||||
fn els_batch_block_header<I: IntoIterator<Item = u32>>(
|
||||
&self,
|
||||
heights: I,
|
||||
) -> Result<Vec<BlockHeader>, Error> {
|
||||
let mut results = vec![];
|
||||
for chunk in ChunksIterator::new(heights.into_iter(), self.concurrency as usize) {
|
||||
let mut futs = FuturesOrdered::new();
|
||||
for height in chunk {
|
||||
futs.push(self._get_header(height));
|
||||
}
|
||||
let partial_results: Vec<BlockHeader> = await_or_block!(futs.try_collect())?;
|
||||
results.extend(partial_results);
|
||||
}
|
||||
Ok(await_or_block!(stream::iter(results).collect()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for an [`EsploraBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
pub struct EsploraBlockchainConfig {
|
||||
/// Base URL of the esplora service
|
||||
///
|
||||
/// eg. `https://blockstream.info/api/`
|
||||
pub base_url: String,
|
||||
/// Optional URL of the proxy to use to make requests to the Esplora server
|
||||
///
|
||||
/// The string should be formatted as: `<protocol>://<user>:<password>@host:<port>`.
|
||||
///
|
||||
/// Note that the format of this value and the supported protocols change slightly between the
|
||||
/// sync version of esplora (using `ureq`) and the async version (using `reqwest`). For more
|
||||
/// details check with the documentation of the two crates. Both of them are compiled with
|
||||
/// the `socks` feature enabled.
|
||||
///
|
||||
/// The proxy is ignored when targeting `wasm32`.
|
||||
pub proxy: Option<String>,
|
||||
/// Number of parallel requests sent to the esplora service (default: 4)
|
||||
pub concurrency: Option<u8>,
|
||||
/// Stop searching addresses for transactions after finding an unused gap of this length.
|
||||
pub stop_gap: usize,
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||
type Config = EsploraBlockchainConfig;
|
||||
type Config = super::EsploraBlockchainConfig;
|
||||
|
||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||
let map_e = |e: reqwest::Error| Error::Esplora(Box::new(e.into()));
|
||||
@@ -360,13 +313,19 @@ impl ConfigurableBlockchain for EsploraBlockchain {
|
||||
if let Some(concurrency) = config.concurrency {
|
||||
blockchain.url_client.concurrency = concurrency;
|
||||
}
|
||||
let mut builder = Client::builder();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(proxy) = &config.proxy {
|
||||
blockchain.url_client.client = Client::builder()
|
||||
.proxy(reqwest::Proxy::all(proxy).map_err(map_e)?)
|
||||
.build()
|
||||
.map_err(map_e)?;
|
||||
builder = builder.proxy(reqwest::Proxy::all(proxy).map_err(map_e)?);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(timeout) = config.timeout {
|
||||
builder = builder.timeout(core::time::Duration::from_secs(timeout));
|
||||
}
|
||||
|
||||
blockchain.url_client.client = builder.build().map_err(map_e)?;
|
||||
|
||||
Ok(blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@ use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||
|
||||
use crate::blockchain::esplora::{EsploraError, EsploraGetHistory};
|
||||
use crate::blockchain::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
||||
use super::api::Tx;
|
||||
use crate::blockchain::esplora::EsploraError;
|
||||
use crate::blockchain::*;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
use crate::FeeRate;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct UrlClient {
|
||||
url: String,
|
||||
agent: Agent,
|
||||
@@ -47,15 +47,7 @@ struct UrlClient {
|
||||
pub struct EsploraBlockchain {
|
||||
url_client: UrlClient,
|
||||
stop_gap: usize,
|
||||
}
|
||||
|
||||
impl std::convert::From<UrlClient> for EsploraBlockchain {
|
||||
fn from(url_client: UrlClient) -> Self {
|
||||
EsploraBlockchain {
|
||||
url_client,
|
||||
stop_gap: 20,
|
||||
}
|
||||
}
|
||||
concurrency: u8,
|
||||
}
|
||||
|
||||
impl EsploraBlockchain {
|
||||
@@ -66,6 +58,7 @@ impl EsploraBlockchain {
|
||||
url: base_url.to_string(),
|
||||
agent: Agent::new(),
|
||||
},
|
||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||
stop_gap,
|
||||
}
|
||||
}
|
||||
@@ -75,6 +68,12 @@ impl EsploraBlockchain {
|
||||
self.url_client.agent = agent;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the number of parallel requests the client can make.
|
||||
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
||||
self.concurrency = concurrency;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Blockchain for EsploraBlockchain {
|
||||
@@ -91,10 +90,94 @@ impl Blockchain for EsploraBlockchain {
|
||||
fn setup<D: BatchDatabase, P: Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
_progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
self.url_client
|
||||
.electrum_like_setup(self.stop_gap, database, progress_update)
|
||||
use crate::blockchain::script_sync::Request;
|
||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||
let mut tx_index: HashMap<Txid, Tx> = HashMap::new();
|
||||
let batch_update = loop {
|
||||
request = match request {
|
||||
Request::Script(script_req) => {
|
||||
let scripts = script_req
|
||||
.request()
|
||||
.take(self.concurrency as usize)
|
||||
.cloned();
|
||||
|
||||
let handles = scripts.map(move |script| {
|
||||
let client = self.url_client.clone();
|
||||
// make each request in its own thread.
|
||||
std::thread::spawn(move || {
|
||||
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
|
||||
|
||||
let n_confirmed =
|
||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||
// esplora pages on 25 confirmed transactions. If there's more than
|
||||
// 25 we need to keep requesting.
|
||||
if n_confirmed >= 25 {
|
||||
loop {
|
||||
let new_related_txs: Vec<Tx> = client._scripthash_txs(
|
||||
&script,
|
||||
Some(related_txs.last().unwrap().txid),
|
||||
)?;
|
||||
let n = new_related_txs.len();
|
||||
related_txs.extend(new_related_txs);
|
||||
// we've reached the end
|
||||
if n < 25 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Result::<_, Error>::Ok(related_txs)
|
||||
})
|
||||
});
|
||||
|
||||
let txs_per_script: Vec<Vec<Tx>> = handles
|
||||
.map(|handle| handle.join().unwrap())
|
||||
.collect::<Result<_, _>>()?;
|
||||
let mut satisfaction = vec![];
|
||||
|
||||
for txs in txs_per_script {
|
||||
satisfaction.push(
|
||||
txs.iter()
|
||||
.map(|tx| (tx.txid, tx.status.block_height))
|
||||
.collect(),
|
||||
);
|
||||
for tx in txs {
|
||||
tx_index.insert(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
|
||||
script_req.satisfy(satisfaction)?
|
||||
}
|
||||
Request::Conftime(conftimereq) => {
|
||||
let conftimes = conftimereq
|
||||
.request()
|
||||
.map(|txid| {
|
||||
tx_index
|
||||
.get(txid)
|
||||
.expect("must be in index")
|
||||
.confirmation_time()
|
||||
})
|
||||
.collect();
|
||||
conftimereq.satisfy(conftimes)?
|
||||
}
|
||||
Request::Tx(txreq) => {
|
||||
let full_txs = txreq
|
||||
.request()
|
||||
.map(|txid| {
|
||||
let tx = tx_index.get(txid).expect("must be in index");
|
||||
(tx.confirmation_time(), tx.previous_outputs(), tx.to_tx())
|
||||
})
|
||||
.collect();
|
||||
txreq.satisfy(full_txs)?
|
||||
}
|
||||
Request::Finish(batch_update) => break batch_update,
|
||||
}
|
||||
};
|
||||
|
||||
database.commit_batch(batch_update)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
@@ -117,10 +200,6 @@ impl Blockchain for EsploraBlockchain {
|
||||
}
|
||||
|
||||
impl UrlClient {
|
||||
fn script_to_scripthash(script: &Script) -> String {
|
||||
sha256::Hash::hash(script.as_bytes()).into_inner().to_hex()
|
||||
}
|
||||
|
||||
fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
||||
let resp = self
|
||||
.agent
|
||||
@@ -200,81 +279,6 @@ impl UrlClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn _script_get_history(&self, script: &Script) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
|
||||
let mut result = Vec::new();
|
||||
let scripthash = Self::script_to_scripthash(script);
|
||||
|
||||
// Add the unconfirmed transactions first
|
||||
|
||||
let resp = self
|
||||
.agent
|
||||
.get(&format!(
|
||||
"{}/scripthash/{}/txs/mempool",
|
||||
self.url, scripthash
|
||||
))
|
||||
.call();
|
||||
|
||||
let v = match resp {
|
||||
Ok(resp) => {
|
||||
let v: Vec<EsploraGetHistory> = resp.into_json()?;
|
||||
Ok(v)
|
||||
}
|
||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}?;
|
||||
|
||||
result.extend(v.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 resp = self
|
||||
.agent
|
||||
.get(&format!(
|
||||
"{}/scripthash/{}/txs/chain/{}",
|
||||
self.url, scripthash, last_txid
|
||||
))
|
||||
.call();
|
||||
|
||||
let v = match resp {
|
||||
Ok(resp) => {
|
||||
let v: Vec<EsploraGetHistory> = resp.into_json()?;
|
||||
Ok(v)
|
||||
}
|
||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}?;
|
||||
|
||||
let len = v.len();
|
||||
if let Some(elem) = v.last() {
|
||||
last_txid = elem.txid.to_hex();
|
||||
}
|
||||
|
||||
debug!("... adding {} confirmed transactions", len);
|
||||
|
||||
result.extend(v.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)
|
||||
}
|
||||
|
||||
fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
|
||||
let resp = self
|
||||
.agent
|
||||
@@ -292,6 +296,22 @@ impl UrlClient {
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn _scripthash_txs(
|
||||
&self,
|
||||
script: &Script,
|
||||
last_seen: Option<Txid>,
|
||||
) -> Result<Vec<Tx>, EsploraError> {
|
||||
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
|
||||
let url = match last_seen {
|
||||
Some(last_seen) => format!(
|
||||
"{}/scripthash/{}/txs/chain/{}",
|
||||
self.url, script_hash, last_seen
|
||||
),
|
||||
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
|
||||
};
|
||||
Ok(self.agent.get(&url).call()?.into_json()?)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_status_not_found(status: u16) -> bool {
|
||||
@@ -315,84 +335,37 @@ fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
impl ElectrumLikeSync for UrlClient {
|
||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
|
||||
&self,
|
||||
scripts: I,
|
||||
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
||||
let mut results = vec![];
|
||||
for script in scripts.into_iter() {
|
||||
let v = self._script_get_history(script)?;
|
||||
results.push(v);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
|
||||
&self,
|
||||
txids: I,
|
||||
) -> Result<Vec<Transaction>, Error> {
|
||||
let mut results = vec![];
|
||||
for txid in txids.into_iter() {
|
||||
let tx = self._get_tx_no_opt(txid)?;
|
||||
results.push(tx);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn els_batch_block_header<I: IntoIterator<Item = u32>>(
|
||||
&self,
|
||||
heights: I,
|
||||
) -> Result<Vec<BlockHeader>, Error> {
|
||||
let mut results = vec![];
|
||||
for height in heights.into_iter() {
|
||||
let header = self._get_header(height)?;
|
||||
results.push(header);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for an [`EsploraBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
pub struct EsploraBlockchainConfig {
|
||||
/// Base URL of the esplora service eg. `https://blockstream.info/api/`
|
||||
pub base_url: String,
|
||||
/// Optional URL of the proxy to use to make requests to the Esplora server
|
||||
///
|
||||
/// The string should be formatted as: `<protocol>://<user>:<password>@host:<port>`.
|
||||
///
|
||||
/// Note that the format of this value and the supported protocols change slightly between the
|
||||
/// sync version of esplora (using `ureq`) and the async version (using `reqwest`). For more
|
||||
/// details check with the documentation of the two crates. Both of them are compiled with
|
||||
/// the `socks` feature enabled.
|
||||
///
|
||||
/// The proxy is ignored when targeting `wasm32`.
|
||||
pub proxy: Option<String>,
|
||||
/// Socket read timeout.
|
||||
pub timeout_read: u64,
|
||||
/// Socket write timeout.
|
||||
pub timeout_write: u64,
|
||||
/// Stop searching addresses for transactions after finding an unused gap of this length.
|
||||
pub stop_gap: usize,
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||
type Config = EsploraBlockchainConfig;
|
||||
type Config = super::EsploraBlockchainConfig;
|
||||
|
||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||
let mut agent_builder = ureq::AgentBuilder::new()
|
||||
.timeout_read(Duration::from_secs(config.timeout_read))
|
||||
.timeout_write(Duration::from_secs(config.timeout_write));
|
||||
let mut agent_builder = ureq::AgentBuilder::new();
|
||||
|
||||
if let Some(timeout) = config.timeout {
|
||||
agent_builder = agent_builder.timeout(Duration::from_secs(timeout));
|
||||
}
|
||||
|
||||
if let Some(proxy) = &config.proxy {
|
||||
agent_builder = agent_builder
|
||||
.proxy(Proxy::new(proxy).map_err(|e| Error::Esplora(Box::new(e.into())))?);
|
||||
}
|
||||
|
||||
Ok(
|
||||
EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap)
|
||||
.with_agent(agent_builder.build()),
|
||||
)
|
||||
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap)
|
||||
.with_agent(agent_builder.build());
|
||||
|
||||
if let Some(concurrency) = config.concurrency {
|
||||
blockchain = blockchain.with_concurrency(concurrency);
|
||||
}
|
||||
|
||||
Ok(blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ureq::Error> for EsploraError {
|
||||
fn from(e: ureq::Error) -> Self {
|
||||
match e {
|
||||
ureq::Error::Status(code, _) => EsploraError::HttpResponse(code),
|
||||
e => EsploraError::Ureq(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user