From 8856611781074127c1c689957d199c36cd625ccd Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sat, 30 Sep 2023 16:57:00 -0500 Subject: [PATCH] feat: add blockchain traits module with esplora_async impl --- crates/bdk/Cargo.toml | 8 + crates/bdk/src/blockchain/async_traits.rs | 115 +++++++++++++ crates/bdk/src/blockchain/blocking_traits.rs | 117 +++++++++++++ crates/bdk/src/blockchain/esplora_async.rs | 169 +++++++++++++++++++ crates/bdk/src/blockchain/mod.rs | 67 ++++++++ crates/bdk/src/lib.rs | 2 + crates/esplora/Cargo.toml | 2 +- 7 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 crates/bdk/src/blockchain/async_traits.rs create mode 100644 crates/bdk/src/blockchain/blocking_traits.rs create mode 100644 crates/bdk/src/blockchain/esplora_async.rs create mode 100644 crates/bdk/src/blockchain/mod.rs diff --git a/crates/bdk/Cargo.toml b/crates/bdk/Cargo.toml index dc38fdd63f..a71caea1e0 100644 --- a/crates/bdk/Cargo.toml +++ b/crates/bdk/Cargo.toml @@ -25,6 +25,9 @@ bdk_chain = { path = "../chain", version = "0.5.0", features = ["miniscript", "s hwi = { version = "0.7.0", optional = true, features = [ "miniscript"] } bip39 = { version = "1.0.1", optional = true } +async-trait = { version = "0.1", optional = true, features = [] } +bdk_esplora = { path = "../esplora", optional = true } + [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = "0.2" js-sys = "0.3" @@ -37,6 +40,11 @@ all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] hardware-signer = ["hwi"] test-hardware-signer = ["hardware-signer"] +blockchain = [] +async = ["blockchain", "async-trait"] +blocking = ["blockchain"] +esplora_async = ["async", "bdk_esplora", "bdk_esplora/async"] +esplora_blocking = ["blocking", "bdk_esplora", "bdk_esplora/blocking"] # This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended # for libraries to explicitly include the "getrandom/js" feature, so we only do it when diff --git a/crates/bdk/src/blockchain/async_traits.rs b/crates/bdk/src/blockchain/async_traits.rs new file mode 100644 index 0000000000..4f6086c61e --- /dev/null +++ b/crates/bdk/src/blockchain/async_traits.rs @@ -0,0 +1,115 @@ +//! Async Blockchain +//! +//! This module provides async traits that can be used to fetch [`Update`] data from the bitcoin +//! blockchain and perform other common functions needed by a [`Wallet`] user. +//! +//! Also provides async implementations of these traits for the commonly used blockchain client protocol +//! [Esplora]. Creators of new or custom blockchain clients should implement which ever of these +//! traits that are applicable. +//! +//! [Esplora]: TBD + +use async_trait::async_trait; +use std::boxed::Box; + +use crate::wallet::Update; +use crate::FeeRate; +use crate::Wallet; +use bitcoin::Transaction; + +/// Trait that defines broadcasting a transaction by a blockchain client +#[async_trait] +pub trait Broadcast { + type Error: core::fmt::Debug; + + /// Broadcast a transaction + async fn broadcast(&self, tx: &Transaction) -> Result<(), Self::Error>; +} + +/// Trait that defines a function for estimating the fee rate by a blockchain backend or service. +#[async_trait] +pub trait EstimateFee { + type Error: core::fmt::Debug; + type Target: core::fmt::Debug; + + /// Estimate the fee rate required to confirm a transaction in a given `target` number of blocks. + /// The `target` parameter can be customized with implementation specific features. + async fn estimate_fee(&self, target: Self::Target) -> Result; +} + +/// Trait that defines a function to scan all script pub keys (spks). +#[async_trait] +pub trait ScanSpks { + type Error: core::fmt::Debug; + + /// Iterate through script pub keys (spks) of all [`Wallet`] keychains and scan each spk for + /// transactions. Scanning starts with the lowest derivation index spk and stop scanning after + /// `stop_gap` number of consecutive spks have no transaction history. A Scan is usually done + /// restoring a previously used wallet. It is a special case. Applications should use "sync" + /// style updates after an initial scan or when creating a wallet from new and never used + /// keychains. An ['Update'] is returned with newly found or updated transactions. + /// + /// The returned update must be applied to the wallet and persisted (if a data store is being use). + /// For example: + /// ```no_run + /// # use bdk::{Wallet, wallet::Update}; + /// # let mut wallet: Wallet = todo!(); + /// # let update: Update = todo!(); + /// wallet.apply_update(update).expect("update applied"); + /// wallet.commit().expect("updates commited to db"); + /// ``` + async fn scan_spks(&self, wallet: &Wallet, stop_gap: usize) -> Result; +} + +/// Trait that defines a function to sync the status of script pub keys (spks), UTXOs, and +/// unconfirmed transactions. The data to be synced can be adjusted, in an implementation specific +/// way, by providing a sync mode. +#[async_trait] +pub trait ModalSyncSpks { + type Error: core::fmt::Debug; + type SyncMode: core::fmt::Debug; + + /// Iterate through derived script pub keys (spks) of [`Wallet`] keychains and get their current + /// transaction history. Also iterate through transaction UTXOs and unconfirmed transactions + /// know by the [`Wallet`] to get their current status. An [`Update`] is returned with new + /// or updated transactions and utxos. The data to be synced can be adjusted, in an + /// implementation specific way, by providing a sync mode. + /// + /// The returned update must be applied to the wallet and persisted (if a data store is being use). + /// For example: + /// ```no_run + /// # use bdk::{Wallet, wallet::Update}; + /// # let mut wallet: Wallet = todo!(); + /// # let update: Update = todo!(); + /// wallet.apply_update(update).expect("update applied"); + /// wallet.commit().expect("updates commited to db"); + /// ``` + async fn sync_spks( + &self, + wallet: &Wallet, + sync_mode: Self::SyncMode, + ) -> Result; +} + +/// Trait that defines a function to sync the status of script pub keys (spks), UTXOs, and +/// unconfirmed transactions. +#[async_trait] +pub trait SyncSpks { + type Error: core::fmt::Debug; + + /// Iterate through derived script pub keys (spks) of [`Wallet`] keychains and get their current + /// transaction history. Also iterate through transaction UTXOs and unconfirmed transactions + /// know by the [`Wallet`] to get their current status. An [`Update`] is returned with new + /// or updated transactions and UTXOs. + /// + /// The returned update must be applied to the wallet and persisted (if a data store is being use). + /// For example: + /// ```no_run + /// # use bdk::{Wallet, wallet::Update}; + /// # let mut wallet: Wallet = todo!(); + /// # let update: Update = todo!(); + /// wallet.apply_update(update).expect("update applied"); + /// wallet.commit().expect("updates commited to db"); + /// ``` + async fn sync_spks(&self, wallet: &Wallet) -> Result; +} diff --git a/crates/bdk/src/blockchain/blocking_traits.rs b/crates/bdk/src/blockchain/blocking_traits.rs new file mode 100644 index 0000000000..674bc39a3e --- /dev/null +++ b/crates/bdk/src/blockchain/blocking_traits.rs @@ -0,0 +1,117 @@ +//! Blocking Blockchain +//! +//! This module provides blocking traits that can be used to fetch [`Update`] data from the bitcoin +//! blockchain and perform other common functions needed by a [`Wallet`] user. +//! +//! Also provides blocking implementations of these traits for the commonly used blockchain client protocols +//! [Electrum], [Esplora] and [BitcoinCoreRPC]. Creators of new or custom blockchain clients should +//! implement which ever of these traits that are applicable. +//! +//! [`Update`]: crate::wallet::Update +//! [Electrum]: TBD +//! [Esplora]: TBD +//! [BitcoinCoreRPC]: TBD + +use crate::wallet::Update; +use crate::FeeRate; +use crate::Wallet; +use bitcoin::Transaction; + +/// Trait that defines broadcasting a transaction by a blockchain client +pub trait Broadcast { + type Error: core::fmt::Debug; + + /// Broadcast a transaction + fn broadcast(&self, tx: &Transaction) -> Result<(), Self::Error>; +} + +// /// Which mode to use when calculating the estimated transaction fee. A conservative estimate +// /// satisfies a longer history but potentially returns a higher fee rate and is more likely +// /// to be sufficient for the desired target, but is not as responsive to short term drops in the +// /// prevailing fee market. +// enum EstimateMode { +// /// Provides a lower but possibly less accurate fee rate +// ECONOMICAL, +// /// Provides a higher fee rate that is less responsive to short term drops +// CONSERVATIVE, +// } + +/// Trait that defines a function for estimating the fee rate by a blockchain backend or service. +pub trait EstimateFee { + type Error: core::fmt::Debug; + type Target: core::fmt::Debug; + + /// Estimate the fee rate required to confirm a transaction in a given `target` number of blocks. + /// The `target` parameter can be customized with implementation specific features. + fn estimate_fee(&self, target: Self::Target) -> Result; +} + +/// Trait that defines a function to scan all script pub keys (spks). +pub trait ScanSpks { + type Error: core::fmt::Debug; + + /// Iterate through script pub keys (spks) of all [`Wallet`] keychains and scan each spk for + /// transactions. Scanning starts with the lowest derivation index spk and stop scanning after + /// `stop_gap` number of consecutive spks have no transaction history. A Scan is usually done + /// restoring a previously used wallet. It is a special case. Applications should use "sync" + /// style updates after an initial scan or when creating a wallet from new and never used + /// keychains. An ['Update'] is returned with newly found or updated transactions. + /// + /// The returned update must be applied to the wallet and persisted (if a data store is being use). + /// For example: + /// ```no_run + /// # use bdk::{Wallet, wallet::Update}; + /// # let mut wallet: Wallet = todo!(); + /// # let update: Update = todo!(); + /// wallet.apply_update(update).expect("update applied"); + /// wallet.commit().expect("updates commited to db"); + /// ``` + fn scan_spks(&self, wallet: &Wallet, stop_gap: usize) -> Result; +} + +/// Trait that defines a function to sync the status of script pub keys (spks), UTXOs, and +/// unconfirmed transactions. The data to be synced can be adjusted, in an implementation specific +/// way, by providing a sync mode. +pub trait ModalSyncSpks { + type Error: core::fmt::Debug; + type SyncMode: core::fmt::Debug; + + /// Iterate through derived script pub keys (spks) of [`Wallet`] keychains and get their current + /// transaction history. Also iterate through transaction UTXOs and unconfirmed transactions + /// know by the [`Wallet`] to get their current status. An [`Update`] is returned with new + /// or updated transactions and utxos. The data to be synced can be adjusted, in an + /// implementation specific way, by providing a sync mode. + /// + /// The returned update must be applied to the wallet and persisted (if a data store is being use). + /// For example: + /// ```no_run + /// # use bdk::{Wallet, wallet::Update}; + /// # let mut wallet: Wallet = todo!(); + /// # let update: Update = todo!(); + /// wallet.apply_update(update).expect("update applied"); + /// wallet.commit().expect("updates commited to db"); + /// ``` + fn sync_spks(&self, wallet: &Wallet, sync_mode: Self::SyncMode) -> Result; +} + +/// Trait that defines a function to sync the status of script pub keys (spks), UTXOs, and +/// unconfirmed transactions. +pub trait SyncSpks { + type Error: core::fmt::Debug; + + /// Iterate through derived script pub keys (spks) of [`Wallet`] keychains and get their current + /// transaction history. Also iterate through transaction UTXOs and unconfirmed transactions + /// know by the [`Wallet`] to get their current status. An [`Update`] is returned with new + /// or updated transactions and UTXOs. + /// + /// The returned update must be applied to the wallet and persisted (if a data store is being use). + /// For example: + /// ```no_run + /// # use bdk::{Wallet, wallet::Update}; + /// # let mut wallet: Wallet = todo!(); + /// # let update: Update = todo!(); + /// wallet.apply_update(update).expect("update applied"); + /// wallet.commit().expect("updates commited to db"); + /// ``` + fn sync_spks(&self, wallet: &Wallet) -> Result; +} diff --git a/crates/bdk/src/blockchain/esplora_async.rs b/crates/bdk/src/blockchain/esplora_async.rs new file mode 100644 index 0000000000..f3a4a6f72b --- /dev/null +++ b/crates/bdk/src/blockchain/esplora_async.rs @@ -0,0 +1,169 @@ +use async_trait::async_trait; +use std::boxed::Box; +use std::collections::BTreeMap; +use std::prelude::v1::{ToString, Vec}; + +use bdk_esplora::esplora_client::AsyncClient; +use bdk_esplora::esplora_client::Error as EsploraError; +use bdk_esplora::EsploraAsyncExt; +use bitcoin::{OutPoint, ScriptBuf, Transaction, Txid}; + +use crate::blockchain::async_traits::*; +use crate::blockchain::{EstimateFeeError, SpkSyncMode}; +use crate::wallet::Update; +use crate::{FeeRate, Wallet}; + +pub struct EsploraAsync { + client: AsyncClient, + parallel_requests: usize, +} + +#[async_trait] +impl Broadcast for EsploraAsync { + type Error = EsploraError; + + async fn broadcast(&self, tx: &Transaction) -> Result<(), Self::Error> { + self.client.broadcast(tx).await + } +} + +#[async_trait] +impl EstimateFee for EsploraAsync { + type Error = EstimateFeeError; + type Target = usize; + + async fn estimate_fee(&self, target: Self::Target) -> Result { + let estimates = self + .client + .get_fee_estimates() + .await + .map_err(EstimateFeeError::ClientError)?; + match estimates.get(target.to_string().as_str()) { + None => Err(EstimateFeeError::InsufficientData), + Some(rate) => { + let fee_rate = FeeRate::from_sat_per_vb(*rate as f32); + Ok(fee_rate) + } + } + } +} + +#[async_trait] +impl ScanSpks for EsploraAsync { + type Error = EsploraError; + + async fn scan_spks(&self, wallet: &Wallet, stop_gap: usize) -> Result { + let keychain_spks = wallet + .spks_of_all_keychains() + .into_iter() + .collect::>(); + + // The client scans keychain spks for transaction histories, stopping after `stop_gap` number of + // unused spks is reached. It returns a `TxGraph` update (`graph_update`) and a structure that + // represents the last active spk derivation indices of the keychains. + let (graph_update, last_active_indices) = self + .client + .scan_txs_with_keychains( + keychain_spks, + core::iter::empty(), + core::iter::empty(), + stop_gap, + self.parallel_requests, + ) + .await?; + + let prev_tip = wallet.latest_checkpoint(); + let missing_heights = wallet.tx_graph().missing_heights(wallet.local_chain()); + + let chain_update = self + .client + .update_local_chain(prev_tip.clone(), missing_heights) + .await?; + + let update = Update { + last_active_indices, + graph: graph_update, + chain: Some(chain_update), + }; + Ok(update) + } +} + +#[async_trait] +impl ModalSyncSpks for EsploraAsync { + type Error = EsploraError; + type SyncMode = SpkSyncMode; + + async fn sync_spks( + &self, + wallet: &Wallet, + sync_mode: Self::SyncMode, + ) -> Result { + // Spks, outpoints and txids we want updates on will be accumulated here. + let mut spks: Box> = Box::default(); + let mut outpoints: Box + Send> = + Box::new(core::iter::empty()); + let mut txids: Box + Send> = Box::new(core::iter::empty()); + + // Sync all SPKs know to the wallet + if sync_mode.all_spks { + let all_spks: Vec = wallet + .spk_index() + .all_spks() + .iter() + .map(|((_keychain, _index), script_buf)| ScriptBuf::from(script_buf.as_script())) + .collect(); + spks = Box::new(all_spks); + } + // Sync only unused SPKs + else if sync_mode.unused_spks { + let unused_spks: Vec = wallet + .spk_index() + .unused_spks(..) + .map(|((_keychain, _index), script)| ScriptBuf::from(script)) + .collect(); + spks = Box::new(unused_spks); + } + + // Sync UTXOs + if sync_mode.utxos { + // We want to search for whether the UTXO is spent, and spent by which + // transaction. We provide the outpoint of the UTXO to + // `EsploraExt::update_tx_graph_without_keychain`. + let utxo_outpoints = wallet.list_unspent().map(|utxo| utxo.outpoint); + outpoints = Box::new(utxo_outpoints); + }; + + // Sync unconfirmed TX + if sync_mode.unconfirmed_tx { + // We want to search for whether the unconfirmed transaction is now confirmed. + // We provide the unconfirmed txids to + // `EsploraExt::update_tx_graph_without_keychain`. + let unconfirmed_txids = wallet + .transactions() + .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) + .map(|canonical_tx| canonical_tx.tx_node.txid); + txids = Box::new(unconfirmed_txids); + } + + let graph_update = self + .client + .scan_txs(spks.into_iter(), txids, outpoints, self.parallel_requests) + .await?; + + let prev_tip = wallet.latest_checkpoint(); + let missing_heights = wallet.tx_graph().missing_heights(wallet.local_chain()); + let chain_update = self + .client + .update_local_chain(prev_tip, missing_heights) + .await?; + + let update = Update { + // no update to active indices + last_active_indices: BTreeMap::new(), + graph: graph_update, + chain: Some(chain_update), + }; + Ok(update) + } +} diff --git a/crates/bdk/src/blockchain/mod.rs b/crates/bdk/src/blockchain/mod.rs new file mode 100644 index 0000000000..0d53f11838 --- /dev/null +++ b/crates/bdk/src/blockchain/mod.rs @@ -0,0 +1,67 @@ +//! Blockchain +//! +//! This module provides traits that can be used to fetch [`Update`] data from the bitcoin +//! blockchain and perform other common functions needed by a [`Wallet`] user. There is an async +//! and blocking version of each trait. +//! +//! Also provides are implementations of these traits for the commonly used blockchain client protocols +//! [Electrum], [Esplora] and [BitcoinCoreRPC]. Creators of new or custom blockchain clients should +//! implement which ever of these traits that are applicable. +//! +//! [`Update`]: crate::wallet::Update +//! [Electrum]: TBD +//! [Esplora]: TBD +//! [BitcoinCoreRPC]: TBD + +#[cfg(feature = "async")] +mod async_traits; + +#[cfg(feature = "blocking")] +mod blocking_traits; + +#[cfg(feature = "esplora_async")] +mod esplora_async; + +// sync modes + +/// Defines the options when syncing spks with an Electrum or Esplora blockchain client. +#[derive(Debug)] +pub struct SpkSyncMode { + /// Sync all spks the wallet has ever derived + pub all_spks: bool, + /// Sync only derived spks that have not been used, only applies if `all_spks` is false + pub unused_spks: bool, + /// Sync wallet utxos + pub utxos: bool, + /// Sync unconfirmed transactions + pub unconfirmed_tx: bool, +} + +impl Default for SpkSyncMode { + fn default() -> Self { + Self { + all_spks: false, + unused_spks: true, + utxos: true, + unconfirmed_tx: true, + } + } +} + +// trait errors + +/// Errors that occur when using a blockchain client to get a transaction fee estimate. +#[derive(Debug)] +pub enum EstimateFeeError { + /// Insufficient data available to give an estimated [`FeeRate`] for the requested blocks + InsufficientData, + /// A blockchain client error + ClientError(C), +} + +/// Errors that can occur when using a blockchain client to scan script pub keys (spks). +#[derive(Debug)] +pub enum ScanSpksError { + /// A blockchain client error + ClientError(C), +} diff --git a/crates/bdk/src/lib.rs b/crates/bdk/src/lib.rs index 012a868a61..1de5a7ce57 100644 --- a/crates/bdk/src/lib.rs +++ b/crates/bdk/src/lib.rs @@ -30,6 +30,8 @@ extern crate bip39; #[allow(unused_imports)] #[macro_use] pub(crate) mod error; +#[cfg(feature = "blockchain")] +pub mod blockchain; pub mod descriptor; pub mod keys; pub mod psbt; diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 0fffeda684..40f6f7a36e 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] bdk_chain = { path = "../chain", version = "0.5.0", default-features = false, features = ["serde", "miniscript"] } -esplora-client = { version = "0.6.0", default-features = false } +esplora-client = { git = "https://github.com/bitcoindevkit/rust-esplora-client.git", branch = "master", default-features = false } async-trait = { version = "0.1.66", optional = true } futures = { version = "0.3.26", optional = true }