Merge bitcoindevkit/bdk#569: [blockchain] Add traits to reuse Blockchain
s across multiple wallets
8795da48397800effeffae8417c6e749ec488212 wallet: Move `wallet_name_from_descriptor` above the tests (Alekos Filini) 9c405e9c70e417dea0e610f9d44e99911d6b4e44 [blockchain] Add traits to reuse `Blockchain`s across multiple wallets (Alekos Filini) 2d83af49051f5d33105315164f3edf7f72f3741f Move testutils macro module before the others (Alekos Filini) Pull request description: ### Description Add three 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. - `StatefulBlockchain` is the opposite of `StatelessBlockchain`: it provides a method to "clone" a `Blockchain` with an updated internal state (a new wallet checksum and, optionally, a different number of blocks to skip from genesis). Potentially this also allows reusing the underlying connection on `Blockchain` types that support it. - `MultiBlockchain` is a generalization of this concept: it's implemented automatically for every type that implements `StatefulBlockchain` and 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: MultiBlockchain` without having to deal with stateful and statless blockchains individually. These new traits have been implemented for Electrum, Esplora and RPC (the first two being stateless and the latter stateful). 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. ### Notes to the reviewers This is still a draft because: - I'm still wondering if these traits should "inherit" from `Blockchain` instead of the less-restrictive `WalletSync` + `GetHeight` which is the bare minimum to sync a wallet - I need to write tests, at least for rpc which is stateful - I need to add examples ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature * [x] I've updated `CHANGELOG.md` ACKs for top commit: rajarshimaitra: tACK 8795da48397800effeffae8417c6e749ec488212 Tree-SHA512: 03f3b63d51199b26a20d58cf64929fd690e2530f873120291a7ffea14a6237334845ceb37bff20d6c5466fca961699460af42134d561935d77b830e2e131df9d
This commit is contained in:
commit
7aa2746c51
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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()?)
|
||||
|
@ -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>);
|
||||
|
||||
|
@ -438,18 +438,127 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
|
||||
Ok(result.wallets.into_iter().map(|n| n.name).collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-rpc")]
|
||||
crate::bdk_blockchain_tests! {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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()
|
||||
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")]
|
||||
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();
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
15
src/lib.rs
15
src/lib.rs
@ -249,6 +249,14 @@ pub extern crate sled;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub extern crate rusqlite;
|
||||
|
||||
// We should consider putting this under a feature flag but we need the macro in doctests so we need
|
||||
// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed.
|
||||
//
|
||||
// Stuff in here is too rough to document atm
|
||||
#[doc(hidden)]
|
||||
#[macro_use]
|
||||
pub mod testutils;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
pub(crate) mod error;
|
||||
@ -277,10 +285,3 @@ pub use wallet::Wallet;
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION", "unknown")
|
||||
}
|
||||
|
||||
// We should consider putting this under a feature flag but we need the macro in doctests so we need
|
||||
// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed.
|
||||
//
|
||||
// Stuff in here is too rough to document atm
|
||||
#[doc(hidden)]
|
||||
pub mod testutils;
|
||||
|
@ -1610,6 +1610,37 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
network: Network,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
T: IntoWalletDescriptor,
|
||||
{
|
||||
//TODO check descriptors contains only public keys
|
||||
let descriptor = descriptor
|
||||
.into_wallet_descriptor(secp, network)?
|
||||
.0
|
||||
.to_string();
|
||||
let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
|
||||
if let Some(change_descriptor) = change_descriptor {
|
||||
let change_descriptor = change_descriptor
|
||||
.into_wallet_descriptor(secp, network)?
|
||||
.0
|
||||
.to_string();
|
||||
wallet_name.push_str(
|
||||
get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(wallet_name)
|
||||
}
|
||||
|
||||
/// Return a fake wallet that appears to be funded for testing.
|
||||
pub fn get_funded_wallet(
|
||||
descriptor: &str,
|
||||
@ -4087,32 +4118,3 @@ pub(crate) mod test {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministically generate a unique name given the descriptors defining the wallet
|
||||
pub fn wallet_name_from_descriptor<T>(
|
||||
descriptor: T,
|
||||
change_descriptor: Option<T>,
|
||||
network: Network,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
T: IntoWalletDescriptor,
|
||||
{
|
||||
//TODO check descriptors contains only public keys
|
||||
let descriptor = descriptor
|
||||
.into_wallet_descriptor(secp, network)?
|
||||
.0
|
||||
.to_string();
|
||||
let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
|
||||
if let Some(change_descriptor) = change_descriptor {
|
||||
let change_descriptor = change_descriptor
|
||||
.into_wallet_descriptor(secp, network)?
|
||||
.0
|
||||
.to_string();
|
||||
wallet_name.push_str(
|
||||
get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(wallet_name)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user