]> Untitled Git - bdk/commitdiff
[blockchain] Add traits to reuse `Blockchain`s across multiple wallets
authorAlekos Filini <alekos.filini@gmail.com>
Tue, 15 Mar 2022 09:48:00 +0000 (10:48 +0100)
committerAlekos Filini <alekos.filini@gmail.com>
Mon, 9 May 2022 17:34:04 +0000 (19:34 +0200)
Add two new traits:
- `StatelessBlockchain` is used to tag `Blockchain`s that don't have any
  wallet-specic state, i.e. they can be used as-is to sync multiple wallets.
- `BlockchainFactory` is a trait for objects that can build multiple
  blockchains for different descriptors. It's implemented automatically
  for every `Arc<T>` where `T` is a `StatelessBlockchain`. This allows a
  piece of code that deals with multiple sub-wallets to just get a
  `&B: BlockchainFactory` to sync all of them.

These new traits have been implemented for Electrum, Esplora and RPC
(the first two being stateless and the latter having a dedicated
`RpcBlockchainFactory` struct). It hasn't been implemented on the CBF
blockchain, because I don't think it would work in its current form
(it throws away old block filters, so it's hard to go back and rescan).

This is the first step for #549, as BIP47 needs to sync many different
descriptors internally.

It's also very useful for #486.

CHANGELOG.md
src/blockchain/electrum.rs
src/blockchain/esplora/reqwest.rs
src/blockchain/esplora/ureq.rs
src/blockchain/mod.rs
src/blockchain/rpc.rs
src/testutils/mod.rs
src/wallet/mod.rs

index 5cb9f31c3f7378d2efe7f1c813dc73b34ce7a6d5..22152cf4b2bf2d2bf6e4a187ead72c547c120406 100644 (file)
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
 - New MSRV set to `1.56`
+- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
 
 
 ## [v0.18.0] - [v0.17.0]
index 0b8691bc1fb2f95c39cdc59172c190b47602c4ec..f8ac758ce0e6b3f2874d960eac075778963236e5 100644 (file)
@@ -79,6 +79,8 @@ impl Blockchain for ElectrumBlockchain {
     }
 }
 
