- 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]
}
}
+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?
#[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);
}
}
}
}
+impl StatelessBlockchain for EsploraBlockchain {}
+
#[maybe_async]
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
}
}
+impl StatelessBlockchain for EsploraBlockchain {}
+
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.url_client._get_height()?)
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",
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>);
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"
+ );
}
}
(external, internal)
})
}
-
-pub use testutils;
}
/// 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>,