Compare commits
81 Commits
v1.0.0-alp
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7876c8fd06 | ||
|
|
db9fdccc18 | ||
|
|
63e3bbe820 | ||
|
|
b45897e6fe | ||
|
|
92fb6cb373 | ||
|
|
b2f3cacce6 | ||
|
|
c0d7d60a58 | ||
|
|
2945c6be88 | ||
|
|
9ed33c25ea | ||
|
|
b1f861b932 | ||
|
|
a6fdfb2ae4 | ||
|
|
653e4fed6d | ||
|
|
58f27b38eb | ||
|
|
721bb7f519 | ||
|
|
e3cfb84898 | ||
|
|
2ffb65618a | ||
|
|
fb7ff298a4 | ||
|
|
86711d4f46 | ||
|
|
86408b90a5 | ||
|
|
de53d72191 | ||
|
|
9d8023bf56 | ||
|
|
6c8748124f | ||
|
|
537aa03ae0 | ||
|
|
ed117de7a5 | ||
|
|
6a3fb849e8 | ||
|
|
1d294b734d | ||
|
|
0e3e136f6f | ||
|
|
76afccc555 | ||
|
|
4f05441a00 | ||
|
|
8ff99f27df | ||
|
|
b9902936a0 | ||
|
|
66abc73c3d | ||
|
|
de2763a4b8 | ||
|
|
dcd2d4741d | ||
|
|
23538c4039 | ||
|
|
a9f7377934 | ||
|
|
f6dc6890c3 | ||
|
|
22aa534d76 | ||
|
|
d5c0e7200c | ||
|
|
f6218e4741 | ||
|
|
125959976f | ||
|
|
8a33d98db9 | ||
|
|
2703cc6e78 | ||
|
|
db47347472 | ||
|
|
a577c22b12 | ||
|
|
fbe17820dc | ||
|
|
2cda9f44ee | ||
|
|
b6909e133b | ||
|
|
a5fb7fdf50 | ||
|
|
08fac47c29 | ||
|
|
ed3ccc1a9d | ||
|
|
c0374a0eeb | ||
|
|
81de8f6051 | ||
|
|
0f94f24aaf | ||
|
|
4c52f3e08e | ||
|
|
cdfec5f907 | ||
|
|
8e73998cfa | ||
|
|
96a9aa6e63 | ||
|
|
2f22987c9e | ||
|
|
9800f8d88e | ||
|
|
e0bcca32b1 | ||
|
|
d39b319ddf | ||
|
|
a266b4718f | ||
|
|
d87874780b | ||
|
|
d3763e5e37 | ||
|
|
f00de9e0c1 | ||
|
|
d3a14d411d | ||
|
|
52f3955557 | ||
|
|
fac228337c | ||
|
|
daf588f016 | ||
|
|
77d35954c1 | ||
|
|
1269b0610e | ||
|
|
72fe65b65f | ||
|
|
eded1a7ea0 | ||
|
|
519cd75d23 | ||
|
|
a6e613e6b9 | ||
|
|
494d253493 | ||
|
|
886d72e3d5 | ||
|
|
bd62aa0fe1 | ||
|
|
1e99793983 | ||
|
|
e51af49ffa |
2
.github/workflows/cont_integration.yml
vendored
2
.github/workflows/cont_integration.yml
vendored
@@ -118,7 +118,7 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.78.0
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Rust Cache
|
||||
|
||||
@@ -8,6 +8,7 @@ members = [
|
||||
"crates/esplora",
|
||||
"crates/bitcoind_rpc",
|
||||
"crates/hwi",
|
||||
"crates/persist",
|
||||
"crates/testenv",
|
||||
"example-crates/example_cli",
|
||||
"example-crates/example_electrum",
|
||||
|
||||
@@ -41,6 +41,7 @@ The project is split up into several crates in the `/crates` directory:
|
||||
|
||||
- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
|
||||
- [`chain`](./crates/chain): Tools for storing and indexing chain data
|
||||
- [`persist`](./crates/persist): Types that define data persistence of a BDK wallet
|
||||
- [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file.
|
||||
- [`esplora`](./crates/esplora): Extends the [`esplora-client`] crate with methods to fetch chain data from an esplora HTTP server in the form that [`bdk_chain`] and `Wallet` can consume.
|
||||
- [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.9"
|
||||
version = "1.0.0-alpha.11"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
@@ -13,12 +13,14 @@ edition = "2021"
|
||||
rust-version = "1.63"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", default-features = false }
|
||||
rand = "^0.8"
|
||||
miniscript = { version = "11.0.0", features = ["serde"], default-features = false }
|
||||
bitcoin = { version = "0.31.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.12.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_persist = { path = "../persist", version = "0.2.0" }
|
||||
|
||||
# Optional dependencies
|
||||
bip39 = { version = "2.0", optional = true }
|
||||
|
||||
@@ -219,7 +219,7 @@ license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
|
||||
[`Wallet`]: https://docs.rs/bdk/1.0.0-alpha.7/bdk/wallet/struct.Wallet.html
|
||||
[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html
|
||||
[`PersistBackend`]: https://docs.rs/bdk_persist/latest/bdk_persist/trait.PersistBackend.html
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
||||
|
||||
@@ -21,7 +21,6 @@ use bitcoin::Network;
|
||||
use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{KeychainKind, Wallet};
|
||||
|
||||
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
|
||||
@@ -51,7 +50,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
println!(
|
||||
"First derived address from the descriptor: \n{}",
|
||||
wallet.get_address(New)
|
||||
wallet.next_unused_address(KeychainKind::External)?,
|
||||
);
|
||||
|
||||
// BDK also has it's own `Policy` structure to represent the spending condition in a more
|
||||
|
||||
@@ -74,7 +74,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2Pkh;
|
||||
///
|
||||
/// let key =
|
||||
@@ -82,7 +82,9 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)?
|
||||
/// .to_string(),
|
||||
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -102,15 +104,17 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2Wpkh_P2Sh;
|
||||
/// use bdk::wallet::AddressIndex;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(AddressIndex::New).to_string(),
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)?
|
||||
/// .to_string(),
|
||||
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -131,15 +135,17 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2Wpkh;
|
||||
/// use bdk::wallet::AddressIndex::New;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)?
|
||||
/// .to_string(),
|
||||
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -159,7 +165,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2TR;
|
||||
///
|
||||
/// let key =
|
||||
@@ -167,7 +173,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)?
|
||||
/// .to_string(),
|
||||
/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -192,7 +200,6 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
@@ -202,7 +209,7 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -229,7 +236,6 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
@@ -240,7 +246,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -267,7 +273,6 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
@@ -277,7 +282,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -304,7 +309,6 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
@@ -315,7 +319,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -342,7 +346,6 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
@@ -352,7 +355,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -379,7 +382,6 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
@@ -390,7 +392,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -417,7 +419,6 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip86;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
@@ -427,7 +428,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -454,7 +455,6 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip86Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
@@ -465,7 +465,7 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
|
||||
//! # use bdk::wallet::error::CreateTxError;
|
||||
//! # use bdk_chain::PersistBackend;
|
||||
//! # use bdk_persist::PersistBackend;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::coin_selection::decide_change;
|
||||
//! # use anyhow::Error;
|
||||
@@ -92,7 +92,7 @@
|
||||
//! .unwrap();
|
||||
//! let psbt = {
|
||||
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
|
||||
//! builder.add_recipient(to_address.script_pubkey(), 50_000);
|
||||
//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
|
||||
//! builder.finish()?
|
||||
//! };
|
||||
//!
|
||||
|
||||
@@ -47,11 +47,11 @@ impl std::error::Error for MiniscriptPsbtError {}
|
||||
/// Error returned from [`TxBuilder::finish`]
|
||||
///
|
||||
/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
|
||||
pub enum CreateTxError<P> {
|
||||
pub enum CreateTxError {
|
||||
/// There was a problem with the descriptors passed in
|
||||
Descriptor(DescriptorError),
|
||||
/// We were unable to write wallet data to the persistence backend
|
||||
Persist(P),
|
||||
/// We were unable to load wallet data from or write wallet data to the persistence backend
|
||||
Persist(anyhow::Error),
|
||||
/// There was a problem while extracting and manipulating policies
|
||||
Policy(PolicyError),
|
||||
/// Spending policy is not compatible with this [`KeychainKind`]
|
||||
@@ -119,17 +119,14 @@ pub enum CreateTxError<P> {
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
}
|
||||
|
||||
impl<P> fmt::Display for CreateTxError<P>
|
||||
where
|
||||
P: fmt::Display,
|
||||
{
|
||||
impl fmt::Display for CreateTxError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Descriptor(e) => e.fmt(f),
|
||||
Self::Persist(e) => {
|
||||
write!(
|
||||
f,
|
||||
"failed to write wallet data to persistence backend: {}",
|
||||
"failed to load wallet data from or write wallet data to persistence backend: {}",
|
||||
e
|
||||
)
|
||||
}
|
||||
@@ -214,38 +211,38 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<descriptor::error::Error> for CreateTxError<P> {
|
||||
impl From<descriptor::error::Error> for CreateTxError {
|
||||
fn from(err: descriptor::error::Error) -> Self {
|
||||
CreateTxError::Descriptor(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<PolicyError> for CreateTxError<P> {
|
||||
impl From<PolicyError> for CreateTxError {
|
||||
fn from(err: PolicyError) -> Self {
|
||||
CreateTxError::Policy(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<MiniscriptPsbtError> for CreateTxError<P> {
|
||||
impl From<MiniscriptPsbtError> for CreateTxError {
|
||||
fn from(err: MiniscriptPsbtError) -> Self {
|
||||
CreateTxError::MiniscriptPsbt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<psbt::Error> for CreateTxError<P> {
|
||||
impl From<psbt::Error> for CreateTxError {
|
||||
fn from(err: psbt::Error) -> Self {
|
||||
CreateTxError::Psbt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<coin_selection::Error> for CreateTxError<P> {
|
||||
impl From<coin_selection::Error> for CreateTxError {
|
||||
fn from(err: coin_selection::Error) -> Self {
|
||||
CreateTxError::CoinSelection(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}
|
||||
impl std::error::Error for CreateTxError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`Wallet::build_fee_bump`]
|
||||
|
||||
@@ -53,9 +53,9 @@
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
|
||||
use alloc::string::String;
|
||||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
|
||||
use alloc::string::{String, ToString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use miniscript::descriptor::{ShInner, WshInner};
|
||||
@@ -80,9 +80,9 @@ pub struct FullyNodedExport {
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl ToString for FullyNodedExport {
|
||||
fn to_string(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
impl fmt::Display for FullyNodedExport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", serde_json::to_string(self).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ impl FullyNodedExport {
|
||||
///
|
||||
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
|
||||
/// returned will be `0`.
|
||||
pub fn export_wallet<D>(
|
||||
wallet: &Wallet<D>,
|
||||
pub fn export_wallet(
|
||||
wallet: &Wallet,
|
||||
label: &str,
|
||||
include_blockheight: bool,
|
||||
) -> Result<Self, &'static str> {
|
||||
@@ -214,6 +214,7 @@ impl FullyNodedExport {
|
||||
mod test {
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::std::string::ToString;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
||||
@@ -225,7 +226,7 @@ mod test {
|
||||
descriptor: &str,
|
||||
change_descriptor: Option<&str>,
|
||||
network: Network,
|
||||
) -> Wallet<()> {
|
||||
) -> Wallet {
|
||||
let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap();
|
||||
let transaction = Transaction {
|
||||
input: vec![],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
||||
//! # use bdk::wallet::ChangeSet;
|
||||
//! # use bdk::wallet::error::CreateTxError;
|
||||
//! # use bdk::wallet::tx_builder::CreateTx;
|
||||
//! # use bdk_chain::PersistBackend;
|
||||
//! # use bdk_persist::PersistBackend;
|
||||
//! # use anyhow::Error;
|
||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
//! # let mut wallet = doctest_wallet!();
|
||||
@@ -29,7 +29,7 @@
|
||||
//!
|
||||
//! tx_builder
|
||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
||||
//! .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
|
||||
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
||||
//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
//! // Only spend non-change outputs
|
||||
@@ -45,13 +45,12 @@ use core::cell::RefCell;
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use bdk_chain::PersistBackend;
|
||||
use bitcoin::psbt::{self, Psbt};
|
||||
use bitcoin::script::PushBytes;
|
||||
use bitcoin::{absolute, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use super::{ChangeSet, CreateTxError, Wallet};
|
||||
use super::{CreateTxError, Wallet};
|
||||
use crate::collections::{BTreeMap, HashSet};
|
||||
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
|
||||
@@ -85,7 +84,7 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// # use core::str::FromStr;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk_chain::PersistBackend;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
@@ -95,8 +94,8 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder
|
||||
/// .ordering(TxOrdering::Untouched)
|
||||
/// .add_recipient(addr1.script_pubkey(), 50_000)
|
||||
/// .add_recipient(addr2.script_pubkey(), 50_000);
|
||||
/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000))
|
||||
/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000));
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
///
|
||||
@@ -105,7 +104,7 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder.ordering(TxOrdering::Untouched);
|
||||
/// for addr in &[addr1, addr2] {
|
||||
/// builder.add_recipient(addr.script_pubkey(), 50_000);
|
||||
/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000));
|
||||
/// }
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
@@ -124,8 +123,8 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`coin_selection`]: Self::coin_selection
|
||||
#[derive(Debug)]
|
||||
pub struct TxBuilder<'a, D, Cs, Ctx> {
|
||||
pub(crate) wallet: Rc<RefCell<&'a mut Wallet<D>>>,
|
||||
pub struct TxBuilder<'a, Cs, Ctx> {
|
||||
pub(crate) wallet: Rc<RefCell<&'a mut Wallet>>,
|
||||
pub(crate) params: TxParams,
|
||||
pub(crate) coin_selection: Cs,
|
||||
pub(crate) phantom: PhantomData<Ctx>,
|
||||
@@ -176,7 +175,7 @@ impl Default for FeePolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> {
|
||||
fn clone(&self) -> Self {
|
||||
TxBuilder {
|
||||
wallet: self.wallet.clone(),
|
||||
@@ -188,7 +187,7 @@ impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
}
|
||||
|
||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
/// Set a custom fee rate.
|
||||
///
|
||||
/// This method sets the mining fee paid by the transaction as a rate on its size.
|
||||
@@ -275,7 +274,7 @@ impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
///
|
||||
/// let builder = wallet
|
||||
/// .build_tx()
|
||||
/// .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
/// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
|
||||
/// .policy_path(path, KeychainKind::External);
|
||||
///
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
@@ -560,7 +559,7 @@ impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm>(
|
||||
self,
|
||||
coin_selection: P,
|
||||
) -> TxBuilder<'a, D, P, Ctx> {
|
||||
) -> TxBuilder<'a, P, Ctx> {
|
||||
TxBuilder {
|
||||
wallet: self.wallet,
|
||||
params: self.params,
|
||||
@@ -615,16 +614,13 @@ impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
impl<'a, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError> {
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
@@ -715,23 +711,28 @@ impl fmt::Display for AllowShrinkingError {
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AllowShrinkingError {}
|
||||
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> {
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self {
|
||||
self.params.recipients = recipients;
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self {
|
||||
self.params.recipients = recipients
|
||||
.into_iter()
|
||||
.map(|(script, amount)| (script, amount.to_sat()))
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a recipient to the internal list
|
||||
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: u64) -> &mut Self {
|
||||
self.params.recipients.push((script_pubkey, amount));
|
||||
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self {
|
||||
self.params
|
||||
.recipients
|
||||
.push((script_pubkey, amount.to_sat()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data as an output, using OP_RETURN
|
||||
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
|
||||
let script = ScriptBuf::new_op_return(data);
|
||||
self.add_recipient(script, 0u64);
|
||||
self.add_recipient(script, Amount::ZERO);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -762,7 +763,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk::wallet::tx_builder::CreateTx;
|
||||
/// # use bdk_chain::PersistBackend;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
@@ -793,7 +794,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
}
|
||||
|
||||
// methods supported only by bump_fee
|
||||
impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
impl<'a> TxBuilder<'a, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
/// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this
|
||||
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
|
||||
/// will attempt to find a change output to shrink instead.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
|
||||
use bdk::{KeychainKind, LocalOutput, Wallet};
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
@@ -20,7 +20,7 @@ pub fn get_funded_wallet_with_change(
|
||||
change: Option<&str>,
|
||||
) -> (Wallet, bitcoin::Txid) {
|
||||
let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap();
|
||||
let change_address = wallet.get_address(AddressIndex::New).address;
|
||||
let change_address = wallet.peek_address(KeychainKind::External, 0).address;
|
||||
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
|
||||
.expect("address")
|
||||
.require_network(Network::Regtest)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use bdk::bitcoin::{Amount, FeeRate, Psbt, TxIn};
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{psbt, SignOptions};
|
||||
use bdk::{psbt, KeychainKind, SignOptions};
|
||||
use core::str::FromStr;
|
||||
mod common;
|
||||
use common::*;
|
||||
@@ -14,9 +12,9 @@ const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6
|
||||
fn test_psbt_malformed_psbt_input_legacy() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||
let options = SignOptions {
|
||||
@@ -31,9 +29,9 @@ fn test_psbt_malformed_psbt_input_legacy() {
|
||||
fn test_psbt_malformed_psbt_input_segwit() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[1].clone());
|
||||
let options = SignOptions {
|
||||
@@ -47,9 +45,9 @@ fn test_psbt_malformed_psbt_input_segwit() {
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_tx_input() {
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.unsigned_tx.input.push(TxIn::default());
|
||||
let options = SignOptions {
|
||||
@@ -63,9 +61,9 @@ fn test_psbt_malformed_tx_input() {
|
||||
fn test_psbt_sign_with_finalized() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
|
||||
// add a finalized input
|
||||
@@ -84,7 +82,7 @@ fn test_psbt_fee_rate_with_witness_utxo() {
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let addr = wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
@@ -109,7 +107,7 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let addr = wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
@@ -133,7 +131,7 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wpkh_wallet.get_address(New);
|
||||
let addr = wpkh_wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wpkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
@@ -145,7 +143,7 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
||||
assert!(wpkh_psbt.fee_rate().is_none());
|
||||
|
||||
let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = pkh_wallet.get_address(New);
|
||||
let addr = pkh_wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = pkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
@@ -174,7 +172,7 @@ fn test_psbt_multiple_internalkey_signers() {
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet(&desc);
|
||||
let to_spend = wallet.get_balance().total();
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(send_to.script_pubkey()).drain_wallet();
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
@@ -203,7 +201,7 @@ fn test_psbt_multiple_internalkey_signers() {
|
||||
// the prevout we're spending
|
||||
let prevouts = &[TxOut {
|
||||
script_pubkey: send_to.script_pubkey(),
|
||||
value: Amount::from_sat(to_spend),
|
||||
value: to_spend,
|
||||
}];
|
||||
let prevouts = Prevouts::All(prevouts);
|
||||
let input_index = 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_bitcoind_rpc"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -16,11 +16,10 @@ readme = "README.md"
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.31", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.18" }
|
||||
bdk_chain = { path = "../chain", version = "0.12", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default_features = false }
|
||||
anyhow = { version = "1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
|
||||
@@ -4,10 +4,10 @@ use bdk_bitcoind_rpc::Emitter;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Address, Amount, Txid},
|
||||
keychain::Balance,
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
local_chain::{CheckPoint, LocalChain},
|
||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bdk_testenv::TestEnv;
|
||||
use bdk_testenv::{anyhow, TestEnv};
|
||||
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
|
||||
@@ -47,10 +47,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
local_chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?,
|
||||
local_chain.apply_update(emission.checkpoint,)?,
|
||||
BTreeMap::from([(height, Some(hash))]),
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
@@ -95,10 +92,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
local_chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?,
|
||||
local_chain.apply_update(emission.checkpoint,)?,
|
||||
if exp_height == exp_hashes.len() - reorged_blocks.len() {
|
||||
core::iter::once((height, Some(hash)))
|
||||
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
|
||||
@@ -168,10 +162,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?;
|
||||
let _ = chain.apply_update(emission.checkpoint)?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
assert!(indexed_additions.is_empty());
|
||||
}
|
||||
@@ -232,10 +223,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
{
|
||||
let emission = emitter.next_block()?.expect("must get mined block");
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?;
|
||||
let _ = chain.apply_update(emission.checkpoint)?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
assert!(indexed_additions.graph.txs.is_empty());
|
||||
assert!(indexed_additions.graph.txouts.is_empty());
|
||||
@@ -294,8 +282,7 @@ fn process_block(
|
||||
block: Block,
|
||||
block_height: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
recv_chain
|
||||
.apply_update(CheckPoint::from_header(&block.header, block_height).into_update(false))?;
|
||||
recv_chain.apply_update(CheckPoint::from_header(&block.header, block_height))?;
|
||||
let _ = recv_graph.apply_block(block, block_height);
|
||||
Ok(())
|
||||
}
|
||||
@@ -390,7 +377,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * ADDITIONAL_COUNT as u64,
|
||||
confirmed: SEND_AMOUNT * ADDITIONAL_COUNT as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"initial balance must be correct",
|
||||
@@ -404,8 +391,8 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * (ADDITIONAL_COUNT - reorg_count) as u64,
|
||||
trusted_pending: SEND_AMOUNT.to_sat() * reorg_count as u64,
|
||||
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
|
||||
trusted_pending: SEND_AMOUNT * reorg_count as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -26,6 +26,6 @@ rand = "0.8"
|
||||
proptest = "1.2.0"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["bitcoin/std", "miniscript/std"]
|
||||
serde = ["serde_crate", "bitcoin/serde"]
|
||||
default = ["std", "miniscript"]
|
||||
std = ["bitcoin/std", "miniscript?/std"]
|
||||
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
use crate::miniscript::{Descriptor, DescriptorPublicKey};
|
||||
use crate::{
|
||||
alloc::{string::ToString, vec::Vec},
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
};
|
||||
use bitcoin::hashes::{hash_newtype, sha256, Hash};
|
||||
|
||||
hash_newtype! {
|
||||
/// Represents the ID of a descriptor, defined as the sha256 hash of
|
||||
/// the descriptor string, checksum excluded.
|
||||
///
|
||||
/// This is useful for having a fixed-length unique representation of a descriptor,
|
||||
/// in particular, we use it to persist application state changes related to the
|
||||
/// descriptor without having to re-write the whole descriptor each time.
|
||||
///
|
||||
pub struct DescriptorId(pub sha256::Hash);
|
||||
}
|
||||
|
||||
/// A trait to extend the functionality of a miniscript descriptor.
|
||||
pub trait DescriptorExt {
|
||||
/// Returns the minimum value (in satoshis) at which an output is broadcastable.
|
||||
/// Panics if the descriptor wildcard is hardened.
|
||||
fn dust_value(&self) -> u64;
|
||||
|
||||
/// Returns the descriptor id, calculated as the sha256 of the descriptor, checksum not
|
||||
/// included.
|
||||
fn descriptor_id(&self) -> DescriptorId;
|
||||
}
|
||||
|
||||
impl DescriptorExt for Descriptor<DescriptorPublicKey> {
|
||||
@@ -15,4 +34,11 @@ impl DescriptorExt for Descriptor<DescriptorPublicKey> {
|
||||
.dust_value()
|
||||
.to_sat()
|
||||
}
|
||||
|
||||
fn descriptor_id(&self) -> DescriptorId {
|
||||
let desc = self.to_string();
|
||||
let desc_without_checksum = desc.split('#').next().expect("Must be here");
|
||||
let descriptor_bytes = <Vec<u8>>::from(desc_without_checksum.as_bytes());
|
||||
DescriptorId(sha256::Hash::hash(&descriptor_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use alloc::vec::Vec;
|
||||
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
|
||||
|
||||
use crate::{
|
||||
keychain,
|
||||
tx_graph::{self, TxGraph},
|
||||
Anchor, AnchorFromBlockPosition, Append, BlockId,
|
||||
};
|
||||
@@ -320,8 +319,9 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, K> From<keychain::ChangeSet<K>> for ChangeSet<A, keychain::ChangeSet<K>> {
|
||||
fn from(indexer: keychain::ChangeSet<K>) -> Self {
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<A, K> From<crate::keychain::ChangeSet<K>> for ChangeSet<A, crate::keychain::ChangeSet<K>> {
|
||||
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
indexer,
|
||||
|
||||
@@ -10,77 +10,12 @@
|
||||
//!
|
||||
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
|
||||
|
||||
use crate::{collections::BTreeMap, Append};
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod txout_index;
|
||||
use bitcoin::Amount;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use txout_index::*;
|
||||
|
||||
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
|
||||
/// It maps each keychain `K` to its last revealed index.
|
||||
///
|
||||
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet`]s are
|
||||
/// monotone in that they will never decrease the revealed derivation index.
|
||||
///
|
||||
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
|
||||
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(
|
||||
crate = "serde_crate",
|
||||
bound(
|
||||
deserialize = "K: Ord + serde::Deserialize<'de>",
|
||||
serialize = "K: Ord + serde::Serialize"
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[must_use]
|
||||
pub struct ChangeSet<K>(pub BTreeMap<K, u32>);
|
||||
|
||||
impl<K> ChangeSet<K> {
|
||||
/// Get the inner map of the keychain to its new derivation index.
|
||||
pub fn as_inner(&self) -> &BTreeMap<K, u32> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord> Append for ChangeSet<K> {
|
||||
/// Append another [`ChangeSet`] into self.
|
||||
///
|
||||
/// If the keychain already exists, increase the index when the other's index > self's index.
|
||||
/// If the keychain did not exist, append the new keychain.
|
||||
fn append(&mut self, mut other: Self) {
|
||||
self.0.iter_mut().for_each(|(key, index)| {
|
||||
if let Some(other_index) = other.0.remove(key) {
|
||||
*index = other_index.max(*index);
|
||||
}
|
||||
});
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
|
||||
/// Returns whether the changeset are empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Default for ChangeSet<K> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
|
||||
fn as_ref(&self) -> &BTreeMap<K, u32> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Balance, differentiated into various categories.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[cfg_attr(
|
||||
@@ -90,13 +25,13 @@ impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
|
||||
)]
|
||||
pub struct Balance {
|
||||
/// All coinbase outputs not yet matured
|
||||
pub immature: u64,
|
||||
pub immature: Amount,
|
||||
/// Unconfirmed UTXOs generated by a wallet tx
|
||||
pub trusted_pending: u64,
|
||||
pub trusted_pending: Amount,
|
||||
/// Unconfirmed UTXOs received from an external wallet
|
||||
pub untrusted_pending: u64,
|
||||
pub untrusted_pending: Amount,
|
||||
/// Confirmed and immediately spendable balance
|
||||
pub confirmed: u64,
|
||||
pub confirmed: Amount,
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
@@ -104,12 +39,12 @@ impl Balance {
|
||||
///
|
||||
/// This is the balance you can spend right now that shouldn't get cancelled via another party
|
||||
/// double spending it.
|
||||
pub fn trusted_spendable(&self) -> u64 {
|
||||
pub fn trusted_spendable(&self) -> Amount {
|
||||
self.confirmed + self.trusted_pending
|
||||
}
|
||||
|
||||
/// Get the whole balance visible to the wallet.
|
||||
pub fn total(&self) -> u64 {
|
||||
pub fn total(&self) -> Amount {
|
||||
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
|
||||
}
|
||||
}
|
||||
@@ -136,40 +71,3 @@ impl core::ops::Add for Balance {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn append_keychain_derivation_indices() {
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
|
||||
enum Keychain {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
let mut lhs_di = BTreeMap::<Keychain, u32>::default();
|
||||
let mut rhs_di = BTreeMap::<Keychain, u32>::default();
|
||||
lhs_di.insert(Keychain::One, 7);
|
||||
lhs_di.insert(Keychain::Two, 0);
|
||||
rhs_di.insert(Keychain::One, 3);
|
||||
rhs_di.insert(Keychain::Two, 5);
|
||||
lhs_di.insert(Keychain::Three, 3);
|
||||
rhs_di.insert(Keychain::Four, 4);
|
||||
|
||||
let mut lhs = ChangeSet(lhs_di);
|
||||
let rhs = ChangeSet(rhs_di);
|
||||
lhs.append(rhs);
|
||||
|
||||
// Exiting index doesn't update if the new index in `other` is lower than `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::One), Some(&7));
|
||||
// Existing index updates if the new index in `other` is higher than `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Two), Some(&5));
|
||||
// Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Three), Some(&3));
|
||||
// New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Four), Some(&4));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,6 @@ pub use tx_data_traits::*;
|
||||
pub use tx_graph::TxGraph;
|
||||
mod chain_oracle;
|
||||
pub use chain_oracle::*;
|
||||
mod persist;
|
||||
pub use persist::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod example_utils;
|
||||
@@ -46,11 +44,12 @@ pub use miniscript;
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod descriptor_ext;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use descriptor_ext::DescriptorExt;
|
||||
pub use descriptor_ext::{DescriptorExt, DescriptorId};
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod spk_iter;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use spk_iter::*;
|
||||
pub mod spk_client;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
|
||||
@@ -96,16 +96,6 @@ impl CheckPoint {
|
||||
.expect("must construct checkpoint")
|
||||
}
|
||||
|
||||
/// Convenience method to convert the [`CheckPoint`] into an [`Update`].
|
||||
///
|
||||
/// For more information, refer to [`Update`].
|
||||
pub fn into_update(self, introduce_older_blocks: bool) -> Update {
|
||||
Update {
|
||||
tip: self,
|
||||
introduce_older_blocks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts another checkpoint onto the linked list representing the blockchain.
|
||||
///
|
||||
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
|
||||
@@ -187,6 +177,82 @@ impl CheckPoint {
|
||||
core::ops::Bound::Unbounded => true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts `block_id` at its height within the chain.
|
||||
///
|
||||
/// The effect of `insert` depends on whether a height already exists. If it doesn't the
|
||||
/// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after
|
||||
/// it. If the height already existed and has a conflicting block hash then it will be purged
|
||||
/// along with all block followin it. The returned chain will have a tip of the `block_id`
|
||||
/// passed in. Of course, if the `block_id` was already present then this just returns `self`.
|
||||
#[must_use]
|
||||
pub fn insert(self, block_id: BlockId) -> Self {
|
||||
assert_ne!(block_id.height, 0, "cannot insert the genesis block");
|
||||
|
||||
let mut cp = self.clone();
|
||||
let mut tail = vec![];
|
||||
let base = loop {
|
||||
if cp.height() == block_id.height {
|
||||
if cp.hash() == block_id.hash {
|
||||
return self;
|
||||
}
|
||||
// if we have a conflict we just return the inserted block because the tail is by
|
||||
// implication invalid.
|
||||
tail = vec![];
|
||||
break cp.prev().expect("can't be called on genesis block");
|
||||
}
|
||||
|
||||
if cp.height() < block_id.height {
|
||||
break cp;
|
||||
}
|
||||
|
||||
tail.push(cp.block_id());
|
||||
cp = cp.prev().expect("will break before genesis block");
|
||||
};
|
||||
|
||||
base.extend(core::iter::once(block_id).chain(tail.into_iter().rev()))
|
||||
.expect("tail is in order")
|
||||
}
|
||||
|
||||
/// Apply `changeset` to the checkpoint.
|
||||
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
// changes after point of agreement
|
||||
let mut extension = BTreeMap::default();
|
||||
// point of agreement
|
||||
let mut base: Option<CheckPoint> = None;
|
||||
|
||||
for cp in self.iter() {
|
||||
if cp.height() >= start_height {
|
||||
extension.insert(cp.height(), cp.hash());
|
||||
} else {
|
||||
base = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (&height, &hash) in changeset {
|
||||
match hash {
|
||||
Some(hash) => {
|
||||
extension.insert(height, hash);
|
||||
}
|
||||
None => {
|
||||
extension.remove(&height);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let new_tip = match base {
|
||||
Some(base) => base
|
||||
.extend(extension.into_iter().map(BlockId::from))
|
||||
.expect("extension is strictly greater than base"),
|
||||
None => LocalChain::from_blocks(extension)?.tip(),
|
||||
};
|
||||
self = new_tip;
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over checkpoints backwards.
|
||||
@@ -199,7 +265,7 @@ impl Iterator for CheckPointIter {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current = self.current.clone()?;
|
||||
self.current = current.prev.clone();
|
||||
self.current.clone_from(¤t.prev);
|
||||
Some(CheckPoint(current))
|
||||
}
|
||||
}
|
||||
@@ -215,31 +281,6 @@ impl IntoIterator for CheckPoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to update [`LocalChain`].
|
||||
///
|
||||
/// This is used as input for [`LocalChain::apply_update`]. It contains the update's chain `tip` and
|
||||
/// a flag `introduce_older_blocks` which signals whether this update intends to introduce missing
|
||||
/// blocks to the original chain.
|
||||
///
|
||||
/// Block-by-block syncing mechanisms would typically create updates that builds upon the previous
|
||||
/// tip. In this case, `introduce_older_blocks` would be `false`.
|
||||
///
|
||||
/// Script-pubkey based syncing mechanisms may not introduce transactions in a chronological order
|
||||
/// so some updates require introducing older blocks (to anchor older transactions). For
|
||||
/// script-pubkey based syncing, `introduce_older_blocks` would typically be `true`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Update {
|
||||
/// The update chain's new tip.
|
||||
pub tip: CheckPoint,
|
||||
|
||||
/// Whether the update allows for introducing older blocks.
|
||||
///
|
||||
/// Refer to [struct-level documentation] for more.
|
||||
///
|
||||
/// [struct-level documentation]: Update
|
||||
pub introduce_older_blocks: bool,
|
||||
}
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalChain {
|
||||
@@ -318,7 +359,7 @@ impl LocalChain {
|
||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||
/// all of the same chain.
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
||||
if blocks.get(&0).is_none() {
|
||||
if !blocks.contains_key(&0) {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
@@ -347,36 +388,22 @@ impl LocalChain {
|
||||
|
||||
/// Applies the given `update` to the chain.
|
||||
///
|
||||
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
|
||||
/// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`.
|
||||
///
|
||||
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
|
||||
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
|
||||
/// the existing chain and invalidate the block after it (if it exists) by including a block at
|
||||
/// the same height but with a different hash to explicitly exclude it as a connection point.
|
||||
///
|
||||
/// Additionally, an empty chain can be updated with any chain, and a chain with a single block
|
||||
/// can have it's block invalidated by an update chain with a block at the same height but
|
||||
/// different hash.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error will occur if the update does not correctly connect with `self`.
|
||||
///
|
||||
/// Refer to [`Update`] for more about the update struct.
|
||||
///
|
||||
/// [module-level documentation]: crate::local_chain
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
|
||||
let changeset = merge_chains(
|
||||
self.tip.clone(),
|
||||
update.tip.clone(),
|
||||
update.introduce_older_blocks,
|
||||
)?;
|
||||
// `._check_index_is_consistent_with_tip` and `._check_changeset_is_applied` is called in
|
||||
// `.apply_changeset`
|
||||
self.apply_changeset(&changeset)
|
||||
.map_err(|_| CannotConnectError {
|
||||
try_include_height: 0,
|
||||
})?;
|
||||
pub fn apply_update(&mut self, update: CheckPoint) -> Result<ChangeSet, CannotConnectError> {
|
||||
let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?;
|
||||
self.tip = new_tip;
|
||||
self._check_changeset_is_applied(&changeset);
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
@@ -428,11 +455,8 @@ impl LocalChain {
|
||||
conn => Some(conn),
|
||||
};
|
||||
|
||||
let update = Update {
|
||||
tip: CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
|
||||
.expect("block ids must be in order"),
|
||||
introduce_older_blocks: false,
|
||||
};
|
||||
let update = CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
|
||||
.expect("block ids must be in order");
|
||||
|
||||
self.apply_update(update)
|
||||
.map_err(ApplyHeaderError::CannotConnect)
|
||||
@@ -471,43 +495,10 @@ impl LocalChain {
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
// changes after point of agreement
|
||||
let mut extension = BTreeMap::default();
|
||||
// point of agreement
|
||||
let mut base: Option<CheckPoint> = None;
|
||||
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() >= start_height {
|
||||
extension.insert(cp.height(), cp.hash());
|
||||
} else {
|
||||
base = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (&height, &hash) in changeset {
|
||||
match hash {
|
||||
Some(hash) => {
|
||||
extension.insert(height, hash);
|
||||
}
|
||||
None => {
|
||||
extension.remove(&height);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let new_tip = match base {
|
||||
Some(base) => base
|
||||
.extend(extension.into_iter().map(BlockId::from))
|
||||
.expect("extension is strictly greater than base"),
|
||||
None => LocalChain::from_blocks(extension)?.tip(),
|
||||
};
|
||||
self.tip = new_tip;
|
||||
|
||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||
}
|
||||
|
||||
let old_tip = self.tip.clone();
|
||||
let new_tip = old_tip.apply_changeset(changeset)?;
|
||||
self.tip = new_tip;
|
||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -730,14 +721,17 @@ impl core::fmt::Display for ApplyHeaderError {
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ApplyHeaderError {}
|
||||
|
||||
/// Applies `update_tip` onto `original_tip`.
|
||||
///
|
||||
/// On success, a tuple is returned `(changeset, can_replace)`. If `can_replace` is true, then the
|
||||
/// `update_tip` can replace the `original_tip`.
|
||||
fn merge_chains(
|
||||
original_tip: CheckPoint,
|
||||
update_tip: CheckPoint,
|
||||
introduce_older_blocks: bool,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
) -> Result<(CheckPoint, ChangeSet), CannotConnectError> {
|
||||
let mut changeset = ChangeSet::default();
|
||||
let mut orig = original_tip.into_iter();
|
||||
let mut update = update_tip.into_iter();
|
||||
let mut orig = original_tip.iter();
|
||||
let mut update = update_tip.iter();
|
||||
let mut curr_orig = None;
|
||||
let mut curr_update = None;
|
||||
let mut prev_orig: Option<CheckPoint> = None;
|
||||
@@ -746,6 +740,12 @@ fn merge_chains(
|
||||
let mut prev_orig_was_invalidated = false;
|
||||
let mut potentially_invalidated_heights = vec![];
|
||||
|
||||
// If we can, we want to return the update tip as the new tip because this allows checkpoints
|
||||
// in multiple locations to keep the same `Arc` pointers when they are being updated from each
|
||||
// other using this function. We can do this as long as long as the update contains every
|
||||
// block's height of the original chain.
|
||||
let mut is_update_height_superset_of_original = true;
|
||||
|
||||
// To find the difference between the new chain and the original we iterate over both of them
|
||||
// from the tip backwards in tandem. We always dealing with the highest one from either chain
|
||||
// first and move to the next highest. The crucial logic is applied when they have blocks at the
|
||||
@@ -771,6 +771,8 @@ fn merge_chains(
|
||||
prev_orig_was_invalidated = false;
|
||||
prev_orig = curr_orig.take();
|
||||
|
||||
is_update_height_superset_of_original = false;
|
||||
|
||||
// OPTIMIZATION: we have run out of update blocks so we don't need to continue
|
||||
// iterating because there's no possibility of adding anything to changeset.
|
||||
if u.is_none() {
|
||||
@@ -793,12 +795,20 @@ fn merge_chains(
|
||||
}
|
||||
point_of_agreement_found = true;
|
||||
prev_orig_was_invalidated = false;
|
||||
// OPTIMIZATION 1 -- If we know that older blocks cannot be introduced without
|
||||
// invalidation, we can break after finding the point of agreement.
|
||||
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
|
||||
// can guarantee that no older blocks are introduced.
|
||||
if !introduce_older_blocks || Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
|
||||
return Ok(changeset);
|
||||
if Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
|
||||
if is_update_height_superset_of_original {
|
||||
return Ok((update_tip, changeset));
|
||||
} else {
|
||||
let new_tip =
|
||||
original_tip.apply_changeset(&changeset).map_err(|_| {
|
||||
CannotConnectError {
|
||||
try_include_height: 0,
|
||||
}
|
||||
})?;
|
||||
return Ok((new_tip, changeset));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We have an invalidation height so we set the height to the updated hash and
|
||||
@@ -832,5 +842,10 @@ fn merge_chains(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
let new_tip = original_tip
|
||||
.apply_changeset(&changeset)
|
||||
.map_err(|_| CannotConnectError {
|
||||
try_include_height: 0,
|
||||
})?;
|
||||
Ok((new_tip, changeset))
|
||||
}
|
||||
|
||||
446
crates/chain/src/spk_client.rs
Normal file
446
crates/chain/src/spk_client.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
//! Helper types for spk-based blockchain clients.
|
||||
|
||||
use crate::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
local_chain::CheckPoint,
|
||||
ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use alloc::{boxed::Box, sync::Arc, vec::Vec};
|
||||
use bitcoin::{OutPoint, Script, ScriptBuf, Transaction, Txid};
|
||||
use core::{fmt::Debug, marker::PhantomData, ops::RangeBounds};
|
||||
|
||||
/// A cache of [`Arc`]-wrapped full transactions, identified by their [`Txid`]s.
|
||||
///
|
||||
/// This is used by the chain-source to avoid re-fetching full transactions.
|
||||
pub type TxCache = HashMap<Txid, Arc<Transaction>>;
|
||||
|
||||
/// Data required to perform a spk-based blockchain client sync.
|
||||
///
|
||||
/// A client sync fetches relevant chain data for a known list of scripts, transaction ids and
|
||||
/// outpoints. The sync process also updates the chain from the given [`CheckPoint`].
|
||||
pub struct SyncRequest {
|
||||
/// A checkpoint for the current chain [`LocalChain::tip`].
|
||||
/// The sync process will return a new chain update that extends this tip.
|
||||
///
|
||||
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
|
||||
pub chain_tip: CheckPoint,
|
||||
/// Cache of full transactions, so the chain-source can avoid re-fetching.
|
||||
pub tx_cache: TxCache,
|
||||
/// Transactions that spend from or to these indexed script pubkeys.
|
||||
pub spks: Box<dyn ExactSizeIterator<Item = ScriptBuf> + Send>,
|
||||
/// Transactions with these txids.
|
||||
pub txids: Box<dyn ExactSizeIterator<Item = Txid> + Send>,
|
||||
/// Transactions with these outpoints or spent from these outpoints.
|
||||
pub outpoints: Box<dyn ExactSizeIterator<Item = OutPoint> + Send>,
|
||||
}
|
||||
|
||||
impl SyncRequest {
|
||||
/// Construct a new [`SyncRequest`] from a given `cp` tip.
|
||||
pub fn from_chain_tip(cp: CheckPoint) -> Self {
|
||||
Self {
|
||||
chain_tip: cp,
|
||||
tx_cache: TxCache::new(),
|
||||
spks: Box::new(core::iter::empty()),
|
||||
txids: Box::new(core::iter::empty()),
|
||||
outpoints: Box::new(core::iter::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add to the [`TxCache`] held by the request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn cache_txs<T>(mut self, full_txs: impl IntoIterator<Item = (Txid, T)>) -> Self
|
||||
where
|
||||
T: Into<Arc<Transaction>>,
|
||||
{
|
||||
self.tx_cache = full_txs
|
||||
.into_iter()
|
||||
.map(|(txid, tx)| (txid, tx.into()))
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add all transactions from [`TxGraph`] into the [`TxCache`].
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn cache_graph_txs<A>(self, graph: &TxGraph<A>) -> Self {
|
||||
self.cache_txs(graph.full_txs().map(|tx_node| (tx_node.txid, tx_node.tx)))
|
||||
}
|
||||
|
||||
/// Set the [`Script`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_spks(
|
||||
mut self,
|
||||
spks: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = ScriptBuf> + Send + 'static>,
|
||||
) -> Self {
|
||||
self.spks = Box::new(spks.into_iter());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Txid`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_txids(
|
||||
mut self,
|
||||
txids: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = Txid> + Send + 'static>,
|
||||
) -> Self {
|
||||
self.txids = Box::new(txids.into_iter());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`OutPoint`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_outpoints(
|
||||
mut self,
|
||||
outpoints: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = OutPoint> + Send + 'static,
|
||||
>,
|
||||
) -> Self {
|
||||
self.outpoints = Box::new(outpoints.into_iter());
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`Script`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_spks(
|
||||
mut self,
|
||||
spks: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = ScriptBuf> + Send + 'static,
|
||||
Item = ScriptBuf,
|
||||
>,
|
||||
) -> Self {
|
||||
self.spks = Box::new(ExactSizeChain::new(self.spks, spks.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`Txid`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_txids(
|
||||
mut self,
|
||||
txids: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = Txid> + Send + 'static,
|
||||
Item = Txid,
|
||||
>,
|
||||
) -> Self {
|
||||
self.txids = Box::new(ExactSizeChain::new(self.txids, txids.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`OutPoint`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_outpoints(
|
||||
mut self,
|
||||
outpoints: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = OutPoint> + Send + 'static,
|
||||
Item = OutPoint,
|
||||
>,
|
||||
) -> Self {
|
||||
self.outpoints = Box::new(ExactSizeChain::new(self.outpoints, outpoints.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for [`Script`]s previously added to this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_spks(
|
||||
mut self,
|
||||
mut inspect: impl FnMut(&Script) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.spks = Box::new(self.spks.inspect(move |spk| inspect(spk)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for [`Txid`]s previously added to this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_txids(mut self, mut inspect: impl FnMut(&Txid) + Send + Sync + 'static) -> Self {
|
||||
self.txids = Box::new(self.txids.inspect(move |txid| inspect(txid)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for [`OutPoint`]s previously added to this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_outpoints(
|
||||
mut self,
|
||||
mut inspect: impl FnMut(&OutPoint) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.outpoints = Box::new(self.outpoints.inspect(move |op| inspect(op)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Populate the request with revealed script pubkeys from `index` with the given `spk_range`.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[cfg(feature = "miniscript")]
|
||||
#[must_use]
|
||||
pub fn populate_with_revealed_spks<K: Clone + Ord + Debug + Send + Sync>(
|
||||
self,
|
||||
index: &crate::keychain::KeychainTxOutIndex<K>,
|
||||
spk_range: impl RangeBounds<K>,
|
||||
) -> Self {
|
||||
use alloc::borrow::ToOwned;
|
||||
self.chain_spks(
|
||||
index
|
||||
.revealed_spks(spk_range)
|
||||
.map(|(_, _, spk)| spk.to_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Data returned from a spk-based blockchain client sync.
|
||||
///
|
||||
/// See also [`SyncRequest`].
|
||||
pub struct SyncResult<A = ConfirmationTimeHeightAnchor> {
|
||||
/// The update to apply to the receiving [`TxGraph`].
|
||||
pub graph_update: TxGraph<A>,
|
||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||
pub chain_update: CheckPoint,
|
||||
}
|
||||
|
||||
/// Data required to perform a spk-based blockchain client full scan.
|
||||
///
|
||||
/// A client full scan iterates through all the scripts for the given keychains, fetching relevant
|
||||
/// data until some stop gap number of scripts is found that have no data. This operation is
|
||||
/// generally only used when importing or restoring previously used keychains in which the list of
|
||||
/// used scripts is not known. The full scan process also updates the chain from the given [`CheckPoint`].
|
||||
pub struct FullScanRequest<K> {
|
||||
/// A checkpoint for the current [`LocalChain::tip`].
|
||||
/// The full scan process will return a new chain update that extends this tip.
|
||||
///
|
||||
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
|
||||
pub chain_tip: CheckPoint,
|
||||
/// Cache of full transactions, so the chain-source can avoid re-fetching.
|
||||
pub tx_cache: TxCache,
|
||||
/// Iterators of script pubkeys indexed by the keychain index.
|
||||
pub spks_by_keychain: BTreeMap<K, Box<dyn Iterator<Item = (u32, ScriptBuf)> + Send>>,
|
||||
}
|
||||
|
||||
impl<K: Ord + Clone> FullScanRequest<K> {
|
||||
/// Construct a new [`FullScanRequest`] from a given `chain_tip`.
|
||||
#[must_use]
|
||||
pub fn from_chain_tip(chain_tip: CheckPoint) -> Self {
|
||||
Self {
|
||||
chain_tip,
|
||||
tx_cache: TxCache::new(),
|
||||
spks_by_keychain: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add to the [`TxCache`] held by the request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn cache_txs<T>(mut self, full_txs: impl IntoIterator<Item = (Txid, T)>) -> Self
|
||||
where
|
||||
T: Into<Arc<Transaction>>,
|
||||
{
|
||||
self.tx_cache = full_txs
|
||||
.into_iter()
|
||||
.map(|(txid, tx)| (txid, tx.into()))
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add all transactions from [`TxGraph`] into the [`TxCache`].
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn cache_graph_txs<A>(self, graph: &TxGraph<A>) -> Self {
|
||||
self.cache_txs(graph.full_txs().map(|tx_node| (tx_node.txid, tx_node.tx)))
|
||||
}
|
||||
|
||||
/// Construct a new [`FullScanRequest`] from a given `chain_tip` and `index`.
|
||||
///
|
||||
/// Unbounded script pubkey iterators for each keychain (`K`) are extracted using
|
||||
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`] and is used to populate the
|
||||
/// [`FullScanRequest`].
|
||||
///
|
||||
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::keychain::KeychainTxOutIndex::all_unbounded_spk_iters
|
||||
#[cfg(feature = "miniscript")]
|
||||
#[must_use]
|
||||
pub fn from_keychain_txout_index(
|
||||
chain_tip: CheckPoint,
|
||||
index: &crate::keychain::KeychainTxOutIndex<K>,
|
||||
) -> Self
|
||||
where
|
||||
K: Debug,
|
||||
{
|
||||
let mut req = Self::from_chain_tip(chain_tip);
|
||||
for (keychain, spks) in index.all_unbounded_spk_iters() {
|
||||
req = req.set_spks_for_keychain(keychain, spks);
|
||||
}
|
||||
req
|
||||
}
|
||||
|
||||
/// Set the [`Script`]s for a given `keychain`.
|
||||
///
|
||||
/// This consumes the [`FullScanRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_spks_for_keychain(
|
||||
mut self,
|
||||
keychain: K,
|
||||
spks: impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send + 'static>,
|
||||
) -> Self {
|
||||
self.spks_by_keychain
|
||||
.insert(keychain, Box::new(spks.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`Script`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`FullScanRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_spks_for_keychain(
|
||||
mut self,
|
||||
keychain: K,
|
||||
spks: impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send + 'static>,
|
||||
) -> Self {
|
||||
match self.spks_by_keychain.remove(&keychain) {
|
||||
// clippy here suggests to remove `into_iter` from `spks.into_iter()`, but doing so
|
||||
// results in a compilation error
|
||||
#[allow(clippy::useless_conversion)]
|
||||
Some(keychain_spks) => self
|
||||
.spks_by_keychain
|
||||
.insert(keychain, Box::new(keychain_spks.chain(spks.into_iter()))),
|
||||
None => self
|
||||
.spks_by_keychain
|
||||
.insert(keychain, Box::new(spks.into_iter())),
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for every [`Script`] previously added to any keychain in
|
||||
/// this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_spks_for_all_keychains(
|
||||
mut self,
|
||||
inspect: impl FnMut(K, u32, &Script) + Send + Sync + Clone + 'static,
|
||||
) -> Self
|
||||
where
|
||||
K: Send + 'static,
|
||||
{
|
||||
for (keychain, spks) in core::mem::take(&mut self.spks_by_keychain) {
|
||||
let mut inspect = inspect.clone();
|
||||
self.spks_by_keychain.insert(
|
||||
keychain.clone(),
|
||||
Box::new(spks.inspect(move |(i, spk)| inspect(keychain.clone(), *i, spk))),
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for every [`Script`] previously added to a given
|
||||
/// `keychain` in this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_spks_for_keychain(
|
||||
mut self,
|
||||
keychain: K,
|
||||
mut inspect: impl FnMut(u32, &Script) + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
K: Send + 'static,
|
||||
{
|
||||
if let Some(spks) = self.spks_by_keychain.remove(&keychain) {
|
||||
self.spks_by_keychain.insert(
|
||||
keychain,
|
||||
Box::new(spks.inspect(move |(i, spk)| inspect(*i, spk))),
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Data returned from a spk-based blockchain client full scan.
|
||||
///
|
||||
/// See also [`FullScanRequest`].
|
||||
pub struct FullScanResult<K, A = ConfirmationTimeHeightAnchor> {
|
||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||
pub graph_update: TxGraph<A>,
|
||||
/// The update to apply to the receiving [`TxGraph`].
|
||||
pub chain_update: CheckPoint,
|
||||
/// Last active indices for the corresponding keychains (`K`).
|
||||
pub last_active_indices: BTreeMap<K, u32>,
|
||||
}
|
||||
|
||||
/// A version of [`core::iter::Chain`] which can combine two [`ExactSizeIterator`]s to form a new
|
||||
/// [`ExactSizeIterator`].
|
||||
///
|
||||
/// The danger of this is explained in [the `ExactSizeIterator` docs]
|
||||
/// (https://doc.rust-lang.org/core/iter/trait.ExactSizeIterator.html#when-shouldnt-an-adapter-be-exactsizeiterator).
|
||||
/// This does not apply here since it would be impossible to scan an item count that overflows
|
||||
/// `usize` anyway.
|
||||
struct ExactSizeChain<A, B, I> {
|
||||
a: Option<A>,
|
||||
b: Option<B>,
|
||||
i: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<A, B, I> ExactSizeChain<A, B, I> {
|
||||
fn new(a: A, b: B) -> Self {
|
||||
ExactSizeChain {
|
||||
a: Some(a),
|
||||
b: Some(b),
|
||||
i: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, I> Iterator for ExactSizeChain<A, B, I>
|
||||
where
|
||||
A: Iterator<Item = I>,
|
||||
B: Iterator<Item = I>,
|
||||
{
|
||||
type Item = I;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(a) = &mut self.a {
|
||||
let item = a.next();
|
||||
if item.is_some() {
|
||||
return item;
|
||||
}
|
||||
self.a = None;
|
||||
}
|
||||
if let Some(b) = &mut self.b {
|
||||
let item = b.next();
|
||||
if item.is_some() {
|
||||
return item;
|
||||
}
|
||||
self.b = None;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, I> ExactSizeIterator for ExactSizeChain<A, B, I>
|
||||
where
|
||||
A: ExactSizeIterator<Item = I>,
|
||||
B: ExactSizeIterator<Item = I>,
|
||||
{
|
||||
fn len(&self) -> usize {
|
||||
let a_len = self.a.as_ref().map(|a| a.len()).unwrap_or(0);
|
||||
let b_len = self.b.as_ref().map(|a| a.len()).unwrap_or(0);
|
||||
a_len + b_len
|
||||
}
|
||||
}
|
||||
@@ -158,8 +158,8 @@ mod test {
|
||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||
|
||||
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
|
||||
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
|
||||
let _ = txout_index.insert_descriptor(TestKeychain::External, external_descriptor.clone());
|
||||
let _ = txout_index.insert_descriptor(TestKeychain::Internal, internal_descriptor.clone());
|
||||
|
||||
(txout_index, external_descriptor, internal_descriptor)
|
||||
}
|
||||
@@ -258,17 +258,10 @@ mod test {
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
// The following dummy traits were created to test if SpkIterator is working properly.
|
||||
trait TestSendStatic: Send + 'static {
|
||||
fn test(&self) -> u32 {
|
||||
20
|
||||
}
|
||||
}
|
||||
|
||||
impl TestSendStatic for SpkIterator<Descriptor<DescriptorPublicKey>> {
|
||||
fn test(&self) -> u32 {
|
||||
20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spk_iterator_is_send_and_static() {
|
||||
fn is_send_and_static<A: Send + 'static>() {}
|
||||
is_send_and_static::<SpkIterator<Descriptor<DescriptorPublicKey>>>()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||
indexed_tx_graph::Indexer,
|
||||
};
|
||||
use bitcoin::{OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
|
||||
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
|
||||
|
||||
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
|
||||
///
|
||||
@@ -229,7 +229,7 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
|
||||
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
|
||||
/// never scanned a transaction output with it.
|
||||
pub fn is_used(&self, index: &I) -> bool {
|
||||
self.unused.get(index).is_none()
|
||||
!self.unused.contains(index)
|
||||
}
|
||||
|
||||
/// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it.
|
||||
@@ -270,37 +270,45 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
|
||||
self.spk_indices.get(script)
|
||||
}
|
||||
|
||||
/// Computes total input value going from script pubkeys in the index (sent) and the total output
|
||||
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
|
||||
/// correctly, the output being spent must have already been scanned by the index. Calculating
|
||||
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has
|
||||
/// not been scanned.
|
||||
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
|
||||
let mut sent = 0;
|
||||
let mut received = 0;
|
||||
/// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is
|
||||
/// *sent* when a script pubkey in the `range` is on an input and *received* when it is on an
|
||||
/// output. For `sent` to be computed correctly, the output being spent must have already been
|
||||
/// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly,
|
||||
/// so it will be correct even if it has not been scanned.
|
||||
pub fn sent_and_received(
|
||||
&self,
|
||||
tx: &Transaction,
|
||||
range: impl RangeBounds<I>,
|
||||
) -> (Amount, Amount) {
|
||||
let mut sent = Amount::ZERO;
|
||||
let mut received = Amount::ZERO;
|
||||
|
||||
for txin in &tx.input {
|
||||
if let Some((_, txout)) = self.txout(txin.previous_output) {
|
||||
sent += txout.value.to_sat();
|
||||
if let Some((index, txout)) = self.txout(txin.previous_output) {
|
||||
if range.contains(index) {
|
||||
sent += txout.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
for txout in &tx.output {
|
||||
if self.index_of_spk(&txout.script_pubkey).is_some() {
|
||||
received += txout.value.to_sat();
|
||||
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
|
||||
if range.contains(index) {
|
||||
received += txout.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(sent, received)
|
||||
}
|
||||
|
||||
/// Computes the net value that this transaction gives to the script pubkeys in the index and
|
||||
/// *takes* from the transaction outputs in the index. Shorthand for calling
|
||||
/// [`sent_and_received`] and subtracting sent from received.
|
||||
/// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
|
||||
/// for calling [`sent_and_received`] and subtracting sent from received.
|
||||
///
|
||||
/// [`sent_and_received`]: Self::sent_and_received
|
||||
pub fn net_value(&self, tx: &Transaction) -> i64 {
|
||||
let (sent, received) = self.sent_and_received(tx);
|
||||
received as i64 - sent as i64
|
||||
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> SignedAmount {
|
||||
let (sent, received) = self.sent_and_received(tx, range);
|
||||
received.to_signed().expect("valid `SignedAmount`")
|
||||
- sent.to_signed().expect("valid `SignedAmount`")
|
||||
}
|
||||
|
||||
/// Whether any of the inputs of this transaction spend a txout tracked or whether any output
|
||||
|
||||
@@ -89,13 +89,13 @@
|
||||
//! [`insert_txout`]: TxGraph::insert_txout
|
||||
|
||||
use crate::{
|
||||
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
|
||||
ChainOracle, ChainPosition, FullTxOut,
|
||||
collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
|
||||
FullTxOut,
|
||||
};
|
||||
use alloc::collections::vec_deque::VecDeque;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||
use bitcoin::{Amount, OutPoint, Script, Transaction, TxOut, Txid};
|
||||
use core::fmt::{self, Formatter};
|
||||
use core::{
|
||||
convert::Infallible,
|
||||
@@ -516,12 +516,12 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Inserts the given transaction into [`TxGraph`].
|
||||
///
|
||||
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
|
||||
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A> {
|
||||
pub fn insert_tx<T: Into<Arc<Transaction>>>(&mut self, tx: T) -> ChangeSet<A> {
|
||||
let tx = tx.into();
|
||||
let mut update = Self::default();
|
||||
update.txs.insert(
|
||||
tx.txid(),
|
||||
(TxNodeInternal::Whole(tx.into()), BTreeSet::new(), 0),
|
||||
);
|
||||
update
|
||||
.txs
|
||||
.insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
|
||||
self.apply_update(update)
|
||||
}
|
||||
|
||||
@@ -759,69 +759,6 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
}
|
||||
|
||||
impl<A: Anchor> TxGraph<A> {
|
||||
/// Find missing block heights of `chain`.
|
||||
///
|
||||
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
|
||||
/// exists in the [`LocalChain`]. The returned iterator does not output duplicate heights.
|
||||
pub fn missing_heights<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
|
||||
// Map of txids to skip.
|
||||
//
|
||||
// Usually, if a height of a tx anchor is missing from the chain, we would want to return
|
||||
// this height in the iterator. The exception is when the tx is confirmed in chain. All the
|
||||
// other missing-height anchors of this tx can be skipped.
|
||||
//
|
||||
// * Some(true) => skip all anchors of this txid
|
||||
// * Some(false) => do not skip anchors of this txid
|
||||
// * None => we do not know whether we can skip this txid
|
||||
let mut txids_to_skip = HashMap::<Txid, bool>::new();
|
||||
|
||||
// Keeps track of the last height emitted so we don't double up.
|
||||
let mut last_height_emitted = Option::<u32>::None;
|
||||
|
||||
self.anchors
|
||||
.iter()
|
||||
.filter(move |(_, txid)| {
|
||||
let skip = *txids_to_skip.entry(*txid).or_insert_with(|| {
|
||||
let tx_anchors = match self.txs.get(txid) {
|
||||
Some((_, anchors, _)) => anchors,
|
||||
None => return true,
|
||||
};
|
||||
let mut has_missing_height = false;
|
||||
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
|
||||
match chain.get(anchor_block.height) {
|
||||
None => {
|
||||
has_missing_height = true;
|
||||
continue;
|
||||
}
|
||||
Some(chain_cp) => {
|
||||
if chain_cp.hash() == anchor_block.hash {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
!has_missing_height
|
||||
});
|
||||
#[cfg(feature = "std")]
|
||||
debug_assert!({
|
||||
println!("txid={} skip={}", txid, skip);
|
||||
true
|
||||
});
|
||||
!skip
|
||||
})
|
||||
.filter_map(move |(a, _)| {
|
||||
let anchor_block = a.anchor_block();
|
||||
if Some(anchor_block.height) != last_height_emitted
|
||||
&& chain.get(anchor_block.height).is_none()
|
||||
{
|
||||
last_height_emitted = Some(anchor_block.height);
|
||||
Some(anchor_block.height)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
||||
///
|
||||
/// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.
|
||||
@@ -1218,10 +1155,10 @@ impl<A: Anchor> TxGraph<A> {
|
||||
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
|
||||
mut trust_predicate: impl FnMut(&OI, &Script) -> bool,
|
||||
) -> Result<Balance, C::Error> {
|
||||
let mut immature = 0;
|
||||
let mut trusted_pending = 0;
|
||||
let mut untrusted_pending = 0;
|
||||
let mut confirmed = 0;
|
||||
let mut immature = Amount::ZERO;
|
||||
let mut trusted_pending = Amount::ZERO;
|
||||
let mut untrusted_pending = Amount::ZERO;
|
||||
let mut confirmed = Amount::ZERO;
|
||||
|
||||
for res in self.try_filter_chain_unspents(chain, chain_tip, outpoints) {
|
||||
let (spk_i, txout) = res?;
|
||||
@@ -1229,16 +1166,16 @@ impl<A: Anchor> TxGraph<A> {
|
||||
match &txout.chain_position {
|
||||
ChainPosition::Confirmed(_) => {
|
||||
if txout.is_confirmed_and_spendable(chain_tip.height) {
|
||||
confirmed += txout.txout.value.to_sat();
|
||||
confirmed += txout.txout.value;
|
||||
} else if !txout.is_mature(chain_tip.height) {
|
||||
immature += txout.txout.value.to_sat();
|
||||
immature += txout.txout.value;
|
||||
}
|
||||
}
|
||||
ChainPosition::Unconfirmed(_) => {
|
||||
if trust_predicate(&spk_i, &txout.txout.script_pubkey) {
|
||||
trusted_pending += txout.txout.value.to_sat();
|
||||
trusted_pending += txout.txout.value;
|
||||
} else {
|
||||
untrusted_pending += txout.txout.value.to_sat();
|
||||
untrusted_pending += txout.txout.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1330,8 +1267,6 @@ impl<A> ChangeSet<A> {
|
||||
///
|
||||
/// This is useful if you want to find which heights you need to fetch data about in order to
|
||||
/// confirm or exclude these anchors.
|
||||
///
|
||||
/// See also: [`TxGraph::missing_heights`]
|
||||
pub fn anchor_heights(&self) -> impl Iterator<Item = u32> + '_
|
||||
where
|
||||
A: Anchor,
|
||||
@@ -1346,24 +1281,6 @@ impl<A> ChangeSet<A> {
|
||||
!duplicate
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator for the [`anchor_heights`] in this changeset that are not included in
|
||||
/// `local_chain`. This tells you which heights you need to include in `local_chain` in order
|
||||
/// for it to conclusively act as a [`ChainOracle`] for the transaction anchors this changeset
|
||||
/// will add.
|
||||
///
|
||||
/// [`ChainOracle`]: crate::ChainOracle
|
||||
/// [`anchor_heights`]: Self::anchor_heights
|
||||
pub fn missing_heights_from<'a>(
|
||||
&'a self,
|
||||
local_chain: &'a LocalChain,
|
||||
) -> impl Iterator<Item = u32> + 'a
|
||||
where
|
||||
A: Anchor,
|
||||
{
|
||||
self.anchor_heights()
|
||||
.filter(move |&height| local_chain.get(height).is_none())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Ord> Append for ChangeSet<A> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
mod tx_template;
|
||||
#[allow(unused_imports)]
|
||||
pub use tx_template::*;
|
||||
@@ -32,12 +34,9 @@ macro_rules! local_chain {
|
||||
macro_rules! chain_update {
|
||||
[ $(($height:expr, $hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::Update {
|
||||
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
.tip(),
|
||||
introduce_older_blocks: true,
|
||||
}
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
.tip()
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -76,3 +75,15 @@ pub fn new_tx(lt: u32) -> bitcoin::Transaction {
|
||||
output: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub const DESCRIPTORS: [&str; 7] = [
|
||||
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)",
|
||||
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)",
|
||||
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0/*)",
|
||||
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)",
|
||||
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)",
|
||||
"wpkh(xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX/1/0/*)",
|
||||
// non-wildcard
|
||||
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)",
|
||||
];
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -52,7 +54,8 @@ impl TxOutTemplate {
|
||||
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
|
||||
) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let (descriptor, _) =
|
||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), super::DESCRIPTORS[2]).unwrap();
|
||||
let mut graph = TxGraph::<A>::default();
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
(0..10).for_each(|index| {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
use crate::common::DESCRIPTORS;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, Balance, KeychainTxOutIndex},
|
||||
local_chain::LocalChain,
|
||||
tx_graph, ChainPosition, ConfirmationHeightAnchor,
|
||||
tx_graph, ChainPosition, ConfirmationHeightAnchor, DescriptorExt,
|
||||
};
|
||||
use bitcoin::{
|
||||
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
|
||||
@@ -23,8 +26,7 @@ use miniscript::Descriptor;
|
||||
/// agnostic.
|
||||
#[test]
|
||||
fn insert_relevant_txs() {
|
||||
const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)";
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR)
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[0])
|
||||
.expect("must be valid");
|
||||
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
||||
@@ -32,7 +34,7 @@ fn insert_relevant_txs() {
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
graph.index.add_keychain((), descriptor);
|
||||
let _ = graph.index.insert_descriptor((), descriptor.clone());
|
||||
|
||||
let tx_a = Transaction {
|
||||
output: vec![
|
||||
@@ -71,7 +73,10 @@ fn insert_relevant_txs() {
|
||||
txs: txs.iter().cloned().map(Arc::new).collect(),
|
||||
..Default::default()
|
||||
},
|
||||
indexer: keychain::ChangeSet([((), 9_u32)].into()),
|
||||
indexer: keychain::ChangeSet {
|
||||
last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
|
||||
keychains_added: [].into(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -79,7 +84,16 @@ fn insert_relevant_txs() {
|
||||
changeset,
|
||||
);
|
||||
|
||||
assert_eq!(graph.initial_changeset(), changeset,);
|
||||
// The initial changeset will also contain info about the keychain we added
|
||||
let initial_changeset = indexed_tx_graph::ChangeSet {
|
||||
graph: changeset.graph,
|
||||
indexer: keychain::ChangeSet {
|
||||
last_revealed: changeset.indexer.last_revealed,
|
||||
keychains_added: [((), descriptor)].into(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(graph.initial_changeset(), initial_changeset);
|
||||
}
|
||||
|
||||
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
|
||||
@@ -117,15 +131,17 @@ fn test_list_owned_txouts() {
|
||||
|
||||
// Initiate IndexedTxGraph
|
||||
|
||||
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap();
|
||||
let (desc_1, _) =
|
||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[2]).unwrap();
|
||||
let (desc_2, _) =
|
||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
|
||||
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
|
||||
graph.index.add_keychain("keychain_1".into(), desc_1);
|
||||
graph.index.add_keychain("keychain_2".into(), desc_2);
|
||||
let _ = graph.index.insert_descriptor("keychain_1".into(), desc_1);
|
||||
let _ = graph.index.insert_descriptor("keychain_2".into(), desc_2);
|
||||
|
||||
// Get trusted and untrusted addresses
|
||||
|
||||
@@ -135,14 +151,20 @@ fn test_list_owned_txouts() {
|
||||
{
|
||||
// we need to scope here to take immutanble reference of the graph
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string());
|
||||
let ((_, script), _) = graph
|
||||
.index
|
||||
.reveal_next_spk(&"keychain_1".to_string())
|
||||
.unwrap();
|
||||
// TODO Assert indexes
|
||||
trusted_spks.push(script.to_owned());
|
||||
}
|
||||
}
|
||||
{
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
|
||||
let ((_, script), _) = graph
|
||||
.index
|
||||
.reveal_next_spk(&"keychain_2".to_string())
|
||||
.unwrap();
|
||||
untrusted_spks.push(script.to_owned());
|
||||
}
|
||||
}
|
||||
@@ -235,26 +257,18 @@ fn test_list_owned_txouts() {
|
||||
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
||||
let txouts = graph
|
||||
.graph()
|
||||
.filter_chain_txouts(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
)
|
||||
.filter_chain_txouts(&local_chain, chain_tip, graph.index.outpoints())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let utxos = graph
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
)
|
||||
.filter_chain_unspents(&local_chain, chain_tip, graph.index.outpoints())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let balance = graph.graph().balance(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
graph.index.outpoints(),
|
||||
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
|
||||
);
|
||||
|
||||
@@ -341,10 +355,10 @@ fn test_list_owned_txouts() {
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -376,10 +390,10 @@ fn test_list_owned_txouts() {
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -408,10 +422,10 @@ fn test_list_owned_txouts() {
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 10000 // tx3 got confirmed
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::from_sat(10000) // tx3 got confirmed
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -439,10 +453,10 @@ fn test_list_owned_txouts() {
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 10000 // tx1 got matured
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::from_sat(10000) // tx1 got matured
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -455,10 +469,10 @@ fn test_list_owned_txouts() {
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 0, // coinbase matured
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 80000 // tx1 + tx3
|
||||
immature: Amount::ZERO, // coinbase matured
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::from_sat(80000) // tx1 + tx3
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,36 +5,39 @@ mod common;
|
||||
use bdk_chain::{
|
||||
collections::BTreeMap,
|
||||
indexed_tx_graph::Indexer,
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
Append,
|
||||
keychain::{self, ChangeSet, KeychainTxOutIndex},
|
||||
Append, DescriptorExt, DescriptorId,
|
||||
};
|
||||
|
||||
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
use miniscript::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
use crate::common::DESCRIPTORS;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
||||
enum TestKeychain {
|
||||
External,
|
||||
Internal,
|
||||
}
|
||||
|
||||
fn parse_descriptor(descriptor: &str) -> Descriptor<DescriptorPublicKey> {
|
||||
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, descriptor)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
fn init_txout_index(
|
||||
external_descriptor: Descriptor<DescriptorPublicKey>,
|
||||
internal_descriptor: Descriptor<DescriptorPublicKey>,
|
||||
lookahead: u32,
|
||||
) -> (
|
||||
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
) {
|
||||
) -> bdk_chain::keychain::KeychainTxOutIndex<TestKeychain> {
|
||||
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
|
||||
|
||||
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||
let _ = txout_index.insert_descriptor(TestKeychain::External, external_descriptor);
|
||||
let _ = txout_index.insert_descriptor(TestKeychain::Internal, internal_descriptor);
|
||||
|
||||
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
|
||||
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
|
||||
|
||||
(txout_index, external_descriptor, internal_descriptor)
|
||||
txout_index
|
||||
}
|
||||
|
||||
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> ScriptBuf {
|
||||
@@ -44,29 +47,136 @@ fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Scr
|
||||
.script_pubkey()
|
||||
}
|
||||
|
||||
// We create two empty changesets lhs and rhs, we then insert various descriptors with various
|
||||
// last_revealed, append rhs to lhs, and check that the result is consistent with these rules:
|
||||
// - Existing index doesn't update if the new index in `other` is lower than `self`.
|
||||
// - Existing index updates if the new index in `other` is higher than `self`.
|
||||
// - Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
// - New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
#[test]
|
||||
fn append_changesets_check_last_revealed() {
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let descriptor_ids: Vec<_> = DESCRIPTORS
|
||||
.iter()
|
||||
.take(4)
|
||||
.map(|d| {
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, d)
|
||||
.unwrap()
|
||||
.0
|
||||
.descriptor_id()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut lhs_di = BTreeMap::<DescriptorId, u32>::default();
|
||||
let mut rhs_di = BTreeMap::<DescriptorId, u32>::default();
|
||||
lhs_di.insert(descriptor_ids[0], 7);
|
||||
lhs_di.insert(descriptor_ids[1], 0);
|
||||
lhs_di.insert(descriptor_ids[2], 3);
|
||||
|
||||
rhs_di.insert(descriptor_ids[0], 3); // value less than lhs desc 0
|
||||
rhs_di.insert(descriptor_ids[1], 5); // value more than lhs desc 1
|
||||
lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs
|
||||
|
||||
let mut lhs = ChangeSet {
|
||||
keychains_added: BTreeMap::<(), _>::new(),
|
||||
last_revealed: lhs_di,
|
||||
};
|
||||
let rhs = ChangeSet {
|
||||
keychains_added: BTreeMap::<(), _>::new(),
|
||||
last_revealed: rhs_di,
|
||||
};
|
||||
lhs.append(rhs);
|
||||
|
||||
// Existing index doesn't update if the new index in `other` is lower than `self`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[0]), Some(&7));
|
||||
// Existing index updates if the new index in `other` is higher than `self`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[1]), Some(&5));
|
||||
// Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[2]), Some(&3));
|
||||
// New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_changeset_with_different_descriptors_to_same_keychain() {
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &external_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
|
||||
let changeset = ChangeSet {
|
||||
keychains_added: [(TestKeychain::External, internal_descriptor.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
};
|
||||
txout_index.apply_changeset(changeset);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &internal_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
|
||||
let changeset = ChangeSet {
|
||||
keychains_added: [(TestKeychain::Internal, external_descriptor.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
};
|
||||
txout_index.apply_changeset(changeset);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &internal_descriptor),
|
||||
(&TestKeychain::Internal, &external_descriptor)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_all_derivation_indices() {
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
|
||||
let (mut txout_index, _, _) = init_txout_index(0);
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
|
||||
let derive_to: BTreeMap<_, _> =
|
||||
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
|
||||
let last_revealed: BTreeMap<_, _> = [
|
||||
(external_descriptor.descriptor_id(), 12),
|
||||
(internal_descriptor.descriptor_id(), 24),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to).1.as_inner(),
|
||||
&derive_to
|
||||
txout_index.reveal_to_target_multi(&derive_to).1,
|
||||
ChangeSet {
|
||||
keychains_added: BTreeMap::new(),
|
||||
last_revealed: last_revealed.clone()
|
||||
}
|
||||
);
|
||||
assert_eq!(txout_index.last_revealed_indices(), &derive_to);
|
||||
assert_eq!(txout_index.last_revealed_indices(), derive_to);
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to).1,
|
||||
keychain::ChangeSet::default(),
|
||||
"no changes if we set to the same thing"
|
||||
);
|
||||
assert_eq!(txout_index.initial_changeset().as_inner(), &derive_to);
|
||||
assert_eq!(txout_index.initial_changeset().last_revealed, last_revealed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookahead() {
|
||||
let (mut txout_index, external_desc, internal_desc) = init_txout_index(10);
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
|
||||
|
||||
// given:
|
||||
// - external lookahead set to 10
|
||||
@@ -76,15 +186,16 @@ fn test_lookahead() {
|
||||
// - scripts cached in spk_txout_index should increase correctly
|
||||
// - stored scripts of external keychain should be of expected counts
|
||||
for index in (0..20).skip_while(|i| i % 2 == 1) {
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::External, index);
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::External, index)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
revealed_spks.collect::<Vec<_>>(),
|
||||
vec![(index, spk_at_index(&external_desc, index))],
|
||||
vec![(index, spk_at_index(&external_descriptor, index))],
|
||||
);
|
||||
assert_eq!(
|
||||
revealed_changeset.as_inner(),
|
||||
&[(TestKeychain::External, index)].into()
|
||||
&revealed_changeset.last_revealed,
|
||||
&[(external_descriptor.descriptor_id(), index)].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -126,17 +237,18 @@ fn test_lookahead() {
|
||||
// - derivation index is set ahead of current derivation index + lookahead
|
||||
// expect:
|
||||
// - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::Internal, 24);
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::Internal, 24)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
revealed_spks.collect::<Vec<_>>(),
|
||||
(0..=24)
|
||||
.map(|index| (index, spk_at_index(&internal_desc, index)))
|
||||
.map(|index| (index, spk_at_index(&internal_descriptor, index)))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(
|
||||
revealed_changeset.as_inner(),
|
||||
&[(TestKeychain::Internal, 24)].into()
|
||||
&revealed_changeset.last_revealed,
|
||||
&[(internal_descriptor.descriptor_id(), 24)].into()
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.inner().all_spks().len(),
|
||||
@@ -172,14 +284,14 @@ fn test_lookahead() {
|
||||
let tx = Transaction {
|
||||
output: vec![
|
||||
TxOut {
|
||||
script_pubkey: external_desc
|
||||
script_pubkey: external_descriptor
|
||||
.at_derivation_index(external_index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
value: Amount::from_sat(10_000),
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: internal_desc
|
||||
script_pubkey: internal_descriptor
|
||||
.at_derivation_index(internal_index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
@@ -219,14 +331,17 @@ fn test_lookahead() {
|
||||
// - last used index should change as expected
|
||||
#[test]
|
||||
fn test_scan_with_lookahead() {
|
||||
let (mut txout_index, external_desc, _) = init_txout_index(10);
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
|
||||
|
||||
let spks: BTreeMap<u32, ScriptBuf> = [0, 10, 20, 30]
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
(
|
||||
i,
|
||||
external_desc
|
||||
external_descriptor
|
||||
.at_derivation_index(i)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
@@ -243,8 +358,8 @@ fn test_scan_with_lookahead() {
|
||||
|
||||
let changeset = txout_index.index_txout(op, &txout);
|
||||
assert_eq!(
|
||||
changeset.as_inner(),
|
||||
&[(TestKeychain::External, spk_i)].into()
|
||||
&changeset.last_revealed,
|
||||
&[(external_descriptor.descriptor_id(), spk_i)].into()
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::External),
|
||||
@@ -257,7 +372,7 @@ fn test_scan_with_lookahead() {
|
||||
}
|
||||
|
||||
// now try with index 41 (lookahead surpassed), we expect that the txout to not be indexed
|
||||
let spk_41 = external_desc
|
||||
let spk_41 = external_descriptor
|
||||
.at_derivation_index(41)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
@@ -273,11 +388,13 @@ fn test_scan_with_lookahead() {
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_wildcard_derivations() {
|
||||
let (mut txout_index, external_desc, _) = init_txout_index(0);
|
||||
let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let external_spk_16 = external_desc.at_derivation_index(16).unwrap().script_pubkey();
|
||||
let external_spk_26 = external_desc.at_derivation_index(26).unwrap().script_pubkey();
|
||||
let external_spk_27 = external_desc.at_derivation_index(27).unwrap().script_pubkey();
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index = init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
|
||||
let external_spk_0 = external_descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let external_spk_16 = external_descriptor.at_derivation_index(16).unwrap().script_pubkey();
|
||||
let external_spk_26 = external_descriptor.at_derivation_index(26).unwrap().script_pubkey();
|
||||
let external_spk_27 = external_descriptor.at_derivation_index(27).unwrap().script_pubkey();
|
||||
|
||||
// - nothing is derived
|
||||
// - unused list is also empty
|
||||
@@ -285,13 +402,13 @@ fn test_wildcard_derivations() {
|
||||
// - next_derivation_index() == (0, true)
|
||||
// - derive_new() == ((0, <spk>), keychain::ChangeSet)
|
||||
// - next_unused() == ((0, <spk>), keychain::ChangeSet:is_empty())
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 0)].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
// - derived till 25
|
||||
// - used all spks till 15.
|
||||
@@ -307,16 +424,16 @@ fn test_wildcard_derivations() {
|
||||
.chain([17, 20, 23])
|
||||
.for_each(|index| assert!(txout_index.mark_used(TestKeychain::External, index)));
|
||||
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (26, true));
|
||||
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (26, external_spk_26.as_script()));
|
||||
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into());
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 26)].into());
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (16, external_spk_16.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
// - Use all the derived till 26.
|
||||
// - next_unused() = ((27, <spk>), keychain::ChangeSet)
|
||||
@@ -324,9 +441,9 @@ fn test_wildcard_derivations() {
|
||||
txout_index.mark_used(TestKeychain::External, index);
|
||||
});
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (27, external_spk_27.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into());
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 27)].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -334,13 +451,14 @@ fn test_non_wildcard_derivations() {
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
|
||||
let (no_wildcard_descriptor, _) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[6]).unwrap();
|
||||
let external_spk = no_wildcard_descriptor
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
|
||||
txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor);
|
||||
let _ = txout_index.insert_descriptor(TestKeychain::External, no_wildcard_descriptor.clone());
|
||||
|
||||
// given:
|
||||
// - `txout_index` with no stored scripts
|
||||
@@ -348,14 +466,24 @@ fn test_non_wildcard_derivations() {
|
||||
// - next derivation index should be new
|
||||
// - when we derive a new script, script @ index 0
|
||||
// - when we get the next unused script, script @ index 0
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(
|
||||
txout_index.next_index(&TestKeychain::External).unwrap(),
|
||||
(0, true)
|
||||
);
|
||||
let (spk, changeset) = txout_index
|
||||
.reveal_next_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
|
||||
assert_eq!(
|
||||
&changeset.last_revealed,
|
||||
&[(no_wildcard_descriptor.descriptor_id(), 0)].into()
|
||||
);
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
let (spk, changeset) = txout_index
|
||||
.next_unused_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
// given:
|
||||
// - the non-wildcard descriptor already has a stored and used script
|
||||
@@ -363,18 +491,26 @@ fn test_non_wildcard_derivations() {
|
||||
// - next derivation index should not be new
|
||||
// - derive new and next unused should return the old script
|
||||
// - store_up_to should not panic and return empty changeset
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
|
||||
assert_eq!(
|
||||
txout_index.next_index(&TestKeychain::External).unwrap(),
|
||||
(0, false)
|
||||
);
|
||||
txout_index.mark_used(TestKeychain::External, 0);
|
||||
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
let (spk, changeset) = txout_index
|
||||
.reveal_next_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
let (spk, changeset) = txout_index
|
||||
.next_unused_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::External, 200);
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::External, 200)
|
||||
.unwrap();
|
||||
assert_eq!(revealed_spks.count(), 0);
|
||||
assert!(revealed_changeset.is_empty());
|
||||
|
||||
@@ -438,7 +574,13 @@ fn lookahead_to_target() {
|
||||
];
|
||||
|
||||
for t in test_cases {
|
||||
let (mut index, _, _) = init_txout_index(t.lookahead);
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut index = init_txout_index(
|
||||
external_descriptor.clone(),
|
||||
internal_descriptor.clone(),
|
||||
t.lookahead,
|
||||
);
|
||||
|
||||
if let Some(last_revealed) = t.external_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
|
||||
@@ -449,17 +591,19 @@ fn lookahead_to_target() {
|
||||
|
||||
let keychain_test_cases = [
|
||||
(
|
||||
external_descriptor.descriptor_id(),
|
||||
TestKeychain::External,
|
||||
t.external_last_revealed,
|
||||
t.external_target,
|
||||
),
|
||||
(
|
||||
internal_descriptor.descriptor_id(),
|
||||
TestKeychain::Internal,
|
||||
t.internal_last_revealed,
|
||||
t.internal_target,
|
||||
),
|
||||
];
|
||||
for (keychain, last_revealed, target) in keychain_test_cases {
|
||||
for (descriptor_id, keychain, last_revealed, target) in keychain_test_cases {
|
||||
if let Some(target) = target {
|
||||
let original_last_stored_index = match last_revealed {
|
||||
Some(last_revealed) => Some(last_revealed + t.lookahead),
|
||||
@@ -475,10 +619,10 @@ fn lookahead_to_target() {
|
||||
let keys = index
|
||||
.inner()
|
||||
.all_spks()
|
||||
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
|
||||
.map(|(k, _)| k.clone())
|
||||
.range((descriptor_id, 0)..=(descriptor_id, u32::MAX))
|
||||
.map(|(k, _)| *k)
|
||||
.collect::<Vec<_>>();
|
||||
let exp_keys = core::iter::repeat(keychain)
|
||||
let exp_keys = core::iter::repeat(descriptor_id)
|
||||
.zip(0_u32..=exp_last_stored_index)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(keys, exp_keys);
|
||||
@@ -486,3 +630,150 @@ fn lookahead_to_target() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `::index_txout` should still index txouts with spks derived from descriptors without keychains.
|
||||
/// This includes properly refilling the lookahead for said descriptors.
|
||||
#[test]
|
||||
fn index_txout_after_changing_descriptor_under_keychain() {
|
||||
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let (desc_a, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0])
|
||||
.expect("descriptor 0 must be valid");
|
||||
let (desc_b, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[1])
|
||||
.expect("descriptor 1 must be valid");
|
||||
let desc_id_a = desc_a.descriptor_id();
|
||||
|
||||
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<()>::new(10);
|
||||
|
||||
// Introduce `desc_a` under keychain `()` and replace the descriptor.
|
||||
let _ = txout_index.insert_descriptor((), desc_a.clone());
|
||||
let _ = txout_index.insert_descriptor((), desc_b.clone());
|
||||
|
||||
// Loop through spks in intervals of `lookahead` to create outputs with. We should always be
|
||||
// able to index these outputs if `lookahead` is respected.
|
||||
let spk_indices = [9, 19, 29, 39];
|
||||
for i in spk_indices {
|
||||
let spk_at_index = desc_a
|
||||
.at_derivation_index(i)
|
||||
.expect("must derive")
|
||||
.script_pubkey();
|
||||
let index_changeset = txout_index.index_txout(
|
||||
// Use spk derivation index as vout as we just want an unique outpoint.
|
||||
OutPoint::new(h!("mock_tx"), i as _),
|
||||
&TxOut {
|
||||
value: Amount::from_sat(10_000),
|
||||
script_pubkey: spk_at_index,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
index_changeset,
|
||||
bdk_chain::keychain::ChangeSet {
|
||||
keychains_added: BTreeMap::default(),
|
||||
last_revealed: [(desc_id_a, i)].into(),
|
||||
},
|
||||
"must always increase last active if impl respects lookahead"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_descriptor_no_change() {
|
||||
let secp = Secp256k1::signing_only();
|
||||
let (desc, _) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0]).unwrap();
|
||||
let mut txout_index = KeychainTxOutIndex::<()>::default();
|
||||
assert_eq!(
|
||||
txout_index.insert_descriptor((), desc.clone()),
|
||||
keychain::ChangeSet {
|
||||
keychains_added: [((), desc.clone())].into(),
|
||||
last_revealed: Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.insert_descriptor((), desc.clone()),
|
||||
keychain::ChangeSet::default(),
|
||||
"inserting the same descriptor for keychain should return an empty changeset",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
|
||||
let desc = parse_descriptor(DESCRIPTORS[0]);
|
||||
let changesets: &[ChangeSet<TestKeychain>] = &[
|
||||
ChangeSet {
|
||||
keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
},
|
||||
ChangeSet {
|
||||
keychains_added: [(TestKeychain::External, desc.clone())].into(),
|
||||
last_revealed: [(desc.descriptor_id(), 12)].into(),
|
||||
},
|
||||
];
|
||||
|
||||
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
for changeset in changesets {
|
||||
indexer_a.apply_changeset(changeset.clone());
|
||||
}
|
||||
|
||||
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
let aggregate_changesets = changesets
|
||||
.iter()
|
||||
.cloned()
|
||||
.reduce(|mut agg, cs| {
|
||||
agg.append(cs);
|
||||
agg
|
||||
})
|
||||
.expect("must aggregate changesets");
|
||||
indexer_b.apply_changeset(aggregate_changesets);
|
||||
|
||||
assert_eq!(
|
||||
indexer_a.keychains().collect::<Vec<_>>(),
|
||||
indexer_b.keychains().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
indexer_a.spk_at_index(TestKeychain::External, 0),
|
||||
indexer_b.spk_at_index(TestKeychain::External, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
indexer_a.spk_at_index(TestKeychain::Internal, 0),
|
||||
indexer_b.spk_at_index(TestKeychain::Internal, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
indexer_a.last_revealed_indices(),
|
||||
indexer_b.last_revealed_indices()
|
||||
);
|
||||
}
|
||||
|
||||
// When the same descriptor is associated with various keychains,
|
||||
// index methods only return the highest keychain by Ord
|
||||
#[test]
|
||||
fn test_only_highest_ord_keychain_is_returned() {
|
||||
let desc = parse_descriptor(DESCRIPTORS[0]);
|
||||
|
||||
let mut indexer = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
let _ = indexer.insert_descriptor(TestKeychain::Internal, desc.clone());
|
||||
let _ = indexer.insert_descriptor(TestKeychain::External, desc);
|
||||
|
||||
// reveal_next_spk will work with either keychain
|
||||
let spk0: ScriptBuf = indexer
|
||||
.reveal_next_spk(&TestKeychain::External)
|
||||
.unwrap()
|
||||
.0
|
||||
.1
|
||||
.into();
|
||||
let spk1: ScriptBuf = indexer
|
||||
.reveal_next_spk(&TestKeychain::Internal)
|
||||
.unwrap()
|
||||
.0
|
||||
.1
|
||||
.into();
|
||||
|
||||
// index_of_spk will always return External
|
||||
assert_eq!(
|
||||
indexer.index_of_spk(&spk0),
|
||||
Some((TestKeychain::External, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
indexer.index_of_spk(&spk1),
|
||||
Some((TestKeychain::External, 1))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
|
||||
use bdk_chain::{
|
||||
local_chain::{
|
||||
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
||||
LocalChain, MissingGenesisError, Update,
|
||||
LocalChain, MissingGenesisError,
|
||||
},
|
||||
BlockId,
|
||||
};
|
||||
@@ -17,7 +19,7 @@ mod common;
|
||||
struct TestLocalChain<'a> {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
update: Update,
|
||||
update: CheckPoint,
|
||||
exp: ExpectedResult<'a>,
|
||||
}
|
||||
|
||||
@@ -577,6 +579,77 @@ fn checkpoint_query() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_insert() {
|
||||
struct TestCase<'a> {
|
||||
/// The name of the test.
|
||||
name: &'a str,
|
||||
/// The original checkpoint chain to call [`CheckPoint::insert`] on.
|
||||
chain: &'a [(u32, BlockHash)],
|
||||
/// The `block_id` to insert.
|
||||
to_insert: (u32, BlockHash),
|
||||
/// The expected final checkpoint chain after calling [`CheckPoint::insert`].
|
||||
exp_final_chain: &'a [(u32, BlockHash)],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "insert_above_tip",
|
||||
chain: &[(1, h!("a")), (2, h!("b"))],
|
||||
to_insert: (4, h!("d")),
|
||||
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (4, h!("d"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "insert_already_exists_expect_no_change",
|
||||
chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
|
||||
to_insert: (2, h!("b")),
|
||||
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "insert_in_middle",
|
||||
chain: &[(2, h!("b")), (4, h!("d")), (5, h!("e"))],
|
||||
to_insert: (3, h!("c")),
|
||||
exp_final_chain: &[(2, h!("b")), (3, h!("c")), (4, h!("d")), (5, h!("e"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "replace_one",
|
||||
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e"))],
|
||||
to_insert: (5, h!("E")),
|
||||
exp_final_chain: &[(3, h!("c")), (4, h!("d")), (5, h!("E"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "insert_conflict_should_evict",
|
||||
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e")), (6, h!("f"))],
|
||||
to_insert: (4, h!("D")),
|
||||
exp_final_chain: &[(3, h!("c")), (4, h!("D"))],
|
||||
},
|
||||
];
|
||||
|
||||
fn genesis_block() -> impl Iterator<Item = BlockId> {
|
||||
core::iter::once((0, h!("_"))).map(BlockId::from)
|
||||
}
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Running [{}] '{}'", i, t.name);
|
||||
|
||||
let chain = CheckPoint::from_block_ids(
|
||||
genesis_block().chain(t.chain.iter().copied().map(BlockId::from)),
|
||||
)
|
||||
.expect("test formed incorrectly, must construct checkpoint chain");
|
||||
|
||||
let exp_final_chain = CheckPoint::from_block_ids(
|
||||
genesis_block().chain(t.exp_final_chain.iter().copied().map(BlockId::from)),
|
||||
)
|
||||
.expect("test formed incorrectly, must construct checkpoint chain");
|
||||
|
||||
assert_eq!(
|
||||
chain.insert(t.to_insert.into()),
|
||||
exp_final_chain,
|
||||
"unexpected final chain"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chain_apply_header_connected_to() {
|
||||
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
||||
@@ -601,9 +674,9 @@ fn local_chain_apply_header_connected_to() {
|
||||
|
||||
let test_cases = [
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("A"));
|
||||
let header = header_from_prev_blockhash(h!("_"));
|
||||
let hash = header.block_hash();
|
||||
let height = 2;
|
||||
let height = 1;
|
||||
let connected_to = BlockId { height, hash };
|
||||
TestCase {
|
||||
name: "connected_to_self_header_applied_to_self",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
|
||||
use bitcoin::{absolute, transaction, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
|
||||
use bitcoin::{
|
||||
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn spk_txout_sent_and_received() {
|
||||
@@ -20,12 +22,23 @@ fn spk_txout_sent_and_received() {
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
|
||||
assert_eq!(index.net_value(&tx1), 42_000);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1, ..),
|
||||
(Amount::from_sat(0), Amount::from_sat(42_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1, ..1),
|
||||
(Amount::from_sat(0), Amount::from_sat(42_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1, 1..),
|
||||
(Amount::from_sat(0), Amount::from_sat(0))
|
||||
);
|
||||
assert_eq!(index.net_value(&tx1, ..), SignedAmount::from_sat(42_000));
|
||||
index.index_tx(&tx1);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1),
|
||||
(0, 42_000),
|
||||
index.sent_and_received(&tx1, ..),
|
||||
(Amount::from_sat(0), Amount::from_sat(42_000)),
|
||||
"shouldn't change after scanning"
|
||||
);
|
||||
|
||||
@@ -51,8 +64,19 @@ fn spk_txout_sent_and_received() {
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
|
||||
assert_eq!(index.net_value(&tx2), 8_000);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx2, ..),
|
||||
(Amount::from_sat(42_000), Amount::from_sat(50_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx2, ..1),
|
||||
(Amount::from_sat(42_000), Amount::from_sat(30_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx2, 1..),
|
||||
(Amount::from_sat(0), Amount::from_sat(20_000))
|
||||
);
|
||||
assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use bdk_chain::tx_graph::CalculateFeeError;
|
||||
@@ -1087,139 +1089,6 @@ fn update_last_seen_unconfirmed() {
|
||||
assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_blocks() {
|
||||
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, core::hash::Hash)]
|
||||
struct TestAnchor(BlockId);
|
||||
|
||||
impl Anchor for TestAnchor {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Scenario<'a> {
|
||||
name: &'a str,
|
||||
graph: TxGraph<TestAnchor>,
|
||||
chain: LocalChain,
|
||||
exp_heights: &'a [u32],
|
||||
}
|
||||
|
||||
const fn new_anchor(height: u32, hash: BlockHash) -> TestAnchor {
|
||||
TestAnchor(BlockId { height, hash })
|
||||
}
|
||||
|
||||
fn new_scenario<'a>(
|
||||
name: &'a str,
|
||||
graph_anchors: &'a [(Txid, TestAnchor)],
|
||||
chain: &'a [(u32, BlockHash)],
|
||||
exp_heights: &'a [u32],
|
||||
) -> Scenario<'a> {
|
||||
Scenario {
|
||||
name,
|
||||
graph: {
|
||||
let mut g = TxGraph::default();
|
||||
for (txid, anchor) in graph_anchors {
|
||||
let _ = g.insert_anchor(*txid, anchor.clone());
|
||||
}
|
||||
g
|
||||
},
|
||||
chain: {
|
||||
let (mut c, _) = LocalChain::from_genesis_hash(h!("genesis"));
|
||||
for (height, hash) in chain {
|
||||
let _ = c.insert_block(BlockId {
|
||||
height: *height,
|
||||
hash: *hash,
|
||||
});
|
||||
}
|
||||
c
|
||||
},
|
||||
exp_heights,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(scenarios: &[Scenario]) {
|
||||
for scenario in scenarios {
|
||||
let Scenario {
|
||||
name,
|
||||
graph,
|
||||
chain,
|
||||
exp_heights,
|
||||
} = scenario;
|
||||
|
||||
let heights = graph.missing_heights(chain).collect::<Vec<_>>();
|
||||
assert_eq!(&heights, exp_heights, "scenario: {}", name);
|
||||
}
|
||||
}
|
||||
|
||||
run(&[
|
||||
new_scenario(
|
||||
"2 txs with the same anchor (2:B) which is missing from chain",
|
||||
&[
|
||||
(h!("tx_1"), new_anchor(2, h!("B"))),
|
||||
(h!("tx_2"), new_anchor(2, h!("B"))),
|
||||
],
|
||||
&[(1, h!("A")), (3, h!("C"))],
|
||||
&[2],
|
||||
),
|
||||
new_scenario(
|
||||
"2 txs with different anchors at the same height, one of the anchors is missing",
|
||||
&[
|
||||
(h!("tx_1"), new_anchor(2, h!("B1"))),
|
||||
(h!("tx_2"), new_anchor(2, h!("B2"))),
|
||||
],
|
||||
&[(1, h!("A")), (2, h!("B1"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors of same height which are missing from the chain",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C1"))),
|
||||
(h!("tx"), new_anchor(3, h!("C2"))),
|
||||
],
|
||||
&[(1, h!("A")), (4, h!("D"))],
|
||||
&[3],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at the same height, chain has this height but does not match either anchor",
|
||||
&[
|
||||
(h!("tx"), new_anchor(4, h!("D1"))),
|
||||
(h!("tx"), new_anchor(4, h!("D2"))),
|
||||
],
|
||||
&[(4, h!("D3")), (5, h!("E"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, one anchor exists in chain, should return nothing",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C"))),
|
||||
(h!("tx"), new_anchor(4, h!("D"))),
|
||||
],
|
||||
&[(4, h!("D")), (5, h!("E"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, first height is already in chain with different hash, iterator should only return 2nd height",
|
||||
&[
|
||||
(h!("tx"), new_anchor(5, h!("E1"))),
|
||||
(h!("tx"), new_anchor(6, h!("F1"))),
|
||||
],
|
||||
&[(4, h!("D")), (5, h!("E")), (7, h!("G"))],
|
||||
&[6],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, neither height is in chain, both heights should be returned",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C"))),
|
||||
(h!("tx"), new_anchor(4, h!("D"))),
|
||||
],
|
||||
&[(1, h!("A")), (2, h!("B"))],
|
||||
&[3, 4],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// The `map_anchors` allow a caller to pass a function to reconstruct the [`TxGraph`] with any [`Anchor`],
|
||||
/// even though the function is non-deterministic.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use bdk_chain::{keychain::Balance, BlockId};
|
||||
use bitcoin::{OutPoint, Script};
|
||||
use bitcoin::{Amount, OutPoint, Script};
|
||||
use common::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -79,10 +81,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("confirmed_genesis", 0), ("confirmed_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("confirmed_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 20000,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(20000),
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -115,10 +117,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -150,10 +152,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -192,10 +194,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_3", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 40000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(40000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -227,10 +229,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -262,10 +264,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_1", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 20000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(20000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -311,10 +313,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(50000),
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -356,10 +358,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -397,10 +399,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 20000,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(20000),
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -442,10 +444,10 @@ fn test_tx_conflict_handling() {
|
||||
]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -487,10 +489,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -532,10 +534,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(50000),
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -583,10 +585,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(50000),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,11 +12,9 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.12.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0" }
|
||||
electrum-client = { version = "0.19" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
anyhow = "1"
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
@@ -1,164 +1,48 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
|
||||
local_chain::{self, CheckPoint},
|
||||
tx_graph::{self, TxGraph},
|
||||
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
use electrum_client::{Client, ElectrumApi, Error, HeaderNotification};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
str::FromStr,
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
local_chain::CheckPoint,
|
||||
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult, TxCache},
|
||||
tx_graph::TxGraph,
|
||||
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
use core::str::FromStr;
|
||||
use electrum_client::{ElectrumApi, Error, HeaderNotification};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// We include a chain suffix of a certain length for the purpose of robustness.
|
||||
const CHAIN_SUFFIX_LENGTH: u32 = 8;
|
||||
|
||||
/// Represents updates fetched from an Electrum server, but excludes full transactions.
|
||||
///
|
||||
/// To provide a complete update to [`TxGraph`], you'll need to call [`Self::missing_full_txs`] to
|
||||
/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::into_tx_graph`] to
|
||||
/// fetch the full transactions from Electrum and finalize the update.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RelevantTxids(HashMap<Txid, BTreeSet<ConfirmationHeightAnchor>>);
|
||||
|
||||
impl RelevantTxids {
|
||||
/// Determine the full transactions that are missing from `graph`.
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
pub fn missing_full_txs<A: Anchor>(&self, graph: &TxGraph<A>) -> Vec<Txid> {
|
||||
self.0
|
||||
.keys()
|
||||
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Finalizes the [`TxGraph`] update by fetching `missing` txids from the `client`.
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
pub fn into_tx_graph(
|
||||
self,
|
||||
client: &Client,
|
||||
missing: Vec<Txid>,
|
||||
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
|
||||
let new_txs = client.batch_transaction_get(&missing)?;
|
||||
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
|
||||
for (txid, anchors) in self.0 {
|
||||
for anchor in anchors {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
/// Finalizes the update by fetching `missing` txids from the `client`, where the
|
||||
/// resulting [`TxGraph`] has anchors of type [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
///
|
||||
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
|
||||
// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
|
||||
// use it.
|
||||
pub fn into_confirmation_time_tx_graph(
|
||||
self,
|
||||
client: &Client,
|
||||
missing: Vec<Txid>,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let graph = self.into_tx_graph(client, missing)?;
|
||||
|
||||
let relevant_heights = {
|
||||
let mut visited_heights = HashSet::new();
|
||||
graph
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|(a, _)| a.confirmation_height_upper_bound())
|
||||
.filter(move |&h| visited_heights.insert(h))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let height_to_time = relevant_heights
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(
|
||||
client
|
||||
.batch_block_header(relevant_heights)?
|
||||
.into_iter()
|
||||
.map(|bh| bh.time as u64),
|
||||
)
|
||||
.collect::<HashMap<u32, u64>>();
|
||||
|
||||
let graph_changeset = {
|
||||
let old_changeset = TxGraph::default().apply_update(graph);
|
||||
tx_graph::ChangeSet {
|
||||
txs: old_changeset.txs,
|
||||
txouts: old_changeset.txouts,
|
||||
last_seen: old_changeset.last_seen,
|
||||
anchors: old_changeset
|
||||
.anchors
|
||||
.into_iter()
|
||||
.map(|(height_anchor, txid)| {
|
||||
let confirmation_height = height_anchor.confirmation_height;
|
||||
let confirmation_time = height_to_time[&confirmation_height];
|
||||
let time_anchor = ConfirmationTimeHeightAnchor {
|
||||
anchor_block: height_anchor.anchor_block,
|
||||
confirmation_height,
|
||||
confirmation_time,
|
||||
};
|
||||
(time_anchor, txid)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_graph = TxGraph::default();
|
||||
new_graph.apply_changeset(graph_changeset);
|
||||
Ok(new_graph)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combination of chain and transactions updates from electrum
|
||||
///
|
||||
/// We have to update the chain and the txids at the same time since we anchor the txids to
|
||||
/// the same chain tip that we check before and after we gather the txids.
|
||||
#[derive(Debug)]
|
||||
pub struct ElectrumUpdate {
|
||||
/// Chain update
|
||||
pub chain_update: local_chain::Update,
|
||||
/// Transaction updates from electrum
|
||||
pub relevant_txids: RelevantTxids,
|
||||
}
|
||||
|
||||
/// Trait to extend [`Client`] functionality.
|
||||
/// Trait to extend [`electrum_client::Client`] functionality.
|
||||
pub trait ElectrumExt {
|
||||
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
|
||||
/// returns updates for [`bdk_chain`] data structures.
|
||||
///
|
||||
/// - `prev_tip`: the most recent blockchain tip present locally
|
||||
/// - `keychain_spks`: keychains that we want to scan transactions for
|
||||
///
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
|
||||
/// single batch request.
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
|
||||
/// see [`FullScanRequest`]
|
||||
/// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no
|
||||
/// associated transactions
|
||||
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
|
||||
/// request
|
||||
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
|
||||
/// calculation
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
request: FullScanRequest<K>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumFullScanResult<K>, Error>;
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
|
||||
/// and returns updates for [`bdk_chain`] data structures.
|
||||
///
|
||||
/// - `prev_tip`: the most recent blockchain tip present locally
|
||||
/// - `misc_spks`: an iterator of scripts we want to sync transactions for
|
||||
/// - `txids`: transactions for which we want updated [`Anchor`]s
|
||||
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to include in the update
|
||||
///
|
||||
/// `batch_size` specifies the max number of script pubkeys to request for in a single batch
|
||||
/// request.
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client sync,
|
||||
/// see [`SyncRequest`]
|
||||
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
|
||||
/// request
|
||||
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
|
||||
/// calculation
|
||||
///
|
||||
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
@@ -166,31 +50,33 @@ pub trait ElectrumExt {
|
||||
/// [`full_scan`]: ElectrumExt::full_scan
|
||||
fn sync(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
request: SyncRequest,
|
||||
batch_size: usize,
|
||||
) -> Result<ElectrumUpdate, Error>;
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumSyncResult, Error>;
|
||||
}
|
||||
|
||||
impl<A: ElectrumApi> ElectrumExt for A {
|
||||
impl<E: ElectrumApi> ElectrumExt for E {
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
mut request: FullScanRequest<K>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
||||
let mut request_spks = keychain_spks
|
||||
.into_iter()
|
||||
.map(|(k, s)| (k, s.into_iter()))
|
||||
.collect::<BTreeMap<K, _>>();
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumFullScanResult<K>, Error> {
|
||||
let mut request_spks = request.spks_by_keychain;
|
||||
|
||||
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
|
||||
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
|
||||
// cannot be collected. In addition, we keep track of whether an spk has an active tx
|
||||
// history for determining the `last_active_index`.
|
||||
// * key: (keychain, spk_index) that identifies the spk.
|
||||
// * val: (script_pubkey, has_tx_history).
|
||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||
|
||||
let (electrum_update, keychain_update) = loop {
|
||||
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
|
||||
let mut relevant_txids = RelevantTxids::default();
|
||||
let update = loop {
|
||||
let (tip, _) = construct_update_tip(self, request.chain_tip.clone())?;
|
||||
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
@@ -202,7 +88,8 @@ impl<A: ElectrumApi> ElectrumExt for A {
|
||||
scanned_spks.append(&mut populate_with_spks(
|
||||
self,
|
||||
&cps,
|
||||
&mut relevant_txids,
|
||||
&mut request.tx_cache,
|
||||
&mut graph_update,
|
||||
&mut scanned_spks
|
||||
.iter()
|
||||
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
|
||||
@@ -215,7 +102,8 @@ impl<A: ElectrumApi> ElectrumExt for A {
|
||||
populate_with_spks(
|
||||
self,
|
||||
&cps,
|
||||
&mut relevant_txids,
|
||||
&mut request.tx_cache,
|
||||
&mut graph_update,
|
||||
keychain_spks,
|
||||
stop_gap,
|
||||
batch_size,
|
||||
@@ -232,10 +120,12 @@ impl<A: ElectrumApi> ElectrumExt for A {
|
||||
continue; // reorg
|
||||
}
|
||||
|
||||
let chain_update = local_chain::Update {
|
||||
tip,
|
||||
introduce_older_blocks: true,
|
||||
};
|
||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||
if fetch_prev_txouts {
|
||||
fetch_prev_txout(self, &mut request.tx_cache, &mut graph_update)?;
|
||||
}
|
||||
|
||||
let chain_update = tip;
|
||||
|
||||
let keychain_update = request_spks
|
||||
.into_keys()
|
||||
@@ -248,54 +138,148 @@ impl<A: ElectrumApi> ElectrumExt for A {
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
break (
|
||||
ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
},
|
||||
keychain_update,
|
||||
);
|
||||
break FullScanResult {
|
||||
graph_update,
|
||||
chain_update,
|
||||
last_active_indices: keychain_update,
|
||||
};
|
||||
};
|
||||
|
||||
Ok((electrum_update, keychain_update))
|
||||
Ok(ElectrumFullScanResult(update))
|
||||
}
|
||||
|
||||
fn sync(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
request: SyncRequest,
|
||||
batch_size: usize,
|
||||
) -> Result<ElectrumUpdate, Error> {
|
||||
let spk_iter = misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk));
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumSyncResult, Error> {
|
||||
let mut tx_cache = request.tx_cache.clone();
|
||||
|
||||
let (mut electrum_update, _) = self.full_scan(
|
||||
prev_tip.clone(),
|
||||
[((), spk_iter)].into(),
|
||||
usize::MAX,
|
||||
batch_size,
|
||||
)?;
|
||||
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
|
||||
.cache_txs(request.tx_cache)
|
||||
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
|
||||
let mut full_scan_res = self
|
||||
.full_scan(full_scan_req, usize::MAX, batch_size, false)?
|
||||
.with_confirmation_height_anchor();
|
||||
|
||||
let (tip, _) = construct_update_tip(self, prev_tip)?;
|
||||
let (tip, _) = construct_update_tip(self, request.chain_tip)?;
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
|
||||
populate_with_txids(self, &cps, &mut electrum_update.relevant_txids, txids)?;
|
||||
populate_with_txids(
|
||||
self,
|
||||
&cps,
|
||||
&mut tx_cache,
|
||||
&mut full_scan_res.graph_update,
|
||||
request.txids,
|
||||
)?;
|
||||
populate_with_outpoints(
|
||||
self,
|
||||
&cps,
|
||||
&mut tx_cache,
|
||||
&mut full_scan_res.graph_update,
|
||||
request.outpoints,
|
||||
)?;
|
||||
|
||||
let _txs =
|
||||
populate_with_outpoints(self, &cps, &mut electrum_update.relevant_txids, outpoints)?;
|
||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||
if fetch_prev_txouts {
|
||||
fetch_prev_txout(self, &mut tx_cache, &mut full_scan_res.graph_update)?;
|
||||
}
|
||||
|
||||
Ok(electrum_update)
|
||||
Ok(ElectrumSyncResult(SyncResult {
|
||||
chain_update: full_scan_res.chain_update,
|
||||
graph_update: full_scan_res.graph_update,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [`ElectrumExt::full_scan`].
|
||||
///
|
||||
/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or
|
||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
||||
pub struct ElectrumFullScanResult<K>(FullScanResult<K, ConfirmationHeightAnchor>);
|
||||
|
||||
impl<K> ElectrumFullScanResult<K> {
|
||||
/// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`].
|
||||
pub fn with_confirmation_height_anchor(self) -> FullScanResult<K, ConfirmationHeightAnchor> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// This requires additional calls to the Electrum server.
|
||||
pub fn with_confirmation_time_height_anchor(
|
||||
self,
|
||||
client: &impl ElectrumApi,
|
||||
) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
|
||||
let res = self.0;
|
||||
Ok(FullScanResult {
|
||||
graph_update: try_into_confirmation_time_result(res.graph_update, client)?,
|
||||
chain_update: res.chain_update,
|
||||
last_active_indices: res.last_active_indices,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [`ElectrumExt::sync`].
|
||||
///
|
||||
/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or
|
||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
||||
pub struct ElectrumSyncResult(SyncResult<ConfirmationHeightAnchor>);
|
||||
|
||||
impl ElectrumSyncResult {
|
||||
/// Return [`SyncResult`] with [`ConfirmationHeightAnchor`].
|
||||
pub fn with_confirmation_height_anchor(self) -> SyncResult<ConfirmationHeightAnchor> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// This requires additional calls to the Electrum server.
|
||||
pub fn with_confirmation_time_height_anchor(
|
||||
self,
|
||||
client: &impl ElectrumApi,
|
||||
) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let res = self.0;
|
||||
Ok(SyncResult {
|
||||
graph_update: try_into_confirmation_time_result(res.graph_update, client)?,
|
||||
chain_update: res.chain_update,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn try_into_confirmation_time_result(
|
||||
graph_update: TxGraph<ConfirmationHeightAnchor>,
|
||||
client: &impl ElectrumApi,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let relevant_heights = graph_update
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|(a, _)| a.confirmation_height)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let height_to_time = relevant_heights
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(
|
||||
client
|
||||
.batch_block_header(relevant_heights)?
|
||||
.into_iter()
|
||||
.map(|bh| bh.time as u64),
|
||||
)
|
||||
.collect::<HashMap<u32, u64>>();
|
||||
|
||||
Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
|
||||
anchor_block: a.anchor_block,
|
||||
confirmation_height: a.confirmation_height,
|
||||
confirmation_time: height_to_time[&a.confirmation_height],
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||
fn construct_update_tip(
|
||||
client: &impl ElectrumApi,
|
||||
@@ -411,48 +395,48 @@ fn determine_tx_anchor(
|
||||
}
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
|
||||
///
|
||||
/// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
|
||||
/// included. Anchors of the aforementioned transactions are included.
|
||||
///
|
||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
||||
fn populate_with_outpoints(
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
tx_cache: &mut TxCache,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
) -> Result<HashMap<Txid, Transaction>, Error> {
|
||||
let mut full_txs = HashMap::new();
|
||||
) -> Result<(), Error> {
|
||||
for outpoint in outpoints {
|
||||
let txid = outpoint.txid;
|
||||
let tx = client.transaction_get(&txid)?;
|
||||
debug_assert_eq!(tx.txid(), txid);
|
||||
let txout = match tx.output.get(outpoint.vout as usize) {
|
||||
let op_txid = outpoint.txid;
|
||||
let op_tx = fetch_tx(client, tx_cache, op_txid)?;
|
||||
let op_txout = match op_tx.output.get(outpoint.vout as usize) {
|
||||
Some(txout) => txout,
|
||||
None => continue,
|
||||
};
|
||||
debug_assert_eq!(op_tx.txid(), op_txid);
|
||||
|
||||
// attempt to find the following transactions (alongside their chain positions), and
|
||||
// add to our sparsechain `update`:
|
||||
let mut has_residing = false; // tx in which the outpoint resides
|
||||
let mut has_spending = false; // tx that spends the outpoint
|
||||
for res in client.script_get_history(&txout.script_pubkey)? {
|
||||
for res in client.script_get_history(&op_txout.script_pubkey)? {
|
||||
if has_residing && has_spending {
|
||||
break;
|
||||
}
|
||||
|
||||
if res.tx_hash == txid {
|
||||
if has_residing {
|
||||
continue;
|
||||
}
|
||||
if !has_residing && res.tx_hash == op_txid {
|
||||
has_residing = true;
|
||||
full_txs.insert(res.tx_hash, tx.clone());
|
||||
} else {
|
||||
if has_spending {
|
||||
continue;
|
||||
let _ = graph_update.insert_tx(Arc::clone(&op_tx));
|
||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
||||
}
|
||||
let res_tx = match full_txs.get(&res.tx_hash) {
|
||||
Some(tx) => tx,
|
||||
None => {
|
||||
let res_tx = client.transaction_get(&res.tx_hash)?;
|
||||
full_txs.insert(res.tx_hash, res_tx);
|
||||
full_txs.get(&res.tx_hash).expect("just inserted")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if !has_spending && res.tx_hash != op_txid {
|
||||
let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?;
|
||||
// we exclude txs/anchors that do not spend our specified outpoint(s)
|
||||
has_spending = res_tx
|
||||
.input
|
||||
.iter()
|
||||
@@ -460,26 +444,26 @@ fn populate_with_outpoints(
|
||||
if !has_spending {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let anchor = determine_tx_anchor(cps, res.height, res.tx_hash);
|
||||
let tx_entry = relevant_txids.0.entry(res.tx_hash).or_default();
|
||||
if let Some(anchor) = anchor {
|
||||
tx_entry.insert(anchor);
|
||||
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
|
||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(full_txs)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
|
||||
fn populate_with_txids(
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
tx_cache: &mut TxCache,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
) -> Result<(), Error> {
|
||||
for txid in txids {
|
||||
let tx = match client.transaction_get(&txid) {
|
||||
let tx = match fetch_tx(client, tx_cache, txid) {
|
||||
Ok(tx) => tx,
|
||||
Err(electrum_client::Error::Protocol(_)) => continue,
|
||||
Err(other_err) => return Err(other_err),
|
||||
@@ -491,6 +475,8 @@ fn populate_with_txids(
|
||||
.map(|txo| &txo.script_pubkey)
|
||||
.expect("tx must have an output");
|
||||
|
||||
// because of restrictions of the Electrum API, we have to use the `script_get_history`
|
||||
// call to get confirmation status of our transaction
|
||||
let anchor = match client
|
||||
.script_get_history(spk)?
|
||||
.into_iter()
|
||||
@@ -500,18 +486,64 @@ fn populate_with_txids(
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let tx_entry = relevant_txids.0.entry(txid).or_default();
|
||||
let _ = graph_update.insert_tx(tx);
|
||||
if let Some(anchor) = anchor {
|
||||
tx_entry.insert(anchor);
|
||||
let _ = graph_update.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch transaction of given `txid`.
|
||||
///
|
||||
/// We maintain a `tx_cache` so that we won't need to fetch from Electrum with every call.
|
||||
fn fetch_tx<C: ElectrumApi>(
|
||||
client: &C,
|
||||
tx_cache: &mut TxCache,
|
||||
txid: Txid,
|
||||
) -> Result<Arc<Transaction>, Error> {
|
||||
use bdk_chain::collections::hash_map::Entry;
|
||||
Ok(match tx_cache.entry(txid) {
|
||||
Entry::Occupied(entry) => entry.get().clone(),
|
||||
Entry::Vacant(entry) => entry
|
||||
.insert(Arc::new(client.transaction_get(&txid)?))
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
|
||||
// which we do not have by default. This data is needed to calculate the transaction fee.
|
||||
fn fetch_prev_txout<C: ElectrumApi>(
|
||||
client: &C,
|
||||
tx_cache: &mut TxCache,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
) -> Result<(), Error> {
|
||||
let full_txs: Vec<Arc<Transaction>> =
|
||||
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
|
||||
for tx in full_txs {
|
||||
for vin in &tx.input {
|
||||
let outpoint = vin.previous_output;
|
||||
let prev_tx = fetch_tx(client, tx_cache, outpoint.txid)?;
|
||||
for txout in prev_tx.output.clone() {
|
||||
let _ = graph_update.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
|
||||
///
|
||||
/// Transactions that contains an output with requested spk, or spends form an output with
|
||||
/// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
|
||||
/// also included.
|
||||
///
|
||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
||||
fn populate_with_spks<I: Ord + Clone>(
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
tx_cache: &mut TxCache,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
@@ -543,10 +575,10 @@ fn populate_with_spks<I: Ord + Clone>(
|
||||
unused_spk_count = 0;
|
||||
}
|
||||
|
||||
for tx in spk_history {
|
||||
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
|
||||
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
|
||||
tx_entry.insert(anchor);
|
||||
for tx_res in spk_history {
|
||||
let _ = graph_update.insert_tx(fetch_tx(client, tx_cache, tx_res.tx_hash)?);
|
||||
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,10 @@
|
||||
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
|
||||
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
|
||||
//! sync or full scan the user receives relevant blockchain data and output updates for
|
||||
//! [`bdk_chain`] including [`RelevantTxids`].
|
||||
//!
|
||||
//! The [`RelevantTxids`] only includes `txid`s and not full transactions. The caller is responsible
|
||||
//! for obtaining full transactions before applying new data to their [`bdk_chain`]. This can be
|
||||
//! done with these steps:
|
||||
//!
|
||||
//! 1. Determine which full transactions are missing. Use [`RelevantTxids::missing_full_txs`].
|
||||
//!
|
||||
//! 2. Obtaining the full transactions. To do this via electrum use [`ElectrumApi::batch_transaction_get`].
|
||||
//! [`bdk_chain`].
|
||||
//!
|
||||
//! Refer to [`example_electrum`] for a complete example.
|
||||
//!
|
||||
//! [`ElectrumApi::batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get
|
||||
//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use anyhow::Result;
|
||||
use bdk_chain::{
|
||||
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
|
||||
keychain::Balance,
|
||||
local_chain::LocalChain,
|
||||
spk_client::SyncRequest,
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bdk_electrum::{ElectrumExt, ElectrumUpdate};
|
||||
use bdk_testenv::TestEnv;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use bdk_electrum::ElectrumExt;
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
|
||||
fn get_balance(
|
||||
recv_chain: &LocalChain,
|
||||
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
|
||||
) -> Result<Balance> {
|
||||
) -> anyhow::Result<Balance> {
|
||||
let chain_tip = recv_chain.tip().block_id();
|
||||
let outpoints = recv_graph.index.outpoints().clone();
|
||||
let balance = recv_graph
|
||||
@@ -28,7 +27,7 @@ fn get_balance(
|
||||
/// 3. Mine extra block to confirm sent tx.
|
||||
/// 4. Check [`Balance`] to ensure tx is confirmed.
|
||||
#[test]
|
||||
fn scan_detects_confirmed_tx() -> Result<()> {
|
||||
fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
@@ -62,27 +61,52 @@ fn scan_detects_confirmed_tx() -> Result<()> {
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?;
|
||||
let update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip())
|
||||
.chain_spks(core::iter::once(spk_to_track)),
|
||||
5,
|
||||
true,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.apply_update(update.chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(graph_update);
|
||||
let _ = recv_graph.apply_update(update.graph_update);
|
||||
|
||||
// Check to see if tx is confirmed.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat(),
|
||||
confirmed: SEND_AMOUNT,
|
||||
..Balance::default()
|
||||
},
|
||||
);
|
||||
|
||||
for tx in recv_graph.graph().full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transaction's previous outputs.
|
||||
let fee = recv_graph
|
||||
.graph()
|
||||
.calculate_fee(&tx.tx)
|
||||
.expect("fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_sat() as u64;
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -93,7 +117,7 @@ fn scan_detects_confirmed_tx() -> Result<()> {
|
||||
/// 3. Perform 8 separate reorgs on each block with a confirmed tx.
|
||||
/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct.
|
||||
#[test]
|
||||
fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
|
||||
fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
const REORG_COUNT: usize = 8;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
@@ -128,26 +152,27 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
|
||||
let update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.apply_update(update.chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(graph_update.clone());
|
||||
let _ = recv_graph.apply_update(update.graph_update.clone());
|
||||
|
||||
// Retain a snapshot of all anchors before reorg process.
|
||||
let initial_anchors = graph_update.all_anchors();
|
||||
let initial_anchors = update.graph_update.all_anchors();
|
||||
|
||||
// Check if initial balance is correct.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * REORG_COUNT as u64,
|
||||
confirmed: SEND_AMOUNT * REORG_COUNT as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"initial balance must be correct",
|
||||
@@ -158,28 +183,29 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
|
||||
env.reorg_empty_blocks(depth)?;
|
||||
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
|
||||
let update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.apply_update(update.chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
|
||||
// Check to see if a new anchor is added during current reorg.
|
||||
if !initial_anchors.is_superset(graph_update.all_anchors()) {
|
||||
if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
|
||||
println!("New anchor added at reorg depth {}", depth);
|
||||
}
|
||||
let _ = recv_graph.apply_update(graph_update);
|
||||
let _ = recv_graph.apply_update(update.graph_update);
|
||||
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * (REORG_COUNT - depth) as u64,
|
||||
trusted_pending: SEND_AMOUNT.to_sat() * depth as u64,
|
||||
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
|
||||
trusted_pending: SEND_AMOUNT * depth as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.12.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", default-features = false }
|
||||
esplora-client = { version = "0.7.0", default-features = false }
|
||||
async-trait = { version = "0.1.66", optional = true }
|
||||
futures = { version = "0.3.26", optional = true }
|
||||
@@ -23,13 +23,13 @@ miniscript = { version = "11.0.0", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default_features = false }
|
||||
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["std", "async-https", "blocking"]
|
||||
default = ["std", "async-https", "blocking-https-rustls"]
|
||||
std = ["bdk_chain/std"]
|
||||
async = ["async-trait", "futures", "esplora-client/async"]
|
||||
async-https = ["async", "esplora-client/async-https"]
|
||||
async-https-rustls = ["async", "esplora-client/async-https-rustls"]
|
||||
blocking = ["esplora-client/blocking"]
|
||||
blocking-https-rustls = ["esplora-client/blocking-https-rustls"]
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult};
|
||||
use bdk_chain::Anchor;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
collections::BTreeMap,
|
||||
local_chain::{self, CheckPoint},
|
||||
local_chain::CheckPoint,
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use esplora_client::TxStatus;
|
||||
use esplora_client::{Amount, TxStatus};
|
||||
use futures::{stream::FuturesOrdered, TryStreamExt};
|
||||
|
||||
use crate::anchor_from_status;
|
||||
@@ -22,36 +25,15 @@ type Error = Box<esplora_client::Error>;
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EsploraAsyncExt {
|
||||
/// Prepare a [`LocalChain`] update with blocks fetched from Esplora.
|
||||
/// Scan keychain scripts for transactions against Esplora, returning an update that can be
|
||||
/// applied to the receiving structures.
|
||||
///
|
||||
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
|
||||
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
|
||||
/// see [`FullScanRequest`]
|
||||
///
|
||||
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
||||
///
|
||||
/// ## Consistency
|
||||
///
|
||||
/// The chain update returned is guaranteed to be consistent as long as there is not a *large* re-org
|
||||
/// during the call. The size of re-org we can tollerate is server dependent but will be at
|
||||
/// least 10.
|
||||
///
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error>;
|
||||
|
||||
/// Full scan the keychain scripts specified with the blockchain (via an Esplora client) and
|
||||
/// returns a [`TxGraph`] and a map of last active indices.
|
||||
///
|
||||
/// * `keychain_spks`: keychains that we want to scan transactions for
|
||||
///
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no
|
||||
/// associated transactions. `parallel_requests` specifies the max number of HTTP requests to
|
||||
/// make in parallel.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
@@ -67,21 +49,16 @@ pub trait EsploraAsyncExt {
|
||||
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
||||
async fn full_scan<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
||||
>,
|
||||
request: FullScanRequest<K>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
||||
) -> Result<FullScanResult<K>, Error>;
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
|
||||
/// specified and return a [`TxGraph`].
|
||||
///
|
||||
/// * `misc_spks`: scripts that we want to sync transactions for
|
||||
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
|
||||
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to include in the update
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client sync, see
|
||||
/// [`SyncRequest`]
|
||||
///
|
||||
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
@@ -89,207 +66,210 @@ pub trait EsploraAsyncExt {
|
||||
/// [`full_scan`]: EsploraAsyncExt::full_scan
|
||||
async fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
request: SyncRequest,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
|
||||
) -> Result<SyncResult, Error>;
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
// Fetch latest N (server dependent) blocks from Esplora. The server guarantees these are
|
||||
// consistent.
|
||||
let mut fetched_blocks = self
|
||||
.get_blocks(None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| (b.time.height, b.id))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
let new_tip_height = fetched_blocks
|
||||
.keys()
|
||||
.last()
|
||||
.copied()
|
||||
.expect("must have atleast one block");
|
||||
|
||||
// Fetch blocks of heights that the caller is interested in, skipping blocks that are
|
||||
// already fetched when constructing `fetched_blocks`.
|
||||
for height in request_heights {
|
||||
// do not fetch blocks higher than remote tip
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
// only fetch what is missing
|
||||
if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) {
|
||||
// ❗The return value of `get_block_hash` is not strictly guaranteed to be consistent
|
||||
// with the chain at the time of `get_blocks` above (there could have been a deep
|
||||
// re-org). Since `get_blocks` returns 10 (or so) blocks we are assuming that it's
|
||||
// not possible to have a re-org deeper than that.
|
||||
entry.insert(self.get_block_hash(height).await?);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `fetched_blocks` can create an update that connects with the original chain by
|
||||
// finding a "Point of Agreement".
|
||||
for (height, local_hash) in local_tip.iter().map(|cp| (cp.height(), cp.hash())) {
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fetched_hash = match fetched_blocks.entry(height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => {
|
||||
*entry.insert(self.get_block_hash(height).await?)
|
||||
}
|
||||
};
|
||||
|
||||
// We have found point of agreement so the update will connect!
|
||||
if fetched_hash == local_hash {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(local_chain::Update {
|
||||
tip: CheckPoint::from_block_ids(fetched_blocks.into_iter().map(BlockId::from))
|
||||
.expect("must be in height order"),
|
||||
introduce_older_blocks: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn full_scan<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
||||
>,
|
||||
request: FullScanRequest<K>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
let stop_gap = Ord::max(stop_gap, 1);
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
let mut last_index = Option::<u32>::None;
|
||||
let mut last_active_index = Option::<u32>::None;
|
||||
|
||||
loop {
|
||||
let handles = spks
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.map(|(spk_index, spk)| {
|
||||
let client = self.clone();
|
||||
async move {
|
||||
let mut last_seen = None;
|
||||
let mut spk_txs = Vec::new();
|
||||
loop {
|
||||
let txs = client.scripthash_txs(&spk, last_seen).await?;
|
||||
let tx_count = txs.len();
|
||||
last_seen = txs.last().map(|tx| tx.txid);
|
||||
spk_txs.extend(txs);
|
||||
if tx_count < 25 {
|
||||
break Result::<_, Error>::Ok((spk_index, spk_txs));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
|
||||
last_index = Some(index);
|
||||
if !txs.is_empty() {
|
||||
last_active_index = Some(index);
|
||||
}
|
||||
for tx in txs {
|
||||
let _ = graph.insert_tx(tx.to_tx());
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: Amount::from_sat(prevout.value),
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||
let gap_limit_reached = if let Some(i) = last_active_index {
|
||||
last_index >= i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index + 1 >= stop_gap as u32
|
||||
};
|
||||
if gap_limit_reached {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_active_index) = last_active_index {
|
||||
last_active_indexes.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((graph, last_active_indexes))
|
||||
) -> Result<FullScanResult<K>, Error> {
|
||||
let latest_blocks = fetch_latest_blocks(self).await?;
|
||||
let (graph_update, last_active_indices) = full_scan_for_index_and_graph(
|
||||
self,
|
||||
request.spks_by_keychain,
|
||||
stop_gap,
|
||||
parallel_requests,
|
||||
)
|
||||
.await?;
|
||||
let chain_update = chain_update(
|
||||
self,
|
||||
&latest_blocks,
|
||||
&request.chain_tip,
|
||||
graph_update.all_anchors(),
|
||||
)
|
||||
.await?;
|
||||
Ok(FullScanResult {
|
||||
chain_update,
|
||||
graph_update,
|
||||
last_active_indices,
|
||||
})
|
||||
}
|
||||
|
||||
async fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
request: SyncRequest,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let mut graph = self
|
||||
.full_scan(
|
||||
[(
|
||||
(),
|
||||
misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk)),
|
||||
)]
|
||||
.into(),
|
||||
usize::MAX,
|
||||
parallel_requests,
|
||||
)
|
||||
.await
|
||||
.map(|(g, _)| g)?;
|
||||
) -> Result<SyncResult, Error> {
|
||||
let latest_blocks = fetch_latest_blocks(self).await?;
|
||||
let graph_update = sync_for_index_and_graph(
|
||||
self,
|
||||
request.spks,
|
||||
request.txids,
|
||||
request.outpoints,
|
||||
parallel_requests,
|
||||
)
|
||||
.await?;
|
||||
let chain_update = chain_update(
|
||||
self,
|
||||
&latest_blocks,
|
||||
&request.chain_tip,
|
||||
graph_update.all_anchors(),
|
||||
)
|
||||
.await?;
|
||||
Ok(SyncResult {
|
||||
chain_update,
|
||||
graph_update,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch latest blocks from Esplora in an atomic call.
|
||||
///
|
||||
/// We want to do this before fetching transactions and anchors as we cannot fetch latest blocks AND
|
||||
/// transactions atomically, and the checkpoint tip is used to determine last-scanned block (for
|
||||
/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
|
||||
/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
|
||||
/// alternating between chain-sources.
|
||||
async fn fetch_latest_blocks(
|
||||
client: &esplora_client::AsyncClient,
|
||||
) -> Result<BTreeMap<u32, BlockHash>, Error> {
|
||||
Ok(client
|
||||
.get_blocks(None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| (b.time.height, b.id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Used instead of [`esplora_client::BlockingClient::get_block_hash`].
|
||||
///
|
||||
/// This first checks the previously fetched `latest_blocks` before fetching from Esplora again.
|
||||
async fn fetch_block(
|
||||
client: &esplora_client::AsyncClient,
|
||||
latest_blocks: &BTreeMap<u32, BlockHash>,
|
||||
height: u32,
|
||||
) -> Result<Option<BlockHash>, Error> {
|
||||
if let Some(&hash) = latest_blocks.get(&height) {
|
||||
return Ok(Some(hash));
|
||||
}
|
||||
|
||||
// We avoid fetching blocks higher than previously fetched `latest_blocks` as the local chain
|
||||
// tip is used to signal for the last-synced-up-to-height.
|
||||
let &tip_height = latest_blocks
|
||||
.keys()
|
||||
.last()
|
||||
.expect("must have atleast one entry");
|
||||
if height > tip_height {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(client.get_block_hash(height).await?))
|
||||
}
|
||||
|
||||
/// Create the [`local_chain::Update`].
|
||||
///
|
||||
/// We want to have a corresponding checkpoint per anchor height. However, checkpoints fetched
|
||||
/// should not surpass `latest_blocks`.
|
||||
async fn chain_update<A: Anchor>(
|
||||
client: &esplora_client::AsyncClient,
|
||||
latest_blocks: &BTreeMap<u32, BlockHash>,
|
||||
local_tip: &CheckPoint,
|
||||
anchors: &BTreeSet<(A, Txid)>,
|
||||
) -> Result<CheckPoint, Error> {
|
||||
let mut point_of_agreement = None;
|
||||
let mut conflicts = vec![];
|
||||
for local_cp in local_tip.iter() {
|
||||
let remote_hash = match fetch_block(client, latest_blocks, local_cp.height()).await? {
|
||||
Some(hash) => hash,
|
||||
None => continue,
|
||||
};
|
||||
if remote_hash == local_cp.hash() {
|
||||
point_of_agreement = Some(local_cp.clone());
|
||||
break;
|
||||
} else {
|
||||
// it is not strictly necessary to include all the conflicted heights (we do need the
|
||||
// first one) but it seems prudent to make sure the updated chain's heights are a
|
||||
// superset of the existing chain after update.
|
||||
conflicts.push(BlockId {
|
||||
height: local_cp.height(),
|
||||
hash: remote_hash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut tip = point_of_agreement.expect("remote esplora should have same genesis block");
|
||||
|
||||
tip = tip
|
||||
.extend(conflicts.into_iter().rev())
|
||||
.expect("evicted are in order");
|
||||
|
||||
for anchor in anchors {
|
||||
let height = anchor.0.anchor_block().height;
|
||||
if tip.get(height).is_none() {
|
||||
let hash = match fetch_block(client, latest_blocks, height).await? {
|
||||
Some(hash) => hash,
|
||||
None => continue,
|
||||
};
|
||||
tip = tip.insert(BlockId { height, hash });
|
||||
}
|
||||
}
|
||||
|
||||
// insert the most recent blocks at the tip to make sure we update the tip and make the update
|
||||
// robust.
|
||||
for (&height, &hash) in latest_blocks.iter() {
|
||||
tip = tip.insert(BlockId { height, hash });
|
||||
}
|
||||
|
||||
Ok(tip)
|
||||
}
|
||||
|
||||
/// This performs a full scan to get an update for the [`TxGraph`] and
|
||||
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
||||
async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
||||
client: &esplora_client::AsyncClient,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
||||
>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
let mut last_index = Option::<u32>::None;
|
||||
let mut last_active_index = Option::<u32>::None;
|
||||
|
||||
let mut txids = txids.into_iter();
|
||||
loop {
|
||||
let handles = txids
|
||||
let handles = spks
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.filter(|&txid| graph.get_tx(txid).is_none())
|
||||
.map(|txid| {
|
||||
let client = self.clone();
|
||||
async move { client.get_tx_status(&txid).await.map(|s| (txid, s)) }
|
||||
.map(|(spk_index, spk)| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let mut last_seen = None;
|
||||
let mut spk_txs = Vec::new();
|
||||
loop {
|
||||
let txs = client.scripthash_txs(&spk, last_seen).await?;
|
||||
let tx_count = txs.len();
|
||||
last_seen = txs.last().map(|tx| tx.txid);
|
||||
spk_txs.extend(txs);
|
||||
if tx_count < 25 {
|
||||
break Result::<_, Error>::Ok((spk_index, spk_txs));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
@@ -297,38 +277,314 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
break;
|
||||
}
|
||||
|
||||
for (txid, status) in handles.try_collect::<Vec<(Txid, TxStatus)>>().await? {
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
|
||||
last_index = Some(index);
|
||||
if !txs.is_empty() {
|
||||
last_active_index = Some(index);
|
||||
}
|
||||
for tx in txs {
|
||||
let _ = graph.insert_tx(tx.to_tx());
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: Amount::from_sat(prevout.value),
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||
let gap_limit_reached = if let Some(i) = last_active_index {
|
||||
last_index >= i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index + 1 >= stop_gap as u32
|
||||
};
|
||||
if gap_limit_reached {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for op in outpoints.into_iter() {
|
||||
if graph.get_tx(op.txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&op.txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&op.txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(op.txid, anchor);
|
||||
}
|
||||
}
|
||||
if let Some(last_active_index) = last_active_index {
|
||||
last_active_indexes.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _).await? {
|
||||
if let Some(txid) = op_status.txid {
|
||||
if graph.get_tx(txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
Ok((graph, last_active_indexes))
|
||||
}
|
||||
|
||||
async fn sync_for_index_and_graph(
|
||||
client: &esplora_client::AsyncClient,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let mut graph = full_scan_for_index_and_graph(
|
||||
client,
|
||||
[(
|
||||
(),
|
||||
misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk)),
|
||||
)]
|
||||
.into(),
|
||||
usize::MAX,
|
||||
parallel_requests,
|
||||
)
|
||||
.await
|
||||
.map(|(g, _)| g)?;
|
||||
|
||||
let mut txids = txids.into_iter();
|
||||
loop {
|
||||
let handles = txids
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.filter(|&txid| graph.get_tx(txid).is_none())
|
||||
.map(|txid| {
|
||||
let client = client.clone();
|
||||
async move { client.get_tx_status(&txid).await.map(|s| (txid, s)) }
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (txid, status) in handles.try_collect::<Vec<(Txid, TxStatus)>>().await? {
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for op in outpoints.into_iter() {
|
||||
if graph.get_tx(op.txid).is_none() {
|
||||
if let Some(tx) = client.get_tx(&op.txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = client.get_tx_status(&op.txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(op.txid, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(op_status) = client.get_output_status(&op.txid, op.vout as _).await? {
|
||||
if let Some(txid) = op_status.txid {
|
||||
if graph.get_tx(txid).is_none() {
|
||||
if let Some(tx) = client.get_tx(&txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = client.get_tx_status(&txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{collections::BTreeSet, time::Duration};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{hashes::Hash, Txid},
|
||||
local_chain::LocalChain,
|
||||
BlockId,
|
||||
};
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
use esplora_client::Builder;
|
||||
|
||||
use crate::async_ext::{chain_update, fetch_latest_blocks};
|
||||
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||
}};
|
||||
}
|
||||
|
||||
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
|
||||
#[tokio::test]
|
||||
pub async fn test_finalize_chain_update() -> anyhow::Result<()> {
|
||||
struct TestCase<'a> {
|
||||
name: &'a str,
|
||||
/// Initial blockchain height to start the env with.
|
||||
initial_env_height: u32,
|
||||
/// Initial checkpoint heights to start with.
|
||||
initial_cps: &'a [u32],
|
||||
/// The final blockchain height of the env.
|
||||
final_env_height: u32,
|
||||
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
|
||||
/// the blockhash from the env.
|
||||
anchors: &'a [(u32, Txid)],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "chain_extends",
|
||||
initial_env_height: 60,
|
||||
initial_cps: &[59, 60],
|
||||
final_env_height: 90,
|
||||
anchors: &[],
|
||||
},
|
||||
TestCase {
|
||||
name: "introduce_older_heights",
|
||||
initial_env_height: 50,
|
||||
initial_cps: &[10, 15],
|
||||
final_env_height: 50,
|
||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "introduce_older_heights_after_chain_extends",
|
||||
initial_env_height: 50,
|
||||
initial_cps: &[10, 15],
|
||||
final_env_height: 100,
|
||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("[{}] running test case: {}", i, t.name);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
|
||||
// set env to `initial_env_height`
|
||||
if let Some(to_mine) = t
|
||||
.initial_env_height
|
||||
.checked_sub(env.make_checkpoint_tip().height())
|
||||
{
|
||||
env.mine_blocks(to_mine as _, None)?;
|
||||
}
|
||||
while client.get_height().await? < t.initial_env_height {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// craft initial `local_chain`
|
||||
let local_chain = {
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
|
||||
// force `chain_update_blocking` to add all checkpoints in `t.initial_cps`
|
||||
let anchors = t
|
||||
.initial_cps
|
||||
.iter()
|
||||
.map(|&height| -> anyhow::Result<_> {
|
||||
Ok((
|
||||
BlockId {
|
||||
height,
|
||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||
},
|
||||
Txid::all_zeros(),
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<BTreeSet<_>>>()?;
|
||||
let update = chain_update(
|
||||
&client,
|
||||
&fetch_latest_blocks(&client).await?,
|
||||
&chain.tip(),
|
||||
&anchors,
|
||||
)
|
||||
.await?;
|
||||
chain.apply_update(update)?;
|
||||
chain
|
||||
};
|
||||
println!("local chain height: {}", local_chain.tip().height());
|
||||
|
||||
// extend env chain
|
||||
if let Some(to_mine) = t
|
||||
.final_env_height
|
||||
.checked_sub(env.make_checkpoint_tip().height())
|
||||
{
|
||||
env.mine_blocks(to_mine as _, None)?;
|
||||
}
|
||||
while client.get_height().await? < t.final_env_height {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// craft update
|
||||
let update = {
|
||||
let anchors = t
|
||||
.anchors
|
||||
.iter()
|
||||
.map(|&(height, txid)| -> anyhow::Result<_> {
|
||||
Ok((
|
||||
BlockId {
|
||||
height,
|
||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||
},
|
||||
txid,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
chain_update(
|
||||
&client,
|
||||
&fetch_latest_blocks(&client).await?,
|
||||
&local_chain.tip(),
|
||||
&anchors,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
// apply update
|
||||
let mut updated_local_chain = local_chain.clone();
|
||||
updated_local_chain.apply_update(update)?;
|
||||
println!(
|
||||
"updated local chain height: {}",
|
||||
updated_local_chain.tip().height()
|
||||
);
|
||||
|
||||
assert!(
|
||||
{
|
||||
let initial_heights = local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| cp.height())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let updated_heights = updated_local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| cp.height())
|
||||
.collect::<BTreeSet<_>>();
|
||||
updated_heights.is_superset(&initial_heights)
|
||||
},
|
||||
"heights from the initial chain must all be in the updated chain",
|
||||
);
|
||||
|
||||
assert!(
|
||||
{
|
||||
let exp_anchor_heights = t
|
||||
.anchors
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.chain(t.initial_cps.iter().copied())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let anchor_heights = updated_local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| cp.height())
|
||||
.collect::<BTreeSet<_>>();
|
||||
anchor_heights.is_superset(&exp_anchor_heights)
|
||||
},
|
||||
"anchor heights must all be in updated chain",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,13 @@
|
||||
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
|
||||
use bdk_esplora::EsploraAsyncExt;
|
||||
use electrsd::bitcoind::anyhow;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use esplora_client::{self, Builder};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||
use bdk_testenv::TestEnv;
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
@@ -52,15 +51,31 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = client
|
||||
.sync(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
let sync_update = {
|
||||
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
|
||||
client.sync(request, 1).await?
|
||||
};
|
||||
|
||||
assert!(
|
||||
{
|
||||
let update_cps = sync_update
|
||||
.chain_update
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let superset_cps = cp_tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
superset_cps.is_superset(&update_cps)
|
||||
},
|
||||
"update should not alter original checkpoint tip since we already started with all checkpoints",
|
||||
);
|
||||
|
||||
let graph_update = sync_update.graph_update;
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
@@ -121,8 +136,6 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
.enumerate()
|
||||
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||
.collect();
|
||||
let mut keychains = BTreeMap::new();
|
||||
keychains.insert(0, spks);
|
||||
|
||||
// Then receive coins on the 4th address.
|
||||
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -140,14 +153,33 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
// A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||
// will.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 3, 1).await?
|
||||
};
|
||||
assert!(full_scan_update.graph_update.full_txs().next().is_none());
|
||||
assert!(full_scan_update.last_active_indices.is_empty());
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 4, 1).await?
|
||||
};
|
||||
assert_eq!(
|
||||
full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.next()
|
||||
.unwrap()
|
||||
.txid,
|
||||
txid_4th_addr
|
||||
);
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
|
||||
// Now receive a coin on the last address.
|
||||
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -167,16 +199,32 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
|
||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1).await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 5, 1).await?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1).await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 6, 1).await?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
assert_eq!(active_indices[&0], 9);
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
use bdk_chain::local_chain::LocalChain;
|
||||
use bdk_chain::BlockId;
|
||||
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
|
||||
use bdk_esplora::EsploraExt;
|
||||
use electrsd::bitcoind::anyhow;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use esplora_client::{self, Builder};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||
use bdk_testenv::TestEnv;
|
||||
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
}};
|
||||
}
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
@@ -68,13 +51,31 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = client.sync(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
1,
|
||||
)?;
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
let sync_update = {
|
||||
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
|
||||
client.sync(request, 1)?
|
||||
};
|
||||
|
||||
assert!(
|
||||
{
|
||||
let update_cps = sync_update
|
||||
.chain_update
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let superset_cps = cp_tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
superset_cps.is_superset(&update_cps)
|
||||
},
|
||||
"update should not alter original checkpoint tip since we already started with all checkpoints",
|
||||
);
|
||||
|
||||
let graph_update = sync_update.graph_update;
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
@@ -136,8 +137,6 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
.enumerate()
|
||||
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||
.collect();
|
||||
let mut keychains = BTreeMap::new();
|
||||
keychains.insert(0, spks);
|
||||
|
||||
// Then receive coins on the 4th address.
|
||||
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -155,14 +154,33 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||
// will.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 3, 1)?
|
||||
};
|
||||
assert!(full_scan_update.graph_update.full_txs().next().is_none());
|
||||
assert!(full_scan_update.last_active_indices.is_empty());
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 4, 1)?
|
||||
};
|
||||
assert_eq!(
|
||||
full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.next()
|
||||
.unwrap()
|
||||
.txid,
|
||||
txid_4th_addr
|
||||
);
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
|
||||
// Now receive a coin on the last address.
|
||||
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -182,194 +200,32 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
|
||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 5, 1)?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 6, 1)?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
assert_eq!(active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_local_chain() -> anyhow::Result<()> {
|
||||
const TIP_HEIGHT: u32 = 50;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let blocks = {
|
||||
let bitcoind_client = &env.bitcoind.client;
|
||||
assert_eq!(bitcoind_client.get_block_count()?, 1);
|
||||
[
|
||||
(0, bitcoind_client.get_block_hash(0)?),
|
||||
(1, bitcoind_client.get_block_hash(1)?),
|
||||
]
|
||||
.into_iter()
|
||||
.chain((2..).zip(env.mine_blocks((TIP_HEIGHT - 1) as usize, None)?))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
};
|
||||
// so new blocks can be seen by Electrs
|
||||
let env = env.reset_electrsd()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
request_heights: &'static [u32],
|
||||
exp_update_heights: &'static [u32],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "request_later_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (21, blocks[&21])],
|
||||
request_heights: &[22, 25, 28],
|
||||
exp_update_heights: &[21, 22, 25, 28],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (5, blocks[&5])],
|
||||
request_heights: &[4],
|
||||
exp_update_heights: &[4, 5],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks_2",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (10, blocks[&10])],
|
||||
request_heights: &[4, 6],
|
||||
exp_update_heights: &[4, 6, 10],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_and_prev_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (7, blocks[&7]), (11, blocks[&11])],
|
||||
request_heights: &[8, 9, 15],
|
||||
exp_update_heights: &[8, 9, 11, 15],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_tip_only",
|
||||
chain: local_chain![(0, blocks[&0]), (5, blocks[&5]), (49, blocks[&49])],
|
||||
request_heights: &[TIP_HEIGHT],
|
||||
exp_update_heights: &[49],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing",
|
||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, blocks[&23])],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing_during_reorg",
|
||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, h!("23"))],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[13, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing_during_reorg_2",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(21, blocks[&21]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[21, 22, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks_during_reorg",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(21, blocks[&21]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[17, 20],
|
||||
exp_update_heights: &[17, 20, 21, 22, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_blocks_during_reorg",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(9, blocks[&9]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[25, 27],
|
||||
exp_update_heights: &[9, 22, 23, 25, 27],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_blocks_during_reorg_2",
|
||||
chain: local_chain![(0, blocks[&0]), (9, h!("9"))],
|
||||
request_heights: &[10],
|
||||
exp_update_heights: &[0, 9, 10],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_and_prev_blocks_during_reorg",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (9, h!("9"))],
|
||||
request_heights: &[8, 11],
|
||||
exp_update_heights: &[1, 8, 9, 11],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
let mut chain = t.chain;
|
||||
|
||||
let update = client
|
||||
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
|
||||
.map_err(|err| {
|
||||
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
|
||||
})?;
|
||||
|
||||
let update_blocks = update
|
||||
.tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let exp_update_blocks = t
|
||||
.exp_update_heights
|
||||
.iter()
|
||||
.map(|&height| {
|
||||
let hash = blocks[&height];
|
||||
BlockId { height, hash }
|
||||
})
|
||||
.chain(
|
||||
// Electrs Esplora `get_block` call fetches 10 blocks which is included in the
|
||||
// update
|
||||
blocks
|
||||
.range(TIP_HEIGHT - 9..)
|
||||
.map(|(&height, &hash)| BlockId { height, hash }),
|
||||
)
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
assert_eq!(
|
||||
update_blocks, exp_update_blocks,
|
||||
"[{}:{}] unexpected update",
|
||||
i, t.name
|
||||
);
|
||||
|
||||
let _ = chain
|
||||
.apply_update(update)
|
||||
.unwrap_or_else(|err| panic!("[{}:{}] update failed to apply: {}", i, t.name, err));
|
||||
|
||||
// all requested heights must exist in the final chain
|
||||
for height in t.request_heights {
|
||||
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
||||
assert_eq!(
|
||||
chain.get(*height).map(|cp| cp.hash()),
|
||||
Some(*exp_blockhash),
|
||||
"[{}:{}] block {}:{} must exist in final chain",
|
||||
i,
|
||||
t.name,
|
||||
height,
|
||||
exp_blockhash
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.9.0"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -11,7 +11,9 @@ authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.12.0", features = [ "serde", "miniscript" ] }
|
||||
anyhow = { version = "1", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_persist = { path = "../persist", version = "0.2.0"}
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# BDK File Store
|
||||
|
||||
This is a simple append-only flat file implementation of
|
||||
[`Persist`](`bdk_chain::Persist`).
|
||||
[`PersistBackend`](bdk_persist::PersistBackend).
|
||||
|
||||
The main structure is [`Store`](`crate::Store`), which can be used with [`bdk`]'s
|
||||
The main structure is [`Store`](crate::Store), which can be used with [`bdk`]'s
|
||||
`Wallet` to persist wallet data into a flat file.
|
||||
|
||||
[`bdk`]: https://docs.rs/bdk/latest
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
[`bdk_persist`]: https://docs.rs/bdk_persist/latest
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use crate::{bincode_options, EntryIter, FileError, IterError};
|
||||
use anyhow::anyhow;
|
||||
use bdk_chain::Append;
|
||||
use bdk_persist::PersistBackend;
|
||||
use bincode::Options;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
fmt::{self, Debug},
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, Read, Seek, Write},
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use bdk_chain::{Append, PersistBackend};
|
||||
use bincode::Options;
|
||||
|
||||
use crate::{bincode_options, EntryIter, FileError, IterError};
|
||||
|
||||
/// Persists an append-only list of changesets (`C`) to a single file.
|
||||
///
|
||||
/// The changesets are the results of altering a tracker implementation (`T`).
|
||||
#[derive(Debug)]
|
||||
pub struct Store<C> {
|
||||
pub struct Store<C>
|
||||
where
|
||||
C: Sync + Send,
|
||||
{
|
||||
magic_len: usize,
|
||||
db_file: File,
|
||||
marker: PhantomData<C>,
|
||||
@@ -23,24 +24,30 @@ pub struct Store<C> {
|
||||
|
||||
impl<C> PersistBackend<C> for Store<C>
|
||||
where
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
C: Append
|
||||
+ serde::Serialize
|
||||
+ serde::de::DeserializeOwned
|
||||
+ core::marker::Send
|
||||
+ core::marker::Sync,
|
||||
{
|
||||
type WriteError = std::io::Error;
|
||||
|
||||
type LoadError = IterError;
|
||||
|
||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError> {
|
||||
fn write_changes(&mut self, changeset: &C) -> anyhow::Result<()> {
|
||||
self.append_changeset(changeset)
|
||||
.map_err(|e| anyhow!(e).context("failed to write changes to persistence backend"))
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
self.aggregate_changesets().map_err(|e| e.iter_error)
|
||||
fn load_from_persistence(&mut self) -> anyhow::Result<Option<C>> {
|
||||
self.aggregate_changesets()
|
||||
.map_err(|e| anyhow!(e.iter_error).context("error loading from persistence backend"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Store<C>
|
||||
where
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
C: Append
|
||||
+ serde::Serialize
|
||||
+ serde::de::DeserializeOwned
|
||||
+ core::marker::Send
|
||||
+ core::marker::Sync,
|
||||
{
|
||||
/// Create a new [`Store`] file in write-only mode; error if the file exists.
|
||||
///
|
||||
@@ -144,7 +151,7 @@ where
|
||||
///
|
||||
/// You should usually check the error. In many applications, it may make sense to do a full
|
||||
/// wallet scan with a stop-gap after getting an error, since it is likely that one of the
|
||||
/// changesets it was unable to read changed the derivation indices of the tracker.
|
||||
/// changesets was unable to read changes of the derivation indices of a keychain.
|
||||
///
|
||||
/// **WARNING**: This method changes the write position of the underlying file. The next
|
||||
/// changeset will be written over the erroring entry (or the end of the file if none existed).
|
||||
@@ -182,7 +189,7 @@ where
|
||||
bincode_options()
|
||||
.serialize_into(&mut self.db_file, changeset)
|
||||
.map_err(|e| match *e {
|
||||
bincode::ErrorKind::Io(inner) => inner,
|
||||
bincode::ErrorKind::Io(error) => error,
|
||||
unexpected_err => panic!("unexpected bincode error: {}", unexpected_err),
|
||||
})?;
|
||||
|
||||
@@ -212,7 +219,7 @@ impl<C> std::fmt::Display for AggregateChangesetsError<C> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
|
||||
impl<C: fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
@@ -232,9 +239,6 @@ mod test {
|
||||
|
||||
type TestChangeSet = BTreeSet<String>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestTracker;
|
||||
|
||||
/// Check behavior of [`Store::create_new`] and [`Store::open`].
|
||||
#[test]
|
||||
fn construct_store() {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//! # use bdk::bitcoin::Network;
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk_hwi::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
|
||||
22
crates/persist/Cargo.toml
Normal file
22
crates/persist/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "bdk_persist"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "0.2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_persist"
|
||||
description = "Types that define data persistence of a BDK wallet"
|
||||
keywords = ["bitcoin", "wallet", "persistence", "database"]
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["bdk_chain/std"]
|
||||
|
||||
|
||||
3
crates/persist/README.md
Normal file
3
crates/persist/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# BDK Persist
|
||||
|
||||
This crate is home to the [`PersistBackend`](crate::PersistBackend) trait which defines the behavior of a database to perform the task of persisting changes made to BDK data structures. The [`Persist`](crate::Persist) type provides a convenient wrapper around a `PersistBackend` that allows staging changes before committing them.
|
||||
5
crates/persist/src/lib.rs
Normal file
5
crates/persist/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![no_std]
|
||||
#![warn(missing_docs)]
|
||||
mod persist;
|
||||
pub use persist::*;
|
||||
@@ -1,26 +1,33 @@
|
||||
use core::convert::Infallible;
|
||||
extern crate alloc;
|
||||
use alloc::boxed::Box;
|
||||
use bdk_chain::Append;
|
||||
use core::fmt;
|
||||
|
||||
use crate::Append;
|
||||
|
||||
/// `Persist` wraps a [`PersistBackend`] (`B`) to create a convenient staging area for changes (`C`)
|
||||
/// `Persist` wraps a [`PersistBackend`] to create a convenient staging area for changes (`C`)
|
||||
/// before they are persisted.
|
||||
///
|
||||
/// Not all changes to the in-memory representation needs to be written to disk right away, so
|
||||
/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used
|
||||
/// to write changes to disk.
|
||||
#[derive(Debug)]
|
||||
pub struct Persist<B, C> {
|
||||
backend: B,
|
||||
pub struct Persist<C> {
|
||||
backend: Box<dyn PersistBackend<C> + Send + Sync>,
|
||||
stage: C,
|
||||
}
|
||||
|
||||
impl<B, C> Persist<B, C>
|
||||
impl<C: fmt::Debug> fmt::Debug for Persist<C> {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||
write!(fmt, "{:?}", self.stage)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Persist<C>
|
||||
where
|
||||
B: PersistBackend<C>,
|
||||
C: Default + Append,
|
||||
{
|
||||
/// Create a new [`Persist`] from [`PersistBackend`].
|
||||
pub fn new(backend: B) -> Self {
|
||||
pub fn new(backend: impl PersistBackend<C> + Send + Sync + 'static) -> Self {
|
||||
let backend = Box::new(backend);
|
||||
Self {
|
||||
backend,
|
||||
stage: Default::default(),
|
||||
@@ -46,7 +53,7 @@ where
|
||||
/// # Error
|
||||
///
|
||||
/// Returns a backend-defined error if this fails.
|
||||
pub fn commit(&mut self) -> Result<Option<C>, B::WriteError> {
|
||||
pub fn commit(&mut self) -> anyhow::Result<Option<C>> {
|
||||
if self.stage.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -63,7 +70,7 @@ where
|
||||
///
|
||||
/// [`stage`]: Self::stage
|
||||
/// [`commit`]: Self::commit
|
||||
pub fn stage_and_commit(&mut self, changeset: C) -> Result<Option<C>, B::WriteError> {
|
||||
pub fn stage_and_commit(&mut self, changeset: C) -> anyhow::Result<Option<C>> {
|
||||
self.stage(changeset);
|
||||
self.commit()
|
||||
}
|
||||
@@ -74,12 +81,6 @@ where
|
||||
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
|
||||
/// that are to be persisted, or retrieved from persistence.
|
||||
pub trait PersistBackend<C> {
|
||||
/// The error the backend returns when it fails to write.
|
||||
type WriteError: core::fmt::Debug;
|
||||
|
||||
/// The error the backend returns when it fails to load changesets `C`.
|
||||
type LoadError: core::fmt::Debug;
|
||||
|
||||
/// Writes a changeset to the persistence backend.
|
||||
///
|
||||
/// It is up to the backend what it does with this. It could store every changeset in a list or
|
||||
@@ -88,22 +89,18 @@ pub trait PersistBackend<C> {
|
||||
/// changesets had been applied sequentially.
|
||||
///
|
||||
/// [`load_from_persistence`]: Self::load_from_persistence
|
||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
|
||||
fn write_changes(&mut self, changeset: &C) -> anyhow::Result<()>;
|
||||
|
||||
/// Return the aggregate changeset `C` from persistence.
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
|
||||
fn load_from_persistence(&mut self) -> anyhow::Result<Option<C>>;
|
||||
}
|
||||
|
||||
impl<C> PersistBackend<C> for () {
|
||||
type WriteError = Infallible;
|
||||
|
||||
type LoadError = Infallible;
|
||||
|
||||
fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
|
||||
fn write_changes(&mut self, _changeset: &C) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
fn load_from_persistence(&mut self) -> anyhow::Result<Option<C>> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_testenv"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -13,10 +13,8 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bitcoincore-rpc = { version = "0.18" }
|
||||
bdk_chain = { path = "../chain", version = "0.12", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14", default-features = false }
|
||||
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
anyhow = { version = "1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
use bdk_chain::bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
|
||||
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
|
||||
use bdk_chain::{
|
||||
bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
|
||||
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
|
||||
},
|
||||
local_chain::CheckPoint,
|
||||
BlockId,
|
||||
};
|
||||
use bitcoincore_rpc::{
|
||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
RpcApi,
|
||||
};
|
||||
pub use electrsd;
|
||||
pub use electrsd::bitcoind;
|
||||
pub use electrsd::bitcoind::anyhow;
|
||||
pub use electrsd::bitcoind::bitcoincore_rpc;
|
||||
pub use electrsd::electrum_client;
|
||||
use electrsd::electrum_client::ElectrumApi;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -234,13 +243,30 @@ impl TestEnv {
|
||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
||||
Ok(txid)
|
||||
}
|
||||
|
||||
/// Create a checkpoint linked list of all the blocks in the chain.
|
||||
pub fn make_checkpoint_tip(&self) -> CheckPoint {
|
||||
CheckPoint::from_block_ids((0_u32..).map_while(|height| {
|
||||
self.bitcoind
|
||||
.client
|
||||
.get_block_hash(height as u64)
|
||||
.ok()
|
||||
.map(|hash| BlockId { height, hash })
|
||||
}))
|
||||
.expect("must craft tip")
|
||||
}
|
||||
|
||||
/// Get the genesis hash of the blockchain.
|
||||
pub fn genesis_hash(&self) -> anyhow::Result<BlockHash> {
|
||||
let hash = self.bitcoind.client.get_block_hash(0)?;
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::TestEnv;
|
||||
use anyhow::Result;
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
use electrsd::bitcoind::{anyhow::Result, bitcoincore_rpc::RpcApi};
|
||||
|
||||
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
|
||||
#[test]
|
||||
|
||||
@@ -188,10 +188,7 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut db = db.lock().unwrap();
|
||||
|
||||
let chain_changeset = chain
|
||||
.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})
|
||||
.apply_update(emission.checkpoint)
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
|
||||
db.stage((chain_changeset, graph_changeset));
|
||||
@@ -215,7 +212,7 @@ fn main() -> anyhow::Result<()> {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
graph.index.outpoints(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
@@ -301,12 +298,8 @@ fn main() -> anyhow::Result<()> {
|
||||
let changeset = match emission {
|
||||
Emission::Block(block_emission) => {
|
||||
let height = block_emission.block_height();
|
||||
let chain_update = local_chain::Update {
|
||||
tip: block_emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
};
|
||||
let chain_changeset = chain
|
||||
.apply_update(chain_update)
|
||||
.apply_update(block_emission.checkpoint)
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset =
|
||||
graph.apply_block_relevant(&block_emission.block, height);
|
||||
@@ -343,7 +336,7 @@ fn main() -> anyhow::Result<()> {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
graph.index.outpoints(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]}
|
||||
bdk_persist = { path = "../../crates/persist" }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_tmp_plan = { path = "../../nursery/tmp_plan" }
|
||||
bdk_coin_select = { path = "../../nursery/coin_select" }
|
||||
|
||||
@@ -19,9 +19,10 @@ use bdk_chain::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
Descriptor, DescriptorPublicKey,
|
||||
},
|
||||
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend,
|
||||
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut,
|
||||
};
|
||||
pub use bdk_file_store;
|
||||
use bdk_persist::{Persist, PersistBackend};
|
||||
pub use clap;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
@@ -31,7 +32,6 @@ pub type KeychainChangeSet<A> = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>>,
|
||||
);
|
||||
pub type Database<C> = Persist<Store<C>, C>;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
@@ -249,14 +249,20 @@ where
|
||||
script_pubkey: address.script_pubkey(),
|
||||
}];
|
||||
|
||||
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
|
||||
let internal_keychain = if graph
|
||||
.index
|
||||
.keychains()
|
||||
.any(|(k, _)| *k == Keychain::Internal)
|
||||
{
|
||||
Keychain::Internal
|
||||
} else {
|
||||
Keychain::External
|
||||
};
|
||||
|
||||
let ((change_index, change_script), change_changeset) =
|
||||
graph.index.next_unused_spk(&internal_keychain);
|
||||
let ((change_index, change_script), change_changeset) = graph
|
||||
.index
|
||||
.next_unused_spk(&internal_keychain)
|
||||
.expect("Must exist");
|
||||
changeset.append(change_changeset);
|
||||
|
||||
// Clone to drop the immutable reference.
|
||||
@@ -266,8 +272,9 @@ where
|
||||
&graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.find(|(k, _)| *k == &internal_keychain)
|
||||
.expect("must exist")
|
||||
.1
|
||||
.at_derivation_index(change_index)
|
||||
.expect("change_index can't be hardened"),
|
||||
&assets,
|
||||
@@ -284,8 +291,9 @@ where
|
||||
min_drain_value: graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.find(|(k, _)| *k == &internal_keychain)
|
||||
.expect("must exist")
|
||||
.1
|
||||
.dust_value(),
|
||||
..CoinSelectorOpt::fund_outputs(
|
||||
&outputs,
|
||||
@@ -416,7 +424,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
|
||||
assets: &bdk_tmp_plan::Assets<K>,
|
||||
) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
let outpoints = graph.index.outpoints();
|
||||
graph
|
||||
.graph()
|
||||
.try_filter_chain_unspents(chain, chain_tip, outpoints)
|
||||
@@ -428,8 +436,9 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.find(|(keychain, _)| *keychain == &k)
|
||||
.expect("keychain must exist")
|
||||
.1
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
@@ -440,7 +449,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
|
||||
|
||||
pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainOracle, C>(
|
||||
graph: &Mutex<KeychainTxGraph<A>>,
|
||||
db: &Mutex<Database<C>>,
|
||||
db: &Mutex<Persist<C>>,
|
||||
chain: &Mutex<O>,
|
||||
keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
network: Network,
|
||||
@@ -465,7 +474,8 @@ where
|
||||
_ => unreachable!("only these two variants exist in match arm"),
|
||||
};
|
||||
|
||||
let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
|
||||
let ((spk_i, spk), index_changeset) =
|
||||
spk_chooser(index, &Keychain::External).expect("Must exist");
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.stage_and_commit(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
@@ -506,18 +516,18 @@ where
|
||||
let chain = &*chain.lock().unwrap();
|
||||
fn print_balances<'a>(
|
||||
title_str: &'a str,
|
||||
items: impl IntoIterator<Item = (&'a str, u64)>,
|
||||
items: impl IntoIterator<Item = (&'a str, Amount)>,
|
||||
) {
|
||||
println!("{}:", title_str);
|
||||
for (name, amount) in items.into_iter() {
|
||||
println!(" {:<10} {:>12} sats", name, amount)
|
||||
println!(" {:<10} {:>12} sats", name, amount.to_sat())
|
||||
}
|
||||
}
|
||||
|
||||
let balance = graph.graph().try_balance(
|
||||
chain,
|
||||
chain.get_chain_tip()?,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
graph.index.outpoints(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)?;
|
||||
|
||||
@@ -547,7 +557,7 @@ where
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
let outpoints = graph.index.outpoints();
|
||||
|
||||
match txout_cmd {
|
||||
TxOutCmd::List {
|
||||
@@ -667,7 +677,7 @@ pub struct Init<CS: clap::Subcommand, S: clap::Args, C> {
|
||||
/// Keychain-txout index.
|
||||
pub index: KeychainTxOutIndex<Keychain>,
|
||||
/// Persistence backend.
|
||||
pub db: Mutex<Database<C>>,
|
||||
pub db: Mutex<Persist<C>>,
|
||||
/// Initial changeset.
|
||||
pub init_changeset: C,
|
||||
}
|
||||
@@ -679,7 +689,13 @@ pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
|
||||
db_default_path: &str,
|
||||
) -> anyhow::Result<Init<CS, S, C>>
|
||||
where
|
||||
C: Default + Append + Serialize + DeserializeOwned,
|
||||
C: Default
|
||||
+ Append
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ core::marker::Send
|
||||
+ core::marker::Sync
|
||||
+ 'static,
|
||||
{
|
||||
if std::env::var("BDK_DB_PATH").is_err() {
|
||||
std::env::set_var("BDK_DB_PATH", db_default_path);
|
||||
@@ -689,9 +705,11 @@ where
|
||||
|
||||
let mut index = KeychainTxOutIndex::<Keychain>::default();
|
||||
|
||||
// TODO: descriptors are already stored in the db, so we shouldn't re-insert
|
||||
// them in the index here. However, the keymap is not stored in the database.
|
||||
let (descriptor, mut keymap) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
|
||||
index.add_keychain(Keychain::External, descriptor);
|
||||
let _ = index.insert_descriptor(Keychain::External, descriptor);
|
||||
|
||||
if let Some((internal_descriptor, internal_keymap)) = args
|
||||
.change_descriptor
|
||||
@@ -700,7 +718,7 @@ where
|
||||
.transpose()?
|
||||
{
|
||||
keymap.extend(internal_keymap);
|
||||
index.add_keychain(Keychain::Internal, internal_descriptor);
|
||||
let _ = index.insert_descriptor(Keychain::Internal, internal_descriptor);
|
||||
}
|
||||
|
||||
let mut db_backend = match Store::<C>::open_or_create_new(db_magic, &args.db_path) {
|
||||
@@ -715,7 +733,7 @@ where
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db: Mutex::new(Database::new(db_backend)),
|
||||
db: Mutex::new(Persist::new(db_backend)),
|
||||
init_changeset,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::{self, Write},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, OutPoint, Txid},
|
||||
bitcoin::{constants::genesis_block, Address, Network, Txid},
|
||||
collections::BTreeSet,
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
Append, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bdk_electrum::{
|
||||
electrum_client::{self, Client, ElectrumApi},
|
||||
ElectrumExt, ElectrumUpdate,
|
||||
ElectrumExt,
|
||||
};
|
||||
use example_cli::{
|
||||
anyhow::{self, Context},
|
||||
@@ -147,42 +148,56 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
let client = electrum_cmd.electrum_args().client(args.network)?;
|
||||
|
||||
let response = match electrum_cmd.clone() {
|
||||
let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
|
||||
ElectrumCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
let (keychain_spks, tip) = {
|
||||
let request = {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
|
||||
let keychain_spks = graph
|
||||
.index
|
||||
.all_unbounded_spk_iters()
|
||||
.into_iter()
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
let spk_iter = iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
FullScanRequest::from_chain_tip(chain.tip())
|
||||
.cache_graph_txs(graph.graph())
|
||||
.set_spks_for_keychain(
|
||||
Keychain::External,
|
||||
graph
|
||||
.index
|
||||
.unbounded_spk_iter(&Keychain::External)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.set_spks_for_keychain(
|
||||
Keychain::Internal,
|
||||
graph
|
||||
.index
|
||||
.unbounded_spk_iter(&Keychain::Internal)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::new();
|
||||
move |k, spk_i, _| {
|
||||
if once.insert(k) {
|
||||
eprint!("\nScanning {}: {} ", k, spk_i);
|
||||
} else {
|
||||
eprint!("{} ", spk_i);
|
||||
}
|
||||
|
||||
eprint!("{} ", i);
|
||||
let _ = io::stdout().flush();
|
||||
});
|
||||
(keychain, spk_iter)
|
||||
io::stdout().flush().expect("must flush");
|
||||
}
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let tip = chain.tip();
|
||||
(keychain_spks, tip)
|
||||
};
|
||||
|
||||
client
|
||||
.full_scan(tip, keychain_spks, stop_gap, scan_options.batch_size)
|
||||
let res = client
|
||||
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
|
||||
.context("scanning the blockchain")?
|
||||
.with_confirmation_height_anchor();
|
||||
(
|
||||
res.chain_update,
|
||||
res.graph_update,
|
||||
Some(res.last_active_indices),
|
||||
)
|
||||
}
|
||||
ElectrumCommands::Sync {
|
||||
mut unused_spks,
|
||||
@@ -195,7 +210,6 @@ fn main() -> anyhow::Result<()> {
|
||||
// Get a short lock on the tracker to get the spks we're interested in
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
||||
unused_spks = true;
|
||||
@@ -205,18 +219,20 @@ fn main() -> anyhow::Result<()> {
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> =
|
||||
Box::new(core::iter::empty());
|
||||
let chain_tip = chain.tip();
|
||||
let mut request =
|
||||
SyncRequest::from_chain_tip(chain_tip.clone()).cache_graph_txs(graph.graph());
|
||||
|
||||
if all_spks {
|
||||
let all_spks = graph
|
||||
.index
|
||||
.revealed_spks()
|
||||
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||
.revealed_spks(..)
|
||||
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprintln!("scanning {}:{}", k, i);
|
||||
request = request.chain_spks(all_spks.into_iter().map(|(k, spk_i, spk)| {
|
||||
eprint!("Scanning {}: {}", k, spk_i);
|
||||
spk
|
||||
})));
|
||||
}));
|
||||
}
|
||||
if unused_spks {
|
||||
let unused_spks = graph
|
||||
@@ -224,82 +240,88 @@ fn main() -> anyhow::Result<()> {
|
||||
.unused_spks()
|
||||
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprintln!(
|
||||
"Checking if address {} {}:{} has been used",
|
||||
Address::from_script(&spk, args.network).unwrap(),
|
||||
k,
|
||||
i,
|
||||
);
|
||||
spk
|
||||
})));
|
||||
request =
|
||||
request.chain_spks(unused_spks.into_iter().map(move |(k, spk_i, spk)| {
|
||||
eprint!(
|
||||
"Checking if address {} {}:{} has been used",
|
||||
Address::from_script(&spk, args.network).unwrap(),
|
||||
k,
|
||||
spk_i,
|
||||
);
|
||||
spk
|
||||
}));
|
||||
}
|
||||
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
|
||||
if utxos {
|
||||
let init_outpoints = graph.index.outpoints().iter().cloned();
|
||||
let init_outpoints = graph.index.outpoints();
|
||||
|
||||
let utxos = graph
|
||||
.graph()
|
||||
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
|
||||
.filter_chain_unspents(&*chain, chain_tip.block_id(), init_outpoints)
|
||||
.map(|(_, utxo)| utxo)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
outpoints = Box::new(
|
||||
utxos
|
||||
.into_iter()
|
||||
.inspect(|utxo| {
|
||||
eprintln!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
})
|
||||
.map(|utxo| utxo.outpoint),
|
||||
);
|
||||
request = request.chain_outpoints(utxos.into_iter().map(|utxo| {
|
||||
eprint!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
utxo.outpoint
|
||||
}));
|
||||
};
|
||||
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
|
||||
if unconfirmed {
|
||||
let unconfirmed_txids = graph
|
||||
.graph()
|
||||
.list_chain_txs(&*chain, chain_tip)
|
||||
.list_chain_txs(&*chain, chain_tip.block_id())
|
||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||
.collect::<Vec<Txid>>();
|
||||
|
||||
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprintln!("Checking if {} is confirmed yet", txid);
|
||||
}));
|
||||
request = request.chain_txids(
|
||||
unconfirmed_txids
|
||||
.into_iter()
|
||||
.inspect(|txid| eprint!("Checking if {} is confirmed yet", txid)),
|
||||
);
|
||||
}
|
||||
|
||||
let tip = chain.tip();
|
||||
let total_spks = request.spks.len();
|
||||
let total_txids = request.txids.len();
|
||||
let total_ops = request.outpoints.len();
|
||||
request = request
|
||||
.inspect_spks({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32)
|
||||
}
|
||||
})
|
||||
.inspect_txids({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32)
|
||||
}
|
||||
})
|
||||
.inspect_outpoints({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32)
|
||||
}
|
||||
});
|
||||
|
||||
let res = client
|
||||
.sync(request, scan_options.batch_size, false)
|
||||
.context("scanning the blockchain")?
|
||||
.with_confirmation_height_anchor();
|
||||
|
||||
// drop lock on graph and chain
|
||||
drop((graph, chain));
|
||||
|
||||
let electrum_update = client
|
||||
.sync(tip, spks, txids, outpoints, scan_options.batch_size)
|
||||
.context("scanning the blockchain")?;
|
||||
(electrum_update, BTreeMap::new())
|
||||
(res.chain_update, res.graph_update, None)
|
||||
}
|
||||
};
|
||||
|
||||
let (
|
||||
ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
},
|
||||
keychain_update,
|
||||
) = response;
|
||||
|
||||
let missing_txids = {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
relevant_txids.missing_full_txs(graph.graph())
|
||||
};
|
||||
|
||||
let mut graph_update = relevant_txids.into_tx_graph(&client, missing_txids)?;
|
||||
let now = std::time::UNIX_EPOCH
|
||||
.elapsed()
|
||||
.expect("must get time")
|
||||
@@ -310,21 +332,17 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut chain = chain.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
|
||||
let chain = chain.apply_update(chain_update)?;
|
||||
let chain_changeset = chain.apply_update(chain_update)?;
|
||||
|
||||
let indexed_tx_graph = {
|
||||
let mut changeset =
|
||||
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
||||
let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update);
|
||||
changeset.append(indexed_tx_graph::ChangeSet {
|
||||
indexer,
|
||||
..Default::default()
|
||||
});
|
||||
changeset.append(graph.apply_update(graph_update));
|
||||
changeset
|
||||
};
|
||||
let mut indexed_tx_graph_changeset =
|
||||
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
||||
if let Some(keychain_update) = keychain_update {
|
||||
let (_, keychain_changeset) = graph.index.reveal_to_target_multi(&keychain_update);
|
||||
indexed_tx_graph_changeset.append(keychain_changeset.into());
|
||||
}
|
||||
indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
|
||||
|
||||
(chain, indexed_tx_graph)
|
||||
(chain_changeset, indexed_tx_graph_changeset)
|
||||
};
|
||||
|
||||
let mut db = db.lock().unwrap();
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::BTreeSet,
|
||||
io::{self, Write},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
|
||||
bitcoin::{constants::genesis_block, Address, Network, Txid},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
Append, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
|
||||
@@ -60,6 +61,7 @@ enum EsploraCommands {
|
||||
esplora_args: EsploraArgs,
|
||||
},
|
||||
}
|
||||
|
||||
impl EsploraCommands {
|
||||
fn esplora_args(&self) -> EsploraArgs {
|
||||
match self {
|
||||
@@ -149,63 +151,66 @@ fn main() -> anyhow::Result<()> {
|
||||
};
|
||||
|
||||
let client = esplora_cmd.esplora_args().client(args.network)?;
|
||||
// Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing.
|
||||
// Prepare the `IndexedTxGraph` and `LocalChain` updates based on whether we are scanning or
|
||||
// syncing.
|
||||
//
|
||||
// Scanning: We are iterating through spks of all keychains and scanning for transactions for
|
||||
// each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
|
||||
// number of consecutive spks have no transaction history. A Scan is done in situations of
|
||||
// wallet restoration. It is a special case. Applications should use "sync" style updates
|
||||
// after an initial scan.
|
||||
//
|
||||
// Syncing: We only check for specified spks, utxos and txids to update their confirmation
|
||||
// status or fetch missing transactions.
|
||||
let indexed_tx_graph_changeset = match &esplora_cmd {
|
||||
let (local_chain_changeset, indexed_tx_graph_changeset) = match &esplora_cmd {
|
||||
EsploraCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
let keychain_spks = graph
|
||||
.lock()
|
||||
.expect("mutex must not be poisoned")
|
||||
.index
|
||||
.all_unbounded_spk_iters()
|
||||
.into_iter()
|
||||
// This `map` is purely for logging.
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
let spk_iter = iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
let request = {
|
||||
let chain_tip = chain.lock().expect("mutex must not be poisoned").tip();
|
||||
let indexed_graph = &*graph.lock().expect("mutex must not be poisoned");
|
||||
FullScanRequest::from_keychain_txout_index(chain_tip, &indexed_graph.index)
|
||||
.inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::<Keychain>::new();
|
||||
move |keychain, spk_i, _| {
|
||||
if once.insert(keychain) {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
}
|
||||
eprint!("{} ", spk_i);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
}
|
||||
eprint!("{} ", i);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
});
|
||||
(keychain, spk_iter)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
})
|
||||
};
|
||||
|
||||
// The client scans keychain spks for transaction histories, stopping after `stop_gap`
|
||||
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
|
||||
// represents the last active spk derivation indices of keychains
|
||||
// (`keychain_indices_update`).
|
||||
let (mut graph_update, last_active_indices) = client
|
||||
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
|
||||
let mut update = client
|
||||
.full_scan(request, *stop_gap, scan_options.parallel_requests)
|
||||
.context("scanning for transactions")?;
|
||||
|
||||
// We want to keep track of the latest time a transaction was seen unconfirmed.
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
let mut graph = graph.lock().expect("mutex must not be poisoned");
|
||||
let mut chain = chain.lock().expect("mutex must not be poisoned");
|
||||
// Because we did a stop gap based scan we are likely to have some updates to our
|
||||
// deriviation indices. Usually before a scan you are on a fresh wallet with no
|
||||
// addresses derived so we need to derive up to last active addresses the scan found
|
||||
// before adding the transactions.
|
||||
let (_, index_changeset) = graph.index.reveal_to_target_multi(&last_active_indices);
|
||||
let mut indexed_tx_graph_changeset = graph.apply_update(graph_update);
|
||||
indexed_tx_graph_changeset.append(index_changeset.into());
|
||||
indexed_tx_graph_changeset
|
||||
(chain.apply_update(update.chain_update)?, {
|
||||
let (_, index_changeset) = graph
|
||||
.index
|
||||
.reveal_to_target_multi(&update.last_active_indices);
|
||||
let mut indexed_tx_graph_changeset = graph.apply_update(update.graph_update);
|
||||
indexed_tx_graph_changeset.append(index_changeset.into());
|
||||
indexed_tx_graph_changeset
|
||||
})
|
||||
}
|
||||
EsploraCommands::Sync {
|
||||
mut unused_spks,
|
||||
@@ -226,30 +231,28 @@ fn main() -> anyhow::Result<()> {
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
let local_tip = chain.lock().expect("mutex must not be poisoned").tip();
|
||||
// Spks, outpoints and txids we want updates on will be accumulated here.
|
||||
let mut spks: Box<dyn Iterator<Item = ScriptBuf>> = Box::new(core::iter::empty());
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
let mut request = SyncRequest::from_chain_tip(local_tip.clone());
|
||||
|
||||
// Get a short lock on the structures to get spks, utxos, and txs that we are interested
|
||||
// in.
|
||||
{
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
if *all_spks {
|
||||
let all_spks = graph
|
||||
.index
|
||||
.revealed_spks()
|
||||
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||
.revealed_spks(..)
|
||||
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprintln!("scanning {}:{}", k, i);
|
||||
request = request.chain_spks(all_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprint!("scanning {}:{}", k, i);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
spk
|
||||
})));
|
||||
}));
|
||||
}
|
||||
if unused_spks {
|
||||
let unused_spks = graph
|
||||
@@ -257,33 +260,34 @@ fn main() -> anyhow::Result<()> {
|
||||
.unused_spks()
|
||||
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprintln!(
|
||||
"Checking if address {} {}:{} has been used",
|
||||
Address::from_script(&spk, args.network).unwrap(),
|
||||
k,
|
||||
i,
|
||||
);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
spk
|
||||
})));
|
||||
request =
|
||||
request.chain_spks(unused_spks.into_iter().map(move |(k, i, spk)| {
|
||||
eprint!(
|
||||
"Checking if address {} {}:{} has been used",
|
||||
Address::from_script(&spk, args.network).unwrap(),
|
||||
k,
|
||||
i,
|
||||
);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
spk
|
||||
}));
|
||||
}
|
||||
if utxos {
|
||||
// We want to search for whether the UTXO is spent, and spent by which
|
||||
// transaction. We provide the outpoint of the UTXO to
|
||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||
let init_outpoints = graph.index.outpoints().iter().cloned();
|
||||
let init_outpoints = graph.index.outpoints();
|
||||
let utxos = graph
|
||||
.graph()
|
||||
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
|
||||
.filter_chain_unspents(&*chain, local_tip.block_id(), init_outpoints)
|
||||
.map(|(_, utxo)| utxo)
|
||||
.collect::<Vec<_>>();
|
||||
outpoints = Box::new(
|
||||
request = request.chain_outpoints(
|
||||
utxos
|
||||
.into_iter()
|
||||
.inspect(|utxo| {
|
||||
eprintln!(
|
||||
eprint!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
@@ -299,60 +303,61 @@ fn main() -> anyhow::Result<()> {
|
||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||
let unconfirmed_txids = graph
|
||||
.graph()
|
||||
.list_chain_txs(&*chain, chain_tip)
|
||||
.list_chain_txs(&*chain, local_tip.block_id())
|
||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||
.collect::<Vec<Txid>>();
|
||||
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprintln!("Checking if {} is confirmed yet", txid);
|
||||
request = request.chain_txids(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprint!("Checking if {} is confirmed yet", txid);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut graph_update =
|
||||
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
|
||||
let total_spks = request.spks.len();
|
||||
let total_txids = request.txids.len();
|
||||
let total_ops = request.outpoints.len();
|
||||
request = request
|
||||
.inspect_spks({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32)
|
||||
}
|
||||
})
|
||||
.inspect_txids({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32)
|
||||
}
|
||||
})
|
||||
.inspect_outpoints({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32)
|
||||
}
|
||||
});
|
||||
let mut update = client.sync(request, scan_options.parallel_requests)?;
|
||||
|
||||
// Update last seen unconfirmed
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
graph.lock().unwrap().apply_update(graph_update)
|
||||
(
|
||||
chain.lock().unwrap().apply_update(update.chain_update)?,
|
||||
graph.lock().unwrap().apply_update(update.graph_update),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
println!();
|
||||
|
||||
// Now that we're done updating the `IndexedTxGraph`, it's time to update the `LocalChain`! We
|
||||
// want the `LocalChain` to have data about all the anchors in the `TxGraph` - for this reason,
|
||||
// we want retrieve the blocks at the heights of the newly added anchors that are missing from
|
||||
// our view of the chain.
|
||||
let (missing_block_heights, tip) = {
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let missing_block_heights = indexed_tx_graph_changeset
|
||||
.graph
|
||||
.missing_heights_from(chain)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let tip = chain.tip();
|
||||
(missing_block_heights, tip)
|
||||
};
|
||||
|
||||
println!("prev tip: {}", tip.height());
|
||||
println!("missing block heights: {:?}", missing_block_heights);
|
||||
|
||||
// Here, we actually fetch the missing blocks and create a `local_chain::Update`.
|
||||
let chain_changeset = {
|
||||
let chain_update = client
|
||||
.update_local_chain(tip, missing_block_heights)
|
||||
.context("scanning for blocks")?;
|
||||
println!("new tip: {}", chain_update.tip.height());
|
||||
chain.lock().unwrap().apply_update(chain_update)?
|
||||
};
|
||||
|
||||
// We persist the changes
|
||||
let mut db = db.lock().unwrap();
|
||||
db.stage((chain_changeset, indexed_tx_graph_changeset));
|
||||
db.stage((local_chain_changeset, indexed_tx_graph_changeset));
|
||||
db.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
|
||||
const SEND_AMOUNT: u64 = 5000;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
|
||||
const STOP_GAP: usize = 50;
|
||||
const BATCH_SIZE: usize = 5;
|
||||
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::bitcoin::Address;
|
||||
use bdk::wallet::Update;
|
||||
use bdk::SignOptions;
|
||||
use bdk::bitcoin::{Address, Amount};
|
||||
use bdk::chain::collections::HashSet;
|
||||
use bdk::{bitcoin::Network, Wallet};
|
||||
use bdk::{KeychainKind, SignOptions};
|
||||
use bdk_electrum::{
|
||||
electrum_client::{self, ElectrumApi},
|
||||
ElectrumExt, ElectrumUpdate,
|
||||
ElectrumExt,
|
||||
};
|
||||
use bdk_file_store::Store;
|
||||
|
||||
@@ -29,7 +29,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
Network::Testnet,
|
||||
)?;
|
||||
|
||||
let address = wallet.try_get_address(bdk::wallet::AddressIndex::New)?;
|
||||
let address = wallet.next_unused_address(KeychainKind::External)?;
|
||||
println!("Generated Address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance();
|
||||
@@ -38,44 +38,30 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
print!("Syncing...");
|
||||
let client = electrum_client::Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
|
||||
let prev_tip = wallet.latest_checkpoint();
|
||||
let keychain_spks = wallet
|
||||
.all_unbounded_spk_iters()
|
||||
.into_iter()
|
||||
.map(|(k, k_spks)| {
|
||||
let mut once = Some(());
|
||||
let mut stdout = std::io::stdout();
|
||||
let k_spks = k_spks
|
||||
.inspect(move |(spk_i, _)| match once.take() {
|
||||
Some(_) => print!("\nScanning keychain [{:?}]", k),
|
||||
None => print!(" {:<3}", spk_i),
|
||||
})
|
||||
.inspect(move |_| stdout.flush().expect("must flush"));
|
||||
(k, k_spks)
|
||||
let request = wallet
|
||||
.start_full_scan()
|
||||
.inspect_spks_for_all_keychains({
|
||||
let mut once = HashSet::<KeychainKind>::new();
|
||||
move |k, spk_i, _| {
|
||||
if once.insert(k) {
|
||||
print!("\nScanning keychain [{:?}]", k)
|
||||
} else {
|
||||
print!(" {:<3}", spk_i)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));
|
||||
|
||||
let (
|
||||
ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
},
|
||||
keychain_update,
|
||||
) = client.full_scan(prev_tip, keychain_spks, STOP_GAP, BATCH_SIZE)?;
|
||||
let mut update = client
|
||||
.full_scan(request, STOP_GAP, BATCH_SIZE, false)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
println!();
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(wallet.as_ref());
|
||||
let mut graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
let wallet_update = Update {
|
||||
last_active_indices: keychain_update,
|
||||
graph: graph_update,
|
||||
chain: Some(chain_update),
|
||||
};
|
||||
wallet.apply_update(wallet_update)?;
|
||||
wallet.apply_update(update)?;
|
||||
wallet.commit()?;
|
||||
|
||||
let balance = wallet.get_balance();
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::{io::Write, str::FromStr};
|
||||
use std::{collections::BTreeSet, io::Write, str::FromStr};
|
||||
|
||||
use bdk::{
|
||||
bitcoin::{Address, Network},
|
||||
wallet::{AddressIndex, Update},
|
||||
SignOptions, Wallet,
|
||||
bitcoin::{Address, Amount, Network, Script},
|
||||
KeychainKind, SignOptions, Wallet,
|
||||
};
|
||||
use bdk_esplora::{esplora_client, EsploraAsyncExt};
|
||||
use bdk_file_store::Store;
|
||||
|
||||
const DB_MAGIC: &str = "bdk_wallet_esplora_async_example";
|
||||
const SEND_AMOUNT: u64 = 5000;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
|
||||
const STOP_GAP: usize = 50;
|
||||
const PARALLEL_REQUESTS: usize = 5;
|
||||
|
||||
@@ -27,7 +26,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
Network::Testnet,
|
||||
)?;
|
||||
|
||||
let address = wallet.try_get_address(AddressIndex::New)?;
|
||||
let address = wallet.next_unused_address(KeychainKind::External)?;
|
||||
println!("Generated Address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance();
|
||||
@@ -37,35 +36,44 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
let client =
|
||||
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_async()?;
|
||||
|
||||
let prev_tip = wallet.latest_checkpoint();
|
||||
let keychain_spks = wallet
|
||||
.all_unbounded_spk_iters()
|
||||
.into_iter()
|
||||
.map(|(k, k_spks)| {
|
||||
let mut once = Some(());
|
||||
let mut stdout = std::io::stdout();
|
||||
let k_spks = k_spks
|
||||
.inspect(move |(spk_i, _)| match once.take() {
|
||||
Some(_) => print!("\nScanning keychain [{:?}]", k),
|
||||
None => print!(" {:<3}", spk_i),
|
||||
})
|
||||
.inspect(move |_| stdout.flush().expect("must flush"));
|
||||
(k, k_spks)
|
||||
fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
|
||||
let mut once = Some(());
|
||||
let mut stdout = std::io::stdout();
|
||||
move |spk_i, _| {
|
||||
match once.take() {
|
||||
Some(_) => print!("\nScanning keychain [{:?}]", kind),
|
||||
None => print!(" {:<3}", spk_i),
|
||||
};
|
||||
stdout.flush().expect("must flush");
|
||||
}
|
||||
}
|
||||
let request = wallet
|
||||
.start_full_scan()
|
||||
.inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::<KeychainKind>::new();
|
||||
move |keychain, spk_i, _| {
|
||||
match once.insert(keychain) {
|
||||
true => print!("\nScanning keychain [{:?}]", keychain),
|
||||
false => print!(" {:<3}", spk_i),
|
||||
}
|
||||
std::io::stdout().flush().expect("must flush")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let (mut update_graph, last_active_indices) = client
|
||||
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
|
||||
.await?;
|
||||
.inspect_spks_for_keychain(
|
||||
KeychainKind::External,
|
||||
generate_inspect(KeychainKind::External),
|
||||
)
|
||||
.inspect_spks_for_keychain(
|
||||
KeychainKind::Internal,
|
||||
generate_inspect(KeychainKind::Internal),
|
||||
);
|
||||
|
||||
let mut update = client
|
||||
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
|
||||
.await?;
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = update_graph.update_last_seen_unconfirmed(now);
|
||||
let missing_heights = update_graph.missing_heights(wallet.local_chain());
|
||||
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
|
||||
let update = Update {
|
||||
last_active_indices,
|
||||
graph: update_graph,
|
||||
chain: Some(chain_update),
|
||||
};
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
wallet.apply_update(update)?;
|
||||
wallet.commit()?;
|
||||
println!();
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
|
||||
const SEND_AMOUNT: u64 = 1000;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(1000);
|
||||
const STOP_GAP: usize = 5;
|
||||
const PARALLEL_REQUESTS: usize = 1;
|
||||
|
||||
use std::{io::Write, str::FromStr};
|
||||
use std::{collections::BTreeSet, io::Write, str::FromStr};
|
||||
|
||||
use bdk::{
|
||||
bitcoin::{Address, Network},
|
||||
wallet::{AddressIndex, Update},
|
||||
SignOptions, Wallet,
|
||||
bitcoin::{Address, Amount, Network},
|
||||
KeychainKind, SignOptions, Wallet,
|
||||
};
|
||||
use bdk_esplora::{esplora_client, EsploraExt};
|
||||
use bdk_file_store::Store;
|
||||
@@ -26,7 +25,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
Network::Testnet,
|
||||
)?;
|
||||
|
||||
let address = wallet.try_get_address(AddressIndex::New)?;
|
||||
let address = wallet.next_unused_address(KeychainKind::External)?;
|
||||
println!("Generated Address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance();
|
||||
@@ -36,35 +35,20 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
let client =
|
||||
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking();
|
||||
|
||||
let prev_tip = wallet.latest_checkpoint();
|
||||
let keychain_spks = wallet
|
||||
.all_unbounded_spk_iters()
|
||||
.into_iter()
|
||||
.map(|(k, k_spks)| {
|
||||
let mut once = Some(());
|
||||
let mut stdout = std::io::stdout();
|
||||
let k_spks = k_spks
|
||||
.inspect(move |(spk_i, _)| match once.take() {
|
||||
Some(_) => print!("\nScanning keychain [{:?}]", k),
|
||||
None => print!(" {:<3}", spk_i),
|
||||
})
|
||||
.inspect(move |_| stdout.flush().expect("must flush"));
|
||||
(k, k_spks)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let (mut update_graph, last_active_indices) =
|
||||
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?;
|
||||
let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::<KeychainKind>::new();
|
||||
move |keychain, spk_i, _| {
|
||||
match once.insert(keychain) {
|
||||
true => print!("\nScanning keychain [{:?}]", keychain),
|
||||
false => print!(" {:<3}", spk_i),
|
||||
};
|
||||
std::io::stdout().flush().expect("must flush")
|
||||
}
|
||||
});
|
||||
|
||||
let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?;
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = update_graph.update_last_seen_unconfirmed(now);
|
||||
let missing_heights = update_graph.missing_heights(wallet.local_chain());
|
||||
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
|
||||
let update = Update {
|
||||
last_active_indices,
|
||||
graph: update_graph,
|
||||
chain: Some(chain_update),
|
||||
};
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
wallet.apply_update(update)?;
|
||||
wallet.commit()?;
|
||||
|
||||
Reference in New Issue
Block a user