+impl StatelessBlockchain for ElectrumBlockchain {}
+
 impl GetHeight for ElectrumBlockchain {
     fn get_height(&self) -> Result<u32, Error> {
         // TODO: unsubscribe when added to the client, or is there a better call to use here?
@@ -320,8 +322,67 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
 
 #[cfg(test)]
 #[cfg(feature = "test-electrum")]
-crate::bdk_blockchain_tests! {
-    fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
-        ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
+mod test {
+    use std::sync::Arc;
+
+    use super::*;
+    use crate::database::MemoryDatabase;
+    use crate::testutils::blockchain_tests::TestClient;
+    use crate::wallet::{AddressIndex, Wallet};
+
+    crate::bdk_blockchain_tests! {
+        fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
+            ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
+        }
+    }
+
+    fn get_factory() -> (TestClient, Arc<ElectrumBlockchain>) {
+        let test_client = TestClient::default();
+
+        let factory = Arc::new(ElectrumBlockchain::from(
+            Client::new(&test_client.electrsd.electrum_url).unwrap(),
+        ));
+
+        (test_client, factory)
+    }
+
+    #[test]
+    fn test_electrum_blockchain_factory() {
+        let (_test_client, factory) = get_factory();
+
+        let a = factory.build("aaaaaa", None).unwrap();
+        let b = factory.build("bbbbbb", None).unwrap();
+
+        assert_eq!(
+            a.client.block_headers_subscribe().unwrap().height,
+            b.client.block_headers_subscribe().unwrap().height
+        );
+    }
+
+    #[test]
+    fn test_electrum_blockchain_factory_sync_wallet() {
+        let (mut test_client, factory) = get_factory();
+
+        let db = MemoryDatabase::new();
+        let wallet = Wallet::new(
+            "wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)",
+            None,
+            bitcoin::Network::Regtest,
+            db,
+        )
+        .unwrap();
+
+        let address = wallet.get_address(AddressIndex::New).unwrap();
+
+        let tx = testutils! {
+            @tx ( (@addr address.address) => 50_000 )
+        };
+        test_client.receive(tx);
+
+        factory
+            .sync_wallet(&wallet, None, Default::default())
+            .unwrap();
+
+        assert_eq!(wallet.get_balance().unwrap(), 50_000);
     }
 }
index 2141b8e678d1118463264b6930feeecd2f41076c..f68bdd8a1f46d18d503d7c2f4bb689c4dbf463aa 100644 (file)
@@ -101,6 +101,8 @@ impl Blockchain for EsploraBlockchain {
     }
 }
 
+impl StatelessBlockchain for EsploraBlockchain {}
+
 #[maybe_async]
 impl GetHeight for EsploraBlockchain {
     fn get_height(&self) -> Result<u32, Error> {
index 55f1cf7643bd4884f89cdb380d9c49558234653a..50493f9cb353eca01a908199a264101f1f5d966c 100644 (file)
@@ -98,6 +98,8 @@ impl Blockchain for EsploraBlockchain {
     }
 }
 
+impl StatelessBlockchain for EsploraBlockchain {}
+
 impl GetHeight for EsploraBlockchain {
     fn get_height(&self) -> Result<u32, Error> {
         Ok(self.url_client._get_height()?)
index 714fdf6a95bf000dca46bee718cf0d82f86640e0..cf593c3c83fa88226f5ab83ccc28629b8f1c5466 100644 (file)
@@ -25,7 +25,8 @@ use bitcoin::{Transaction, Txid};
 
 use crate::database::BatchDatabase;
 use crate::error::Error;
-use crate::FeeRate;
+use crate::wallet::{wallet_name_from_descriptor, Wallet};
+use crate::{FeeRate, KeychainKind};
 
 #[cfg(any(
     feature = "electrum",
@@ -164,6 +165,106 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
     fn from_config(config: &Self::Config) -> Result<Self, Error>;
 }
 
+/// Trait for blockchains that don't contain any state
+///
+/// Statless blockchains can be used to sync multiple wallets with different descriptors.
+///
+/// [`BlockchainFactory`] is automatically implemented for `Arc<T>` where `T` is a stateless
+/// blockchain.
+pub trait StatelessBlockchain: Blockchain {}
+
+/// Trait for a factory of blockchains that share the underlying connection or configuration
+#[cfg_attr(
+    not(feature = "async-interface"),
+    doc = r##"
+## Example
+
+This example shows how to sync multiple walles and return the sum of their balances
+
+```no_run
+# use bdk::Error;
+# use bdk::blockchain::*;
+# use bdk::database::*;
+# use bdk::wallet::*;
+# use bdk::*;
+fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
+    Ok(wallets
+        .iter()
+        .map(|w| -> Result<_, Error> {
+            blockchain_factory.sync_wallet(&w, None, SyncOptions::default())?;
+            w.get_balance()
+        })
+        .collect::<Result<Vec<_>, _>>()?
+        .into_iter()
+        .sum())
+}
+```
+"##
+)]
+pub trait BlockchainFactory {
+    /// The type returned when building a blockchain from this factory
+    type Inner: Blockchain;
+
+    /// Build a new blockchain for the given descriptor wallet_name
+    ///
+    /// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
+    /// from the factory. Since it's not possible to override the value to `None`, set it to
+    /// `Some(0)` to rescan from the genesis.
+    fn build(
+        &self,
+        wallet_name: &str,
+        override_skip_blocks: Option<u32>,
+    ) -> Result<Self::Inner, Error>;
+
+    /// Build a new blockchain for a given wallet
+    ///
+    /// Internally uses [`wallet_name_from_descriptor`] to derive the name, and then calls
+    /// [`BlockchainFactory::build`] to create the blockchain instance.
+    fn build_for_wallet<D: BatchDatabase>(
+        &self,
+        wallet: &Wallet<D>,
+        override_skip_blocks: Option<u32>,
+    ) -> Result<Self::Inner, Error> {
+        let wallet_name = wallet_name_from_descriptor(
+            wallet.public_descriptor(KeychainKind::External)?.unwrap(),
+            wallet.public_descriptor(KeychainKind::Internal)?,
+            wallet.network(),
+            wallet.secp_ctx(),
+        )?;
+        self.build(&wallet_name, override_skip_blocks)
+    }
+
+    /// Use [`BlockchainFactory::build_for_wallet`] to get a blockchain, then sync the wallet
+    ///
+    /// This can be used when a new blockchain would only be used to sync a wallet and then
+    /// immediately dropped. Keep in mind that specific blockchain factories may perform slow
+    /// operations to build a blockchain for a given wallet, so if a wallet needs to be synced
+    /// often it's recommended to use [`BlockchainFactory::build_for_wallet`] to reuse the same
+    /// blockchain multiple times.
+    #[cfg(not(any(target_arch = "wasm32", feature = "async-interface")))]
+    #[cfg_attr(
+        docsrs,
+        doc(cfg(not(any(target_arch = "wasm32", feature = "async-interface"))))
+    )]
+    fn sync_wallet<D: BatchDatabase>(
+        &self,
+        wallet: &Wallet<D>,
+        override_skip_blocks: Option<u32>,
+        sync_options: crate::wallet::SyncOptions,
+    ) -> Result<(), Error> {
+        let blockchain = self.build_for_wallet(wallet, override_skip_blocks)?;
+        wallet.sync(&blockchain, sync_options)
+    }
+}
+
+impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
+    type Inner = Self;
+
+    fn build(&self, _wallet_name: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
+        Ok(Arc::clone(self))
+    }
+}
+
 /// Data sent with a progress update over a [`channel`]
 pub type ProgressData = (f32, Option<String>);
 
index 78d166e3a79d270e45441724d6e6dbacea9a153b..7eb0592045aaf70813034cf910511cc13b3ca1d3 100644 (file)
@@ -438,18 +438,127 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
     Ok(result.wallets.into_iter().map(|n| n.name).collect())
 }
 
