Skip to content

Commit 7d1d281

Browse files
afilinigp-ngmi
authored andcommitted
[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 8b7d6e0 commit 7d1d281

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6565
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
6666
- 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.
6767
- Added `Wallet::get_signers()`, `Wallet::get_transaction()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
68+
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
6869

6970
### Sync API change
7071

@@ -494,3 +495,4 @@ final transaction is created by calling `finish` on the builder.
494495
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0
495496
[v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
496497
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
498+

src/blockchain/rpc.rs

+143
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,57 @@ impl BlockchainFactory for RpcBlockchainFactory {
873873
}
874874
}
875875

876+
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
877+
///
878+
/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
879+
/// objects for different wallet names and with different rescan heights.
880+
///
881+
/// ## Example
882+
///
883+
/// ```no_run
884+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
885+
/// let factory = RpcBlockchainFactory {
886+
/// url: "http://127.0.0.1:18332",
887+
/// auth: Auth::Cookie { file: "/home/user/.bitcoin/.cookie".to_string() },
888+
/// network: Network::Testnet,
889+
/// wallet_name_prefix: "prefix-".to_string(),
890+
/// default_skip_blocks: 100_000,
891+
/// };
892+
/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
893+
/// # Ok(())
894+
/// ```
895+
#[derive(Debug, Clone)]
896+
pub struct RpcBlockchainFactory {
897+
/// The bitcoin node url
898+
pub url: String,
899+
/// The bitcoin node authentication mechanism
900+
pub auth: Auth,
901+
/// The network we are using (it will be checked the bitcoin node network matches this)
902+
pub network: Network,
903+
/// The prefix used to build the full wallet name for blockchains
904+
pub wallet_name_prefix: String,
905+
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
906+
pub default_skip_blocks: u32,
907+
}
908+
909+
impl BlockchainFactory for RpcBlockchainFactory {
910+
type Inner = RpcBlockchain;
911+
912+
fn build(
913+
&self,
914+
checksum: &str,
915+
override_skip_blocks: Option<u32>,
916+
) -> Result<Self::Inner, Error> {
917+
Ok(RpcBlockchain::from_config(&RpcConfig {
918+
url: self.url.clone(),
919+
auth: self.auth.clone(),
920+
network: self.network,
921+
wallet_name: format!("{}{}", self.wallet_name_prefix, checksum),
922+
skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
923+
})?)
924+
}
925+
}
926+
876927
#[cfg(test)]
877928
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
878929
mod test {
@@ -998,3 +1049,95 @@ mod test {
9981049
});
9991050
}
10001051
}
1052+
1053+
#[cfg(test)]
1054+
#[cfg(feature = "test-rpc")]
1055+
mod test {
1056+
use super::*;
1057+
use crate::blockchain::*;
1058+
use crate::testutils::blockchain_tests::TestClient;
1059+
1060+
use bitcoin::Network;
1061+
use bitcoincore_rpc::RpcApi;
1062+
1063+
#[test]
1064+
fn test_rpc_blockchain_factory() {
1065+
let test_client = TestClient::default();
1066+
1067+
let factory = RpcBlockchainFactory {
1068+
url: test_client.bitcoind.rpc_url(),
1069+
auth: Auth::Cookie {
1070+
file: test_client.bitcoind.params.cookie_file.clone(),
1071+
},
1072+
network: Network::Regtest,
1073+
wallet_name_prefix: "prefix-".into(),
1074+
default_skip_blocks: 0,
1075+
};
1076+
1077+
let a = factory.build("aaaaaa", None).unwrap();
1078+
assert_eq!(a.skip_blocks, Some(0));
1079+
assert_eq!(
1080+
a.client
1081+
.get_wallet_info()
1082+
.expect("Node connection is working")
1083+
.wallet_name,
1084+
"prefix-aaaaaa"
1085+
);
1086+
1087+
let b = factory.build("bbbbbb", Some(100)).unwrap();
1088+
assert_eq!(b.skip_blocks, Some(100));
1089+
assert_eq!(
1090+
a.client
1091+
.get_wallet_info()
1092+
.expect("Node connection is working")
1093+
.wallet_name,
1094+
"prefix-bbbbbb"
1095+
);
1096+
}
1097+
}
1098+
1099+
#[cfg(test)]
1100+
#[cfg(feature = "test-rpc")]
1101+
mod test {
1102+
use super::*;
1103+
use crate::blockchain::*;
1104+
use crate::testutils::blockchain_tests::TestClient;
1105+
1106+
use bitcoin::Network;
1107+
use bitcoincore_rpc::RpcApi;
1108+
1109+
#[test]
1110+
fn test_rpc_blockchain_factory() {
1111+
let test_client = TestClient::default();
1112+
1113+
let factory = RpcBlockchainFactory {
1114+
url: test_client.bitcoind.rpc_url(),
1115+
auth: Auth::Cookie {
1116+
file: test_client.bitcoind.params.cookie_file.clone(),
1117+
},
1118+
network: Network::Regtest,
1119+
wallet_name_prefix: "prefix-".into(),
1120+
default_skip_blocks: 0,
1121+
};
1122+
1123+
let a = factory.build("aaaaaa", None).unwrap();
1124+
assert_eq!(a.skip_blocks, Some(0));
1125+
assert_eq!(
1126+
a.client
1127+
.get_wallet_info()
1128+
.expect("Node connection is working")
1129+
.wallet_name,
1130+
"prefix-aaaaaa"
1131+
);
1132+
1133+
let b = factory.build("bbbbbb", Some(100)).unwrap();
1134+
assert_eq!(b.skip_blocks, Some(100));
1135+
assert_eq!(
1136+
a.client
1137+
.get_wallet_info()
1138+
.expect("Node connection is working")
1139+
.wallet_name,
1140+
"prefix-bbbbbb"
1141+
);
1142+
}
1143+
}

0 commit comments

Comments
 (0)