[blockchain] Add traits to reuse Blockchain
s across multiple wallets
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.
This commit is contained in:
parent
2d83af4905
commit
9c405e9c70
@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
|
- added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
|
||||||
- New MSRV set to `1.56`
|
- New MSRV set to `1.56`
|
||||||
|
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
|
||||||
|
|
||||||
|
|
||||||
## [v0.18.0] - [v0.17.0]
|
## [v0.18.0] - [v0.17.0]
|
||||||
|
@ -79,6 +79,8 @@ impl Blockchain for ElectrumBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StatelessBlockchain for ElectrumBlockchain {}
|
||||||
|
|
||||||
impl GetHeight for ElectrumBlockchain {
|
impl GetHeight for ElectrumBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
// TODO: unsubscribe when added to the client, or is there a better call to use here?
|
// 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(test)]
|
||||||
#[cfg(feature = "test-electrum")]
|
#[cfg(feature = "test-electrum")]
|
||||||
crate::bdk_blockchain_tests! {
|
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 {
|
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
|
||||||
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,6 +101,8 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StatelessBlockchain for EsploraBlockchain {}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl GetHeight for EsploraBlockchain {
|
impl GetHeight for EsploraBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
|
@ -98,6 +98,8 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StatelessBlockchain for EsploraBlockchain {}
|
||||||
|
|
||||||
impl GetHeight for EsploraBlockchain {
|
impl GetHeight for EsploraBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
Ok(self.url_client._get_height()?)
|
Ok(self.url_client._get_height()?)
|
||||||
|
@ -25,7 +25,8 @@ use bitcoin::{Transaction, Txid};
|
|||||||
|
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::wallet::{wallet_name_from_descriptor, Wallet};
|
||||||
|
use crate::{FeeRate, KeychainKind};
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "electrum",
|
feature = "electrum",
|
||||||
@ -164,6 +165,106 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
|
|||||||
fn from_config(config: &Self::Config) -> Result<Self, Error>;
|
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`]
|
/// Data sent with a progress update over a [`channel`]
|
||||||
pub type ProgressData = (f32, Option<String>);
|
pub type ProgressData = (f32, Option<String>);
|
||||||
|
|
||||||
|
@ -438,10 +438,77 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
|
|||||||
Ok(result.wallets.into_iter().map(|n| n.name).collect())
|
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(test)]
|
||||||
#[cfg(feature = "test-rpc")]
|
#[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 {
|
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
|
||||||
let config = RpcConfig {
|
let config = RpcConfig {
|
||||||
url: test_client.bitcoind.rpc_url(),
|
url: test_client.bitcoind.rpc_url(),
|
||||||
@ -452,4 +519,46 @@ crate::bdk_blockchain_tests! {
|
|||||||
};
|
};
|
||||||
RpcBlockchain::from_config(&config).unwrap()
|
RpcBlockchain::from_config(&config).unwrap()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_factory() -> (TestClient, RpcBlockchainFactory) {
|
||||||
|
let test_client = TestClient::default();
|
||||||
|
|
||||||
|
let factory = RpcBlockchainFactory {
|
||||||
|
url: test_client.bitcoind.rpc_url(),
|
||||||
|
auth: Auth::Cookie {
|
||||||
|
file: test_client.bitcoind.params.cookie_file.clone(),
|
||||||
|
},
|
||||||
|
network: Network::Regtest,
|
||||||
|
wallet_name_prefix: Some("prefix-".into()),
|
||||||
|
default_skip_blocks: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
(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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,5 +267,3 @@ macro_rules! testutils {
|
|||||||
(external, internal)
|
(external, internal)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use testutils;
|
|
||||||
|
@ -4089,6 +4089,8 @@ pub(crate) mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deterministically generate a unique name given the descriptors defining the wallet
|
/// Deterministically generate a unique name given the descriptors defining the wallet
|
||||||
|
///
|
||||||
|
/// Compatible with [`wallet_name_from_descriptor`]
|
||||||
pub fn wallet_name_from_descriptor<T>(
|
pub fn wallet_name_from_descriptor<T>(
|
||||||
descriptor: T,
|
descriptor: T,
|
||||||
change_descriptor: Option<T>,
|
change_descriptor: Option<T>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user