Skip to content

Commit ae9c01d

Browse files
committed
[blockchain] Add traits to reuse Blockchains 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 bitcoindevkit#549, as BIP47 needs to sync many different descriptors internally. It's also very useful for bitcoindevkit#486.
1 parent 961d6bf commit ae9c01d

File tree

6 files changed

+160
-1
lines changed

6 files changed

+160
-1
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
1414
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
1515
- Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db.
16+
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
1617

1718
### Sync API change
1819

@@ -437,4 +438,4 @@ final transaction is created by calling `finish` on the builder.
437438
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
438439
[v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1
439440
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
440-
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD
441+
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD

src/blockchain/electrum.rs

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ impl Blockchain for ElectrumBlockchain {
7979
}
8080
}
8181

82+
impl StatelessBlockchain for ElectrumBlockchain {}
83+
8284
impl GetHeight for ElectrumBlockchain {
8385
fn get_height(&self) -> Result<u32, Error> {
8486
// TODO: unsubscribe when added to the client, or is there a better call to use here?

src/blockchain/esplora/reqwest.rs

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ impl Blockchain for EsploraBlockchain {
101101
}
102102
}
103103

104+
impl StatelessBlockchain for EsploraBlockchain {}
105+
104106
#[maybe_async]
105107
impl GetHeight for EsploraBlockchain {
106108
fn get_height(&self) -> Result<u32, Error> {

src/blockchain/esplora/ureq.rs

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ impl Blockchain for EsploraBlockchain {
9898
}
9999
}
100100

101+
impl StatelessBlockchain for EsploraBlockchain {}
102+
101103
impl GetHeight for EsploraBlockchain {
102104
fn get_height(&self) -> Result<u32, Error> {
103105
Ok(self.url_client._get_height()?)

src/blockchain/mod.rs

+55
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,61 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
164164
fn from_config(config: &Self::Config) -> Result<Self, Error>;
165165
}
166166

167+
/// Trait for blockchains that don't contain any state
168+
///
169+
/// Statless blockchains can be used to sync multiple wallets with different descriptors.
170+
///
171+
/// [`BlockchainFactory`] is automatically implemented for `Arc<T>` where `T` is a stateless
172+
/// blockchain.
173+
pub trait StatelessBlockchain: Blockchain {}
174+
175+
/// Trait for a factory of blockchains that share the underlying connection or configuration
176+
///
177+
/// ## Example
178+
///
179+
/// This example shows how to sync multiple walles and return the sum of their balances
180+
///
181+
/// ```no_run
182+
/// # use bdk::Error;
183+
/// # use bdk::blockchain::*;
184+
/// # use bdk::database::*;
185+
/// # use bdk::wallet::*;
186+
/// fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
187+
/// Ok(wallets
188+
/// .iter()
189+
/// .map(|w| -> Result<_, Error> {
190+
/// w.sync(&blockchain_factory.build("wallet_1", None)?, SyncOptions::default())?;
191+
/// w.get_balance()
192+
/// })
193+
/// .collect::<Result<Vec<_>, _>>()?
194+
/// .into_iter()
195+
/// .sum())
196+
/// }
197+
/// ```
198+
pub trait BlockchainFactory {
199+
/// The type returned when building a blockchain from this factory
200+
type Inner: Blockchain;
201+
202+
/// Build a new blockchain for the given descriptor checksum
203+
///
204+
/// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
205+
/// from the factory. Since it's not possible to override the value to `None`, set it to
206+
/// `Some(0)` to rescan from the genesis.
207+
fn build(
208+
&self,
209+
checksum: &str,
210+
override_skip_blocks: Option<u32>,
211+
) -> Result<Self::Inner, Error>;
212+
}
213+
214+
impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
215+
type Inner = Self;
216+
217+
fn build(&self, _checksum: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
218+
Ok(Arc::clone(self))
219+
}
220+
}
221+
167222
/// Data sent with a progress update over a [`channel`]
168223
pub type ProgressData = (f32, Option<String>);
169224

src/blockchain/rpc.rs

+97
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,57 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
441441
Ok(result.wallets.into_iter().map(|n| n.name).collect())
442442
}
443443

444+
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
445+
///
446+
/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
447+
/// objects for different wallet names and with different rescan heights.
448+
///
449+
/// ## Example
450+
///
451+
/// ```no_run
452+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
453+
/// let factory = RpcBlockchainFactory {
454+
/// url: "http://127.0.0.1:18332",
455+
/// auth: Auth::Cookie { file: "/home/user/.bitcoin/.cookie".to_string() },
456+
/// network: Network::Testnet,
457+
/// wallet_name_prefix: "prefix-".to_string(),
458+
/// default_skip_blocks: 100_000,
459+
/// };
460+
/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
461+
/// # Ok(())
462+
/// ```
463+
#[derive(Debug, Clone)]
464+
pub struct RpcBlockchainFactory {
465+
/// The bitcoin node url
466+
pub url: String,
467+
/// The bitcoin node authentication mechanism
468+
pub auth: Auth,
469+
/// The network we are using (it will be checked the bitcoin node network matches this)
470+
pub network: Network,
471+
/// The prefix used to build the full wallet name for blockchains
472+
pub wallet_name_prefix: String,
473+
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
474+
pub default_skip_blocks: u32,
475+
}
476+
477+
impl BlockchainFactory for RpcBlockchainFactory {
478+
type Inner = RpcBlockchain;
479+
480+
fn build(
481+
&self,
482+
checksum: &str,
483+
override_skip_blocks: Option<u32>,
484+
) -> Result<Self::Inner, Error> {
485+
Ok(RpcBlockchain::from_config(&RpcConfig {
486+
url: self.url.clone(),
487+
auth: self.auth.clone(),
488+
network: self.network,
489+
wallet_name: format!("{}{}", self.wallet_name_prefix, checksum),
490+
skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
491+
})?)
492+
}
493+
}
494+
444495
#[cfg(test)]
445496
#[cfg(feature = "test-rpc")]
446497
crate::bdk_blockchain_tests! {
@@ -456,3 +507,49 @@ crate::bdk_blockchain_tests! {
456507
RpcBlockchain::from_config(&config).unwrap()
457508
}
458509
}
510+
511+
#[cfg(test)]
512+
#[cfg(feature = "test-rpc")]
513+
mod test {
514+
use super::*;
515+
use crate::blockchain::*;
516+
use crate::testutils::blockchain_tests::TestClient;
517+
518+
use bitcoin::Network;
519+
use bitcoincore_rpc::RpcApi;
520+
521+
#[test]
522+
fn test_rpc_blockchain_factory() {
523+
let test_client = TestClient::default();
524+
525+
let factory = RpcBlockchainFactory {
526+
url: test_client.bitcoind.rpc_url(),
527+
auth: Auth::Cookie {
528+
file: test_client.bitcoind.params.cookie_file.clone(),
529+
},
530+
network: Network::Regtest,
531+
wallet_name_prefix: "prefix-".into(),
532+
default_skip_blocks: 0,
533+
};
534+
535+
let a = factory.build("aaaaaa", None).unwrap();
536+
assert_eq!(a.skip_blocks, Some(0));
537+
assert_eq!(
538+
a.client
539+
.get_wallet_info()
540+
.expect("Node connection is working")
541+
.wallet_name,
542+
"prefix-aaaaaa"
543+
);
544+
545+
let b = factory.build("bbbbbb", Some(100)).unwrap();
546+
assert_eq!(b.skip_blocks, Some(100));
547+
assert_eq!(
548+
a.client
549+
.get_wallet_info()
550+
.expect("Node connection is working")
551+
.wallet_name,
552+
"prefix-bbbbbb"
553+
);
554+
}
555+
}

0 commit comments

Comments
 (0)