Skip to content

Commit a0cf1f6

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 a0cf1f6

File tree

6 files changed

+166
-1
lines changed

6 files changed

+166
-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

+59
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,65 @@ 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+
#[cfg_attr(
177+
not(feature = "async-interface"),
178+
doc = r##"
179+
## Example
180+
181+
This example shows how to sync multiple walles and return the sum of their balances
182+
183+
```no_run
184+
# use bdk::Error;
185+
# use bdk::blockchain::*;
186+
# use bdk::database::*;
187+
# use bdk::wallet::*;
188+
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
189+
Ok(wallets
190+
.iter()
191+
.map(|w| -> Result<_, Error> {
192+
w.sync(&blockchain_factory.build("wallet_1", None)?, SyncOptions::default())?;
193+
w.get_balance()
194+
})
195+
.collect::<Result<Vec<_>, _>>()?
196+
.into_iter()
197+
.sum())
198+
}
199+
```
200+
"##
201+
)]
202+
pub trait BlockchainFactory {
203+
/// The type returned when building a blockchain from this factory
204+
type Inner: Blockchain;
205+
206+
/// Build a new blockchain for the given descriptor checksum
207+
///
208+
/// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
209+
/// from the factory. Since it's not possible to override the value to `None`, set it to
210+
/// `Some(0)` to rescan from the genesis.
211+
fn build(
212+
&self,
213+
checksum: &str,
214+
override_skip_blocks: Option<u32>,
215+
) -> Result<Self::Inner, Error>;
216+
}
217+
218+
impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
219+
type Inner = Self;
220+
221+
fn build(&self, _checksum: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
222+
Ok(Arc::clone(self))
223+
}
224+
}
225+
167226
/// Data sent with a progress update over a [`channel`]
168227
pub type ProgressData = (f32, Option<String>);
169228

src/blockchain/rpc.rs

+99
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,59 @@ 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 {
456+
/// file: "/home/user/.bitcoin/.cookie".to_string(),
457+
/// },
458+
/// network: Network::Testnet,
459+
/// wallet_name_prefix: "prefix-".to_string(),
460+
/// default_skip_blocks: 100_000,
461+
/// };
462+
/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
463+
/// # Ok(())
464+
/// ```
465+
#[derive(Debug, Clone)]
466+
pub struct RpcBlockchainFactory {
467+
/// The bitcoin node url
468+
pub url: String,
469+
/// The bitcoin node authentication mechanism
470+
pub auth: Auth,
471+
/// The network we are using (it will be checked the bitcoin node network matches this)
472+
pub network: Network,
473+
/// The prefix used to build the full wallet name for blockchains
474+
pub wallet_name_prefix: String,
475+
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
476+
pub default_skip_blocks: u32,
477+
}
478+
479+
impl BlockchainFactory for RpcBlockchainFactory {
480+
type Inner = RpcBlockchain;
481+
482+
fn build(
483+
&self,
484+
checksum: &str,
485+
override_skip_blocks: Option<u32>,
486+
) -> Result<Self::Inner, Error> {
487+
Ok(RpcBlockchain::from_config(&RpcConfig {
488+
url: self.url.clone(),
489+
auth: self.auth.clone(),
490+
network: self.network,
491+
wallet_name: format!("{}{}", self.wallet_name_prefix, checksum),
492+
skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
493+
})?)
494+
}
495+
}
496+
444497
#[cfg(test)]
445498
#[cfg(feature = "test-rpc")]
446499
crate::bdk_blockchain_tests! {
@@ -456,3 +509,49 @@ crate::bdk_blockchain_tests! {
456509
RpcBlockchain::from_config(&config).unwrap()
457510
}
458511
}
512+
513+
#[cfg(test)]
514+
#[cfg(feature = "test-rpc")]
515+
mod test {
516+
use super::*;
517+
use crate::blockchain::*;
518+
use crate::testutils::blockchain_tests::TestClient;
519+
520+
use bitcoin::Network;
521+
use bitcoincore_rpc::RpcApi;
522+
523+
#[test]
524+
fn test_rpc_blockchain_factory() {
525+
let test_client = TestClient::default();
526+
527+
let factory = RpcBlockchainFactory {
528+
url: test_client.bitcoind.rpc_url(),
529+
auth: Auth::Cookie {
530+
file: test_client.bitcoind.params.cookie_file.clone(),
531+
},
532+
network: Network::Regtest,
533+
wallet_name_prefix: "prefix-".into(),
534+
default_skip_blocks: 0,
535+
};
536+
537+
let a = factory.build("aaaaaa", None).unwrap();
538+
assert_eq!(a.skip_blocks, Some(0));
539+
assert_eq!(
540+
a.client
541+
.get_wallet_info()
542+
.expect("Node connection is working")
543+
.wallet_name,
544+
"prefix-aaaaaa"
545+
);
546+
547+
let b = factory.build("bbbbbb", Some(100)).unwrap();
548+
assert_eq!(b.skip_blocks, Some(100));
549+
assert_eq!(
550+
a.client
551+
.get_wallet_info()
552+
.expect("Node connection is working")
553+
.wallet_name,
554+
"prefix-bbbbbb"
555+
);
556+
}
557+
}

0 commit comments

Comments
 (0)