+/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
+///
+/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
+/// objects for different wallet names and with different rescan heights.
+///
+/// ## Example
+///
+/// ```no_run
+/// # use bdk::bitcoin::Network;
+/// # use bdk::blockchain::BlockchainFactory;
+/// # use bdk::blockchain::rpc::{Auth, RpcBlockchainFactory};
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
+/// let factory = RpcBlockchainFactory {
+///     url: "http://127.0.0.1:18332".to_string(),
+///     auth: Auth::Cookie {
+///         file: "/home/user/.bitcoin/.cookie".into(),
+///     },
+///     network: Network::Testnet,
+///     wallet_name_prefix: Some("prefix-".to_string()),
+///     default_skip_blocks: 100_000,
+/// };
+/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
+/// # Ok(())
+/// # }
+/// ```
+#[derive(Debug, Clone)]
+pub struct RpcBlockchainFactory {
+    /// The bitcoin node url
+    pub url: String,
+    /// The bitcoin node authentication mechanism
+    pub auth: Auth,
+    /// The network we are using (it will be checked the bitcoin node network matches this)
+    pub network: Network,
+    /// The optional prefix used to build the full wallet name for blockchains
+    pub wallet_name_prefix: Option<String>,
+    /// Default number of blocks to skip which will be inherited by blockchain unless overridden
+    pub default_skip_blocks: u32,
+}
+
+impl BlockchainFactory for RpcBlockchainFactory {
+    type Inner = RpcBlockchain;
+
+    fn build(
+        &self,
+        checksum: &str,
+        override_skip_blocks: Option<u32>,
+    ) -> Result<Self::Inner, Error> {
+        RpcBlockchain::from_config(&RpcConfig {
+            url: self.url.clone(),
+            auth: self.auth.clone(),
+            network: self.network,
+            wallet_name: format!(
+                "{}{}",
+                self.wallet_name_prefix.as_ref().unwrap_or(&String::new()),
+                checksum
+            ),
+            skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
+        })
+    }
+}
+
 #[cfg(test)]
 #[cfg(feature = "test-rpc")]
-crate::bdk_blockchain_tests! {
+mod test {
+    use super::*;
+    use crate::testutils::blockchain_tests::TestClient;
+
+    use bitcoin::Network;
+    use bitcoincore_rpc::RpcApi;
+
+    crate::bdk_blockchain_tests! {
+        fn test_instance(test_client: &TestClient) -> RpcBlockchain {
+            let config = RpcConfig {
+                url: test_client.bitcoind.rpc_url(),
+                auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
+                network: Network::Regtest,
+                wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
+                skip_blocks: None,
+            };
+            RpcBlockchain::from_config(&config).unwrap()
+        }
+    }
+
+    fn get_factory() -> (TestClient, RpcBlockchainFactory) {
+        let test_client = TestClient::default();
 
-    fn test_instance(test_client: &TestClient) -> RpcBlockchain {
-        let config = RpcConfig {
+        let factory = RpcBlockchainFactory {
             url: test_client.bitcoind.rpc_url(),
-            auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
+            auth: Auth::Cookie {
+                file: test_client.bitcoind.params.cookie_file.clone(),
+            },
             network: Network::Regtest,
-            wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
-            skip_blocks: None,
+            wallet_name_prefix: Some("prefix-".into()),
+            default_skip_blocks: 0,
         };
-        RpcBlockchain::from_config(&config).unwrap()
+
+        (test_client, factory)
+    }
+
+    #[test]
+    fn test_rpc_blockchain_factory() {
+        let (_test_client, factory) = get_factory();
+
+        let a = factory.build("aaaaaa", None).unwrap();
+        assert_eq!(a.skip_blocks, Some(0));
+        assert_eq!(
+            a.client
+                .get_wallet_info()
+                .expect("Node connection isn't working")
+                .wallet_name,
+            "prefix-aaaaaa"
+        );
+
+        let b = factory.build("bbbbbb", Some(100)).unwrap();
+        assert_eq!(b.skip_blocks, Some(100));
+        assert_eq!(
+            b.client
+                .get_wallet_info()
+                .expect("Node connection isn't working")
+                .wallet_name,
+            "prefix-bbbbbb"
+        );
     }
 }
index b10f1a3ba8f7f5d5246900a41c0a146c36001f75..f05c9df48db74f14a0e4c7899368102c45d399c4 100644 (file)
@@ -267,5 +267,3 @@ macro_rules! testutils {
         (external, internal)
     })
 }
-
-pub use testutils;
index 914d10894efa93d39848dbd0de7c0c32ba343caf..071b16e001b79376e7bdd972e78ebfed23c8ea1f 100644 (file)
@@ -4089,6 +4089,8 @@ pub(crate) mod test {
 }
 
 /// Deterministically generate a unique name given the descriptors defining the wallet
+///
+/// Compatible with [`wallet_name_from_descriptor`]
 pub fn wallet_name_from_descriptor<T>(
     descriptor: T,
     change_descriptor: Option<T>,