Compare commits
54 Commits
v1.0.0-alp
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee21ffeee0 | ||
|
|
5f238d8e67 | ||
|
|
358e842dcd | ||
|
|
c7f87b50e4 | ||
|
|
446b045161 | ||
|
|
62619d3a4a | ||
|
|
984c758f96 | ||
|
|
a2a64ffb6e | ||
|
|
37fca35dde | ||
|
|
53791eb6c5 | ||
|
|
53942cced4 | ||
|
|
2d1d95a685 | ||
|
|
9a62d56900 | ||
|
|
2bb654077d | ||
|
|
19304c13ec | ||
|
|
798ed8ced2 | ||
|
|
b5557dce70 | ||
|
|
7b97c956c7 | ||
|
|
e5aa4fe9e6 | ||
|
|
2580013912 | ||
|
|
380bc4025a | ||
|
|
7c1861aab9 | ||
|
|
8ab58af093 | ||
|
|
80e190b3e7 | ||
|
|
7c9ba3cfc8 | ||
|
|
2462e90415 | ||
|
|
04d0ab5a97 | ||
|
|
4edf533b67 | ||
|
|
6e648fd5af | ||
|
|
a837cd349b | ||
|
|
0eb1ac2bcb | ||
|
|
6e8a4a8966 | ||
|
|
475a77219a | ||
|
|
0d64beb040 | ||
|
|
89608ddd0f | ||
|
|
09bd86e2d8 | ||
|
|
004957dc29 | ||
|
|
fc637a7bcc | ||
|
|
ec1c5f4cf8 | ||
|
|
06d7dc5c3a | ||
|
|
c01983d02a | ||
|
|
fef70d5e8f | ||
|
|
c3544c9b8c | ||
|
|
5840ce473e | ||
|
|
b290b29502 | ||
|
|
8c78a42163 | ||
|
|
d77a7f2ff1 | ||
|
|
3d44ffaef2 | ||
|
|
2efa299d04 | ||
|
|
2647aff4bc | ||
|
|
c151d8fd23 | ||
|
|
2c324d3759 | ||
|
|
5489f905a4 | ||
|
|
022d5a21cf |
2
.github/workflows/cont_integration.yml
vendored
2
.github/workflows/cont_integration.yml
vendored
@@ -32,8 +32,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
||||||
cargo update -p time --precise "0.3.20"
|
cargo update -p time --precise "0.3.20"
|
||||||
cargo update -p jobserver --precise "0.1.26"
|
|
||||||
cargo update -p home --precise "0.5.5"
|
cargo update -p home --precise "0.5.5"
|
||||||
|
cargo update -p proptest --precise "1.2.0"
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build ${{ matrix.features }}
|
run: cargo build ${{ matrix.features }}
|
||||||
- name: Test
|
- name: Test
|
||||||
|
|||||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -17,6 +17,8 @@ jobs:
|
|||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.2.1
|
uses: Swatinem/rust-cache@v2.2.1
|
||||||
|
- name: Pin dependencies for MSRV
|
||||||
|
run: cargo update -p home --precise "0.5.5"
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
run: cargo doc --no-deps
|
run: cargo doc --no-deps
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"crates/esplora",
|
"crates/esplora",
|
||||||
"crates/bitcoind_rpc",
|
"crates/bitcoind_rpc",
|
||||||
"crates/hwi",
|
"crates/hwi",
|
||||||
|
"crates/testenv",
|
||||||
"example-crates/example_cli",
|
"example-crates/example_cli",
|
||||||
"example-crates/example_electrum",
|
"example-crates/example_electrum",
|
||||||
"example-crates/example_esplora",
|
"example-crates/example_esplora",
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ cargo update -p time --precise "0.3.20"
|
|||||||
cargo update -p jobserver --precise "0.1.26"
|
cargo update -p jobserver --precise "0.1.26"
|
||||||
# home 0.5.9 has MSRV 1.70.0
|
# home 0.5.9 has MSRV 1.70.0
|
||||||
cargo update -p home --precise "0.5.5"
|
cargo update -p home --precise "0.5.5"
|
||||||
|
# proptest 1.4.0 has MSRV 1.65.0
|
||||||
|
cargo update -p proptest --precise "1.2.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
version = "1.0.0-alpha.6"
|
version = "1.0.0-alpha.9"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
documentation = "https://docs.rs/bdk"
|
documentation = "https://docs.rs/bdk"
|
||||||
description = "A modern, lightweight, descriptor-based wallet library"
|
description = "A modern, lightweight, descriptor-based wallet library"
|
||||||
@@ -14,11 +14,11 @@ rust-version = "1.63"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "^0.8"
|
rand = "^0.8"
|
||||||
miniscript = { version = "10.0.0", features = ["serde"], default-features = false }
|
miniscript = { version = "11.0.0", features = ["serde"], default-features = false }
|
||||||
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
bitcoin = { version = "0.31.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
bdk_chain = { path = "../chain", version = "0.10.0", features = ["miniscript", "serde"], default-features = false }
|
bdk_chain = { path = "../chain", version = "0.12.0", features = ["miniscript", "serde"], default-features = false }
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
bip39 = { version = "2.0", optional = true }
|
bip39 = { version = "2.0", optional = true }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
## `bdk`
|
## `bdk`
|
||||||
|
|
||||||
The `bdk` crate provides the [`Wallet`](`crate::Wallet`) type which is a simple, high-level
|
The `bdk` crate provides the [`Wallet`] type which is a simple, high-level
|
||||||
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
|
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
|
||||||
for many simple applications as well as a good demonstration of how to use the other mechanisms to
|
for many simple applications as well as a good demonstration of how to use the other mechanisms to
|
||||||
construct a wallet. It has two keychains (external and internal) which are defined by
|
construct a wallet. It has two keychains (external and internal) which are defined by
|
||||||
@@ -34,51 +34,51 @@ construct a wallet. It has two keychains (external and internal) which are defin
|
|||||||
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
|
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
|
||||||
can create and sign transactions.
|
can create and sign transactions.
|
||||||
|
|
||||||
For more information, see the [`Wallet`'s documentation](https://docs.rs/bdk/latest/bdk/wallet/struct.Wallet.html).
|
For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
|
||||||
|
|
||||||
### Blockchain data
|
### Blockchain data
|
||||||
|
|
||||||
In order to get blockchain data for `Wallet` to consume, you have to put it into particular form.
|
In order to get blockchain data for `Wallet` to consume, you should configure a client from
|
||||||
Right now this is [`KeychainScan`] which is defined in [`bdk_chain`].
|
an available chain source. Typically you make a request to the chain source and get a response
|
||||||
|
that the `Wallet` can use to update its view of the chain.
|
||||||
This can be created manually or from blockchain-scanning crates.
|
|
||||||
|
|
||||||
**Blockchain Data Sources**
|
**Blockchain Data Sources**
|
||||||
|
|
||||||
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
|
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
|
||||||
* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
|
* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
|
||||||
|
* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures.
|
||||||
|
|
||||||
**Examples**
|
**Examples**
|
||||||
|
|
||||||
* [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora)
|
* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
|
||||||
|
* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking)
|
||||||
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
|
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
|
||||||
|
* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
|
||||||
|
|
||||||
### Persistence
|
### Persistence
|
||||||
|
|
||||||
To persist the `Wallet` on disk, `Wallet` needs to be constructed with a
|
To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
|
||||||
[`Persist`](https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainPersist.html) implementation.
|
|
||||||
|
|
||||||
**Implementations**
|
**Implementations**
|
||||||
|
|
||||||
* [`bdk_file_store`]: a simple flat-file implementation of `Persist`.
|
* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`].
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```rust
|
<!-- compile_fail because outpoint and txout are fake variables -->
|
||||||
use bdk::{bitcoin::Network, wallet::{AddressIndex, Wallet}};
|
```rust,compile_fail
|
||||||
|
use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// a type that implements `Persist`
|
// Create a new file `Store`.
|
||||||
let db = ();
|
let db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store");
|
||||||
|
|
||||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
|
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
|
||||||
let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create");
|
let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet");
|
||||||
|
|
||||||
// get a new address (this increments revealed derivation index)
|
// Insert a single `TxOut` at `OutPoint` into the wallet.
|
||||||
println!("revealed address: {}", wallet.get_address(AddressIndex::New));
|
let _ = wallet.insert_txout(outpoint, txout);
|
||||||
println!("staged changes: {:?}", wallet.staged());
|
wallet.commit().expect("must write to database");
|
||||||
// persist changes
|
|
||||||
wallet.commit().expect("must save");
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -218,9 +218,11 @@ submitted for inclusion in the work by you, as defined in the Apache-2.0
|
|||||||
license, shall be dual licensed as above, without any additional terms or
|
license, shall be dual licensed as above, without any additional terms or
|
||||||
conditions.
|
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
|
||||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
||||||
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
|
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
|
||||||
[`KeychainScan`]: https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainScan.html
|
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
|
||||||
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
|
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
|
||||||
|
|||||||
@@ -274,14 +274,13 @@ macro_rules! impl_sortedmulti {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! parse_tap_tree {
|
macro_rules! parse_tap_tree {
|
||||||
( @merge $tree_a:expr, $tree_b:expr) => {{
|
( @merge $tree_a:expr, $tree_b:expr) => {{
|
||||||
use $crate::alloc::sync::Arc;
|
|
||||||
use $crate::miniscript::descriptor::TapTree;
|
use $crate::miniscript::descriptor::TapTree;
|
||||||
|
|
||||||
$tree_a
|
$tree_a
|
||||||
.and_then(|tree_a| Ok((tree_a, $tree_b?)))
|
.and_then(|tree_a| Ok((tree_a, $tree_b?)))
|
||||||
.and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
|
.and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
|
||||||
a_keymap.extend(b_keymap.into_iter());
|
a_keymap.extend(b_keymap.into_iter());
|
||||||
Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
|
Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
|
||||||
})
|
})
|
||||||
|
|
||||||
}};
|
}};
|
||||||
@@ -806,7 +805,7 @@ mod test {
|
|||||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||||
use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
|
use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
|
||||||
use bitcoin::bip32;
|
use bitcoin::bip32;
|
||||||
use bitcoin::network::constants::Network::{Bitcoin, Regtest, Signet, Testnet};
|
use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet};
|
||||||
use bitcoin::PrivateKey;
|
use bitcoin::PrivateKey;
|
||||||
|
|
||||||
// test the descriptor!() macro
|
// test the descriptor!() macro
|
||||||
@@ -936,7 +935,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip32_legacy_descriptors() {
|
fn test_bip32_legacy_descriptors() {
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
|
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
@@ -981,7 +980,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip32_segwitv0_descriptors() {
|
fn test_bip32_segwitv0_descriptors() {
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
|
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
@@ -1038,10 +1037,10 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dsl_sortedmulti() {
|
fn test_dsl_sortedmulti() {
|
||||||
let key_1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
let path_1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path_1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
|
|
||||||
let key_2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||||
let path_2 = bip32::DerivationPath::from_str("m/1").unwrap();
|
let path_2 = bip32::DerivationPath::from_str("m/1").unwrap();
|
||||||
|
|
||||||
let desc_key1 = (key_1, path_1);
|
let desc_key1 = (key_1, path_1);
|
||||||
@@ -1097,7 +1096,7 @@ mod test {
|
|||||||
// - verify the valid_networks returned is correctly computed based on the keys present in the descriptor
|
// - verify the valid_networks returned is correctly computed based on the keys present in the descriptor
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_networks() {
|
fn test_valid_networks() {
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
@@ -1107,7 +1106,7 @@ mod test {
|
|||||||
[Testnet, Regtest, Signet].iter().cloned().collect()
|
[Testnet, Regtest, Signet].iter().cloned().collect()
|
||||||
);
|
);
|
||||||
|
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||||
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
@@ -1120,15 +1119,15 @@ mod test {
|
|||||||
fn test_key_maps_merged() {
|
fn test_key_maps_merged() {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
|
let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||||
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
||||||
let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
|
let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
||||||
let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||||
let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
|
let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
@@ -1152,7 +1151,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_script_context_validation() {
|
fn test_script_context_validation() {
|
||||||
// this compiles
|
// this compiles
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();
|
let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub enum Error {
|
|||||||
/// Miniscript error
|
/// Miniscript error
|
||||||
Miniscript(miniscript::Error),
|
Miniscript(miniscript::Error),
|
||||||
/// Hex decoding error
|
/// Hex decoding error
|
||||||
Hex(bitcoin::hashes::hex::Error),
|
Hex(bitcoin::hex::HexToBytesError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::keys::KeyError> for Error {
|
impl From<crate::keys::KeyError> for Error {
|
||||||
@@ -110,8 +110,8 @@ impl From<miniscript::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bitcoin::hashes::hex::Error> for Error {
|
impl From<bitcoin::hex::HexToBytesError> for Error {
|
||||||
fn from(err: bitcoin::hashes::hex::Error) -> Self {
|
fn from(err: bitcoin::hex::HexToBytesError) -> Self {
|
||||||
Error::Hex(err)
|
Error::Hex(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::collections::BTreeMap;
|
|||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub};
|
||||||
use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
|
use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
|
||||||
use bitcoin::{psbt, taproot};
|
use bitcoin::{psbt, taproot};
|
||||||
use bitcoin::{Network, TxOut};
|
use bitcoin::{Network, TxOut};
|
||||||
@@ -377,7 +377,7 @@ where
|
|||||||
pub(crate) trait DescriptorMeta {
|
pub(crate) trait DescriptorMeta {
|
||||||
fn is_witness(&self) -> bool;
|
fn is_witness(&self) -> bool;
|
||||||
fn is_taproot(&self) -> bool;
|
fn is_taproot(&self) -> bool;
|
||||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>>;
|
fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>>;
|
||||||
fn derive_from_hd_keypaths(
|
fn derive_from_hd_keypaths(
|
||||||
&self,
|
&self,
|
||||||
hd_keypaths: &HdKeyPaths,
|
hd_keypaths: &HdKeyPaths,
|
||||||
@@ -418,7 +418,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
self.desc_type() == DescriptorType::Tr
|
self.desc_type() == DescriptorType::Tr
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>> {
|
fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>> {
|
||||||
let mut answer = Vec::new();
|
let mut answer = Vec::new();
|
||||||
|
|
||||||
self.for_each_key(|pk| {
|
self.for_each_key(|pk| {
|
||||||
@@ -438,21 +438,20 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Option<DerivedDescriptor> {
|
) -> Option<DerivedDescriptor> {
|
||||||
// Ensure that deriving `xpub` with `path` yields `expected`
|
// Ensure that deriving `xpub` with `path` yields `expected`
|
||||||
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
let verify_key =
|
||||||
path: &DerivationPath,
|
|xpub: &DescriptorXKey<Xpub>, path: &DerivationPath, expected: &SinglePubKey| {
|
||||||
expected: &SinglePubKey| {
|
let derived = xpub
|
||||||
let derived = xpub
|
.xkey
|
||||||
.xkey
|
.derive_pub(secp, path)
|
||||||
.derive_pub(secp, path)
|
.expect("The path should never contain hardened derivation steps")
|
||||||
.expect("The path should never contain hardened derivation steps")
|
.public_key;
|
||||||
.public_key;
|
|
||||||
|
|
||||||
match expected {
|
match expected {
|
||||||
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
|
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
|
||||||
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
|
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut path_found = None;
|
let mut path_found = None;
|
||||||
|
|
||||||
@@ -605,10 +604,10 @@ mod test {
|
|||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
use assert_matches::assert_matches;
|
use assert_matches::assert_matches;
|
||||||
use bitcoin::hashes::hex::FromHex;
|
use bitcoin::hex::FromHex;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use bitcoin::ScriptBuf;
|
use bitcoin::ScriptBuf;
|
||||||
use bitcoin::{bip32, psbt::Psbt};
|
use bitcoin::{bip32, Psbt};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::psbt::PsbtUtils;
|
use crate::psbt::PsbtUtils;
|
||||||
@@ -727,7 +726,7 @@ mod test {
|
|||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
|
|
||||||
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
||||||
@@ -746,7 +745,7 @@ mod test {
|
|||||||
let mut xprv_testnet = xprv;
|
let mut xprv_testnet = xprv;
|
||||||
xprv_testnet.network = Network::Testnet;
|
xprv_testnet.network = Network::Testnet;
|
||||||
|
|
||||||
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
|
let xpub_testnet = bip32::Xpub::from_priv(&secp, &xprv_testnet);
|
||||||
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
|
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
|
||||||
xkey: xpub_testnet,
|
xkey: xpub_testnet,
|
||||||
origin: None,
|
origin: None,
|
||||||
@@ -836,7 +835,7 @@ mod test {
|
|||||||
fn test_descriptor_from_str_from_output_of_macro() {
|
fn test_descriptor_from_str_from_output_of_macro() {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let tpub = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
|
let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
|
||||||
let key = (tpub, path).into_descriptor_key().unwrap();
|
let key = (tpub, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
@@ -895,7 +894,7 @@ mod test {
|
|||||||
.update_with_descriptor_unchecked(&descriptor)
|
.update_with_descriptor_unchecked(&descriptor)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
|
assert_eq!(psbt_input.redeem_script, Some(script.to_p2wsh()));
|
||||||
assert_eq!(psbt_input.witness_script, Some(script));
|
assert_eq!(psbt_input.witness_script, Some(script));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1137,7 +1137,7 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
|||||||
let key_spend_sig =
|
let key_spend_sig =
|
||||||
miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
|
miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
|
||||||
|
|
||||||
if tr.taptree().is_none() {
|
if tr.tap_tree().is_none() {
|
||||||
Ok(Some(key_spend_sig))
|
Ok(Some(key_spend_sig))
|
||||||
} else {
|
} else {
|
||||||
let mut items = vec![key_spend_sig];
|
let mut items = vec![key_spend_sig];
|
||||||
@@ -1184,8 +1184,8 @@ mod test {
|
|||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
||||||
let path = bip32::DerivationPath::from_str(path).unwrap();
|
let path = bip32::DerivationPath::from_str(path).unwrap();
|
||||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
|
||||||
let tpub = bip32::ExtendedPubKey::from_priv(secp, &tprv);
|
let tpub = bip32::Xpub::from_priv(secp, &tprv);
|
||||||
let fingerprint = tprv.fingerprint(secp);
|
let fingerprint = tprv.fingerprint(secp);
|
||||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip44;
|
/// use bdk::template::Bip44;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip44(key.clone(), KeychainKind::External),
|
/// Bip44(key.clone(), KeychainKind::External),
|
||||||
/// Some(Bip44(key, KeychainKind::Internal)),
|
/// Some(Bip44(key, KeychainKind::Internal)),
|
||||||
@@ -232,7 +232,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip44Public;
|
/// use bdk::template::Bip44Public;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||||
@@ -270,7 +270,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip49;
|
/// use bdk::template::Bip49;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip49(key.clone(), KeychainKind::External),
|
/// Bip49(key.clone(), KeychainKind::External),
|
||||||
/// Some(Bip49(key, KeychainKind::Internal)),
|
/// Some(Bip49(key, KeychainKind::Internal)),
|
||||||
@@ -307,7 +307,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip49Public;
|
/// use bdk::template::Bip49Public;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||||
@@ -345,7 +345,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip84;
|
/// use bdk::template::Bip84;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip84(key.clone(), KeychainKind::External),
|
/// Bip84(key.clone(), KeychainKind::External),
|
||||||
/// Some(Bip84(key, KeychainKind::Internal)),
|
/// Some(Bip84(key, KeychainKind::Internal)),
|
||||||
@@ -382,7 +382,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip84Public;
|
/// use bdk::template::Bip84Public;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||||
@@ -420,7 +420,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip86;
|
/// use bdk::template::Bip86;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip86(key.clone(), KeychainKind::External),
|
/// Bip86(key.clone(), KeychainKind::External),
|
||||||
/// Some(Bip86(key, KeychainKind::Internal)),
|
/// Some(Bip86(key, KeychainKind::Internal)),
|
||||||
@@ -457,7 +457,7 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
|||||||
/// # use bdk::wallet::AddressIndex::New;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
/// use bdk::template::Bip86Public;
|
/// use bdk::template::Bip86Public;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||||
/// let mut wallet = Wallet::new_no_persist(
|
/// let mut wallet = Wallet::new_no_persist(
|
||||||
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
|
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
|
||||||
@@ -567,7 +567,7 @@ mod test {
|
|||||||
fn test_bip44_template_cointype() {
|
fn test_bip44_template_cointype() {
|
||||||
use bitcoin::bip32::ChildNumber::{self, Hardened};
|
use bitcoin::bip32::ChildNumber::{self, Hardened};
|
||||||
|
|
||||||
let xprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
let xprvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
||||||
assert_eq!(Network::Bitcoin, xprvkey.network);
|
assert_eq!(Network::Bitcoin, xprvkey.network);
|
||||||
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
|
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
|
||||||
.build(Network::Bitcoin)
|
.build(Network::Bitcoin)
|
||||||
@@ -581,7 +581,7 @@ mod test {
|
|||||||
assert_matches!(coin_type, Hardened { index: 0 });
|
assert_matches!(coin_type, Hardened { index: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let tprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let tprvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
assert_eq!(Network::Testnet, tprvkey.network);
|
assert_eq!(Network::Testnet, tprvkey.network);
|
||||||
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
|
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
|
||||||
.build(Network::Testnet)
|
.build(Network::Testnet)
|
||||||
@@ -740,7 +740,7 @@ mod test {
|
|||||||
// BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
|
// BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip44_template() {
|
fn test_bip44_template() {
|
||||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
@@ -770,7 +770,7 @@ mod test {
|
|||||||
// BIP44 public `pkh(key/{0,1}/*)`
|
// BIP44 public `pkh(key/{0,1}/*)`
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip44_public_template() {
|
fn test_bip44_public_template() {
|
||||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||||
@@ -801,7 +801,7 @@ mod test {
|
|||||||
// BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
|
// BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip49_template() {
|
fn test_bip49_template() {
|
||||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
@@ -831,7 +831,7 @@ mod test {
|
|||||||
// BIP49 public `sh(wpkh(key/{0,1}/*))`
|
// BIP49 public `sh(wpkh(key/{0,1}/*))`
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip49_public_template() {
|
fn test_bip49_public_template() {
|
||||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||||
@@ -862,7 +862,7 @@ mod test {
|
|||||||
// BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)`
|
// BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)`
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip84_template() {
|
fn test_bip84_template() {
|
||||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
@@ -892,7 +892,7 @@ mod test {
|
|||||||
// BIP84 public `wpkh(key/{0,1}/*)`
|
// BIP84 public `wpkh(key/{0,1}/*)`
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip84_public_template() {
|
fn test_bip84_public_template() {
|
||||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||||
@@ -924,7 +924,7 @@ mod test {
|
|||||||
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip86_template() {
|
fn test_bip86_template() {
|
||||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
|
let prvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
@@ -955,7 +955,7 @@ mod test {
|
|||||||
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip86_public_template() {
|
fn test_bip86_public_template() {
|
||||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
|
let pubkey = bitcoin::bip32::Xpub::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
|
||||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap();
|
let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
||||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self[..])?.into())
|
Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_descriptor_key(
|
fn into_descriptor_key(
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
|||||||
Ok((public, KeyMap::default(), valid_networks))
|
Ok((public, KeyMap::default(), valid_networks))
|
||||||
}
|
}
|
||||||
DescriptorKey::Secret(secret, valid_networks, _) => {
|
DescriptorKey::Secret(secret, valid_networks, _) => {
|
||||||
let mut key_map = KeyMap::with_capacity(1);
|
let mut key_map = KeyMap::new();
|
||||||
|
|
||||||
let public = secret
|
let public = secret
|
||||||
.to_public(secp)
|
.to_public(secp)
|
||||||
@@ -309,15 +309,15 @@ pub trait IntoDescriptorKey<Ctx: ScriptContext>: Sized {
|
|||||||
|
|
||||||
/// Enum for extended keys that can be either `xprv` or `xpub`
|
/// Enum for extended keys that can be either `xprv` or `xpub`
|
||||||
///
|
///
|
||||||
/// An instance of [`ExtendedKey`] can be constructed from an [`ExtendedPrivKey`](bip32::ExtendedPrivKey)
|
/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv)
|
||||||
/// or an [`ExtendedPubKey`](bip32::ExtendedPubKey) by using the `From` trait.
|
/// or an [`Xpub`](bip32::Xpub) by using the `From` trait.
|
||||||
///
|
///
|
||||||
/// Defaults to the [`Legacy`](miniscript::Legacy) context.
|
/// Defaults to the [`Legacy`](miniscript::Legacy) context.
|
||||||
pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
|
pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
|
||||||
/// A private extended key, aka an `xprv`
|
/// A private extended key, aka an `xprv`
|
||||||
Private((bip32::ExtendedPrivKey, PhantomData<Ctx>)),
|
Private((bip32::Xpriv, PhantomData<Ctx>)),
|
||||||
/// A public extended key, aka an `xpub`
|
/// A public extended key, aka an `xpub`
|
||||||
Public((bip32::ExtendedPubKey, PhantomData<Ctx>)),
|
Public((bip32::Xpub, PhantomData<Ctx>)),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||||
@@ -329,9 +329,9 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transform the [`ExtendedKey`] into an [`ExtendedPrivKey`](bip32::ExtendedPrivKey) for the
|
/// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the
|
||||||
/// given [`Network`], if the key contains the private data
|
/// given [`Network`], if the key contains the private data
|
||||||
pub fn into_xprv(self, network: Network) -> Option<bip32::ExtendedPrivKey> {
|
pub fn into_xprv(self, network: Network) -> Option<bip32::Xpriv> {
|
||||||
match self {
|
match self {
|
||||||
ExtendedKey::Private((mut xprv, _)) => {
|
ExtendedKey::Private((mut xprv, _)) => {
|
||||||
xprv.network = network;
|
xprv.network = network;
|
||||||
@@ -341,15 +341,15 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transform the [`ExtendedKey`] into an [`ExtendedPubKey`](bip32::ExtendedPubKey) for the
|
/// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the
|
||||||
/// given [`Network`]
|
/// given [`Network`]
|
||||||
pub fn into_xpub<C: Signing>(
|
pub fn into_xpub<C: Signing>(
|
||||||
self,
|
self,
|
||||||
network: bitcoin::Network,
|
network: bitcoin::Network,
|
||||||
secp: &Secp256k1<C>,
|
secp: &Secp256k1<C>,
|
||||||
) -> bip32::ExtendedPubKey {
|
) -> bip32::Xpub {
|
||||||
let mut xpub = match self {
|
let mut xpub = match self {
|
||||||
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_priv(secp, &xprv),
|
ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv),
|
||||||
ExtendedKey::Public((xpub, _)) => xpub,
|
ExtendedKey::Public((xpub, _)) => xpub,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -358,14 +358,14 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> From<bip32::ExtendedPubKey> for ExtendedKey<Ctx> {
|
impl<Ctx: ScriptContext> From<bip32::Xpub> for ExtendedKey<Ctx> {
|
||||||
fn from(xpub: bip32::ExtendedPubKey) -> Self {
|
fn from(xpub: bip32::Xpub) -> Self {
|
||||||
ExtendedKey::Public((xpub, PhantomData))
|
ExtendedKey::Public((xpub, PhantomData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||||
fn from(xprv: bip32::ExtendedPrivKey) -> Self {
|
fn from(xprv: bip32::Xpriv) -> Self {
|
||||||
ExtendedKey::Private((xprv, PhantomData))
|
ExtendedKey::Private((xprv, PhantomData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,8 +383,8 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
|||||||
///
|
///
|
||||||
/// ## Examples
|
/// ## Examples
|
||||||
///
|
///
|
||||||
/// Key types that can be directly converted into an [`ExtendedPrivKey`] or
|
/// Key types that can be directly converted into an [`Xpriv`] or
|
||||||
/// an [`ExtendedPubKey`] can implement only the required `into_extended_key()` method.
|
/// an [`Xpub`] can implement only the required `into_extended_key()` method.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use bdk::bitcoin;
|
/// use bdk::bitcoin;
|
||||||
@@ -399,7 +399,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
|||||||
///
|
///
|
||||||
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||||
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
/// let xprv = bip32::ExtendedPrivKey {
|
/// let xprv = bip32::Xpriv {
|
||||||
/// network: self.network,
|
/// network: self.network,
|
||||||
/// depth: 0,
|
/// depth: 0,
|
||||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||||
@@ -415,7 +415,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
|||||||
///
|
///
|
||||||
/// Types that don't internally encode the [`Network`] in which they are valid need some extra
|
/// Types that don't internally encode the [`Network`] in which they are valid need some extra
|
||||||
/// steps to override the set of valid networks, otherwise only the network specified in the
|
/// steps to override the set of valid networks, otherwise only the network specified in the
|
||||||
/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
|
/// [`Xpriv`] or [`Xpub`] will be considered valid.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use bdk::bitcoin;
|
/// use bdk::bitcoin;
|
||||||
@@ -431,7 +431,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
|||||||
///
|
///
|
||||||
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||||
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
/// let xprv = bip32::ExtendedPrivKey {
|
/// let xprv = bip32::Xpriv {
|
||||||
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
|
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
|
||||||
/// depth: 0,
|
/// depth: 0,
|
||||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||||
@@ -459,8 +459,8 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// [`DerivationPath`]: (bip32::DerivationPath)
|
/// [`DerivationPath`]: (bip32::DerivationPath)
|
||||||
/// [`ExtendedPrivKey`]: (bip32::ExtendedPrivKey)
|
/// [`Xpriv`]: (bip32::Xpriv)
|
||||||
/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey)
|
/// [`Xpub`]: (bip32::Xpub)
|
||||||
pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
|
pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
|
||||||
/// Consume `self` and turn it into an [`ExtendedKey`]
|
/// Consume `self` and turn it into an [`ExtendedKey`]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -520,13 +520,13 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for ExtendedKey<Ctx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPubKey {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpub {
|
||||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
Ok(self.into())
|
Ok(self.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPrivKey {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpriv {
|
||||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
Ok(self.into())
|
Ok(self.into())
|
||||||
}
|
}
|
||||||
@@ -670,7 +670,7 @@ where
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::ExtendedPrivKey {
|
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
|
||||||
type Entropy = [u8; 32];
|
type Entropy = [u8; 32];
|
||||||
|
|
||||||
type Options = ();
|
type Options = ();
|
||||||
@@ -681,7 +681,7 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::ExtendedPrivKey {
|
|||||||
entropy: Self::Entropy,
|
entropy: Self::Entropy,
|
||||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||||
// pick a arbitrary network here, but say that we support all of them
|
// pick a arbitrary network here, but say that we support all of them
|
||||||
let xprv = bip32::ExtendedPrivKey::new_master(Network::Bitcoin, entropy.as_ref())?;
|
let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?;
|
||||||
Ok(GeneratedKey::new(xprv, any_network()))
|
Ok(GeneratedKey::new(xprv, any_network()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -971,7 +971,7 @@ pub mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_keys_generate_xprv() {
|
fn test_keys_generate_xprv() {
|
||||||
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
|
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
|
||||||
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
||||||
|
|
||||||
assert_eq!(generated_xprv.valid_networks, any_network());
|
assert_eq!(generated_xprv.valid_networks, any_network());
|
||||||
assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q");
|
assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q");
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
// You may not use this file except in accordance with one or both of these
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
//! Additional functions on the `rust-bitcoin` `Psbt` structure.
|
||||||
|
|
||||||
use crate::FeeRate;
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
use bitcoin::Amount;
|
||||||
|
use bitcoin::FeeRate;
|
||||||
|
use bitcoin::Psbt;
|
||||||
use bitcoin::TxOut;
|
use bitcoin::TxOut;
|
||||||
|
|
||||||
// TODO upstream the functions here to `rust-bitcoin`?
|
// TODO upstream the functions here to `rust-bitcoin`?
|
||||||
@@ -28,7 +29,7 @@ pub trait PsbtUtils {
|
|||||||
fn fee_amount(&self) -> Option<u64>;
|
fn fee_amount(&self) -> Option<u64>;
|
||||||
|
|
||||||
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
|
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
|
||||||
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
|
/// `Psbt` is finalized and all witness/signature data is added to the
|
||||||
/// transaction.
|
/// transaction.
|
||||||
/// If the PSBT is missing a TxOut for an input returns None.
|
/// If the PSBT is missing a TxOut for an input returns None.
|
||||||
fn fee_rate(&self) -> Option<FeeRate>;
|
fn fee_rate(&self) -> Option<FeeRate>;
|
||||||
@@ -53,8 +54,13 @@ impl PsbtUtils for Psbt {
|
|||||||
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
|
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
|
||||||
|
|
||||||
utxos.map(|inputs| {
|
utxos.map(|inputs| {
|
||||||
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
|
let input_amount: u64 = inputs.iter().map(|i| i.value.to_sat()).sum();
|
||||||
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
|
let output_amount: u64 = self
|
||||||
|
.unsigned_tx
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.map(|o| o.value.to_sat())
|
||||||
|
.sum();
|
||||||
input_amount
|
input_amount
|
||||||
.checked_sub(output_amount)
|
.checked_sub(output_amount)
|
||||||
.expect("input amount must be greater than output amount")
|
.expect("input amount must be greater than output amount")
|
||||||
@@ -63,9 +69,7 @@ impl PsbtUtils for Psbt {
|
|||||||
|
|
||||||
fn fee_rate(&self) -> Option<FeeRate> {
|
fn fee_rate(&self) -> Option<FeeRate> {
|
||||||
let fee_amount = self.fee_amount();
|
let fee_amount = self.fee_amount();
|
||||||
fee_amount.map(|fee| {
|
let weight = self.clone().extract_tx().ok()?.weight();
|
||||||
let weight = self.clone().extract_tx().weight();
|
fee_amount.map(|fee| Amount::from_sat(fee) / weight)
|
||||||
FeeRate::from_wu(fee, weight)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
|
|
||||||
use alloc::boxed::Box;
|
use alloc::boxed::Box;
|
||||||
use core::convert::AsRef;
|
use core::convert::AsRef;
|
||||||
use core::ops::Sub;
|
|
||||||
|
|
||||||
use bdk_chain::ConfirmationTime;
|
use bdk_chain::ConfirmationTime;
|
||||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||||
use bitcoin::{psbt, Weight};
|
use bitcoin::psbt;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -47,116 +46,6 @@ impl AsRef<[u8]> for KeychainKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fee rate
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
|
||||||
// Internally stored as satoshi/vbyte
|
|
||||||
pub struct FeeRate(f32);
|
|
||||||
|
|
||||||
impl FeeRate {
|
|
||||||
/// Create a new instance checking the value provided
|
|
||||||
///
|
|
||||||
/// ## Panics
|
|
||||||
///
|
|
||||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
|
||||||
fn new_checked(value: f32) -> Self {
|
|
||||||
assert!(value.is_normal() || value == 0.0);
|
|
||||||
assert!(value.is_sign_positive());
|
|
||||||
|
|
||||||
FeeRate(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
|
|
||||||
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
|
|
||||||
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
|
|
||||||
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
|
|
||||||
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
|
||||||
///
|
|
||||||
/// ## Panics
|
|
||||||
///
|
|
||||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
|
||||||
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
|
||||||
FeeRate::new_checked(btc_per_kvb * 1e5)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
|
||||||
///
|
|
||||||
/// ## Panics
|
|
||||||
///
|
|
||||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
|
||||||
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
|
||||||
FeeRate::new_checked(sat_per_vb)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`FeeRate`] with the default min relay fee value
|
|
||||||
pub const fn default_min_relay_fee() -> Self {
|
|
||||||
FeeRate(1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate fee rate from `fee` and weight units (`wu`).
|
|
||||||
pub fn from_wu(fee: u64, wu: Weight) -> FeeRate {
|
|
||||||
Self::from_vb(fee, wu.to_vbytes_ceil() as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate fee rate from `fee` and `vbytes`.
|
|
||||||
pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate {
|
|
||||||
let rate = fee as f32 / vbytes as f32;
|
|
||||||
Self::from_sat_per_vb(rate)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the value as satoshi/vbyte
|
|
||||||
pub fn as_sat_per_vb(&self) -> f32 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the value as satoshi/kwu
|
|
||||||
pub fn sat_per_kwu(&self) -> f32 {
|
|
||||||
self.0 * 250.0_f32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate absolute fee in Satoshis using size in weight units.
|
|
||||||
pub fn fee_wu(&self, wu: Weight) -> u64 {
|
|
||||||
self.fee_vb(wu.to_vbytes_ceil() as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
|
||||||
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
|
||||||
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FeeRate {
|
|
||||||
fn default() -> Self {
|
|
||||||
FeeRate::default_min_relay_fee()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sub for FeeRate {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn sub(self, other: FeeRate) -> Self::Output {
|
|
||||||
FeeRate(self.0 - other.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait implemented by types that can be used to measure weight units.
|
|
||||||
pub trait Vbytes {
|
|
||||||
/// Convert weight units to virtual bytes.
|
|
||||||
fn vbytes(self) -> usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vbytes for usize {
|
|
||||||
fn vbytes(self) -> usize {
|
|
||||||
// ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
|
|
||||||
(self as f32 / 4.0).ceil() as usize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An unspent output owned by a [`Wallet`].
|
/// An unspent output owned by a [`Wallet`].
|
||||||
///
|
///
|
||||||
/// [`Wallet`]: crate::Wallet
|
/// [`Wallet`]: crate::Wallet
|
||||||
@@ -244,73 +133,3 @@ impl Utxo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_store_feerate_in_const() {
|
|
||||||
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_invalid_feerate_neg_zero() {
|
|
||||||
let _ = FeeRate::from_sat_per_vb(-0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_invalid_feerate_neg_value() {
|
|
||||||
let _ = FeeRate::from_sat_per_vb(-5.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_invalid_feerate_nan() {
|
|
||||||
let _ = FeeRate::from_sat_per_vb(f32::NAN);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_invalid_feerate_inf() {
|
|
||||||
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_valid_feerate_pos_zero() {
|
|
||||||
let _ = FeeRate::from_sat_per_vb(0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_btc_per_kvb() {
|
|
||||||
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
|
||||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_sat_per_vbyte() {
|
|
||||||
let fee = FeeRate::from_sat_per_vb(1.0);
|
|
||||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_default_min_relay_fee() {
|
|
||||||
let fee = FeeRate::default_min_relay_fee();
|
|
||||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_sat_per_kvb() {
|
|
||||||
let fee = FeeRate::from_sat_per_kvb(1000.0);
|
|
||||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_sat_per_kwu() {
|
|
||||||
let fee = FeeRate::from_sat_per_kwu(250.0);
|
|
||||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
|
||||||
assert_eq!(fee.sat_per_kwu(), 250.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
//! # use bdk::*;
|
//! # use bdk::*;
|
||||||
//! # use bdk::wallet::coin_selection::decide_change;
|
//! # use bdk::wallet::coin_selection::decide_change;
|
||||||
//! # use anyhow::Error;
|
//! # use anyhow::Error;
|
||||||
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
|
|
||||||
//! #[derive(Debug)]
|
//! #[derive(Debug)]
|
||||||
//! struct AlwaysSpendEverything;
|
//! struct AlwaysSpendEverything;
|
||||||
//!
|
//!
|
||||||
@@ -41,7 +40,7 @@
|
|||||||
//! &self,
|
//! &self,
|
||||||
//! required_utxos: Vec<WeightedUtxo>,
|
//! required_utxos: Vec<WeightedUtxo>,
|
||||||
//! optional_utxos: Vec<WeightedUtxo>,
|
//! optional_utxos: Vec<WeightedUtxo>,
|
||||||
//! fee_rate: bdk::FeeRate,
|
//! fee_rate: FeeRate,
|
||||||
//! target_amount: u64,
|
//! target_amount: u64,
|
||||||
//! drain_script: &Script,
|
//! drain_script: &Script,
|
||||||
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
|
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
|
||||||
@@ -53,15 +52,17 @@
|
|||||||
//! .scan(
|
//! .scan(
|
||||||
//! (&mut selected_amount, &mut additional_weight),
|
//! (&mut selected_amount, &mut additional_weight),
|
||||||
//! |(selected_amount, additional_weight), weighted_utxo| {
|
//! |(selected_amount, additional_weight), weighted_utxo| {
|
||||||
//! **selected_amount += weighted_utxo.utxo.txout().value;
|
//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
|
||||||
//! **additional_weight += Weight::from_wu(
|
//! **additional_weight += Weight::from_wu(
|
||||||
//! (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
//! (TxIn::default().segwit_weight().to_wu()
|
||||||
|
//! + weighted_utxo.satisfaction_weight as u64)
|
||||||
|
//! as u64,
|
||||||
//! );
|
//! );
|
||||||
//! Some(weighted_utxo.utxo)
|
//! Some(weighted_utxo.utxo)
|
||||||
//! },
|
//! },
|
||||||
//! )
|
//! )
|
||||||
//! .collect::<Vec<_>>();
|
//! .collect::<Vec<_>>();
|
||||||
//! let additional_fees = fee_rate.fee_wu(additional_weight);
|
//! let additional_fees = (fee_rate * additional_weight).to_sat();
|
||||||
//! let amount_needed_with_fees = additional_fees + target_amount;
|
//! let amount_needed_with_fees = additional_fees + target_amount;
|
||||||
//! if selected_amount < amount_needed_with_fees {
|
//! if selected_amount < amount_needed_with_fees {
|
||||||
//! return Err(coin_selection::Error::InsufficientFunds {
|
//! return Err(coin_selection::Error::InsufficientFunds {
|
||||||
@@ -101,14 +102,15 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::chain::collections::HashSet;
|
use crate::chain::collections::HashSet;
|
||||||
use crate::types::FeeRate;
|
|
||||||
use crate::wallet::utils::IsDust;
|
use crate::wallet::utils::IsDust;
|
||||||
use crate::Utxo;
|
use crate::Utxo;
|
||||||
use crate::WeightedUtxo;
|
use crate::WeightedUtxo;
|
||||||
|
use bitcoin::FeeRate;
|
||||||
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use bitcoin::consensus::encode::serialize;
|
use bitcoin::consensus::encode::serialize;
|
||||||
use bitcoin::OutPoint;
|
use bitcoin::OutPoint;
|
||||||
|
use bitcoin::TxIn;
|
||||||
use bitcoin::{Script, Weight};
|
use bitcoin::{Script, Weight};
|
||||||
|
|
||||||
use core::convert::TryInto;
|
use core::convert::TryInto;
|
||||||
@@ -119,10 +121,6 @@ use rand::seq::SliceRandom;
|
|||||||
/// overridden
|
/// overridden
|
||||||
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
|
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
|
||||||
|
|
||||||
// Base weight of a Txin, not counting the weight needed for satisfying it.
|
|
||||||
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
|
|
||||||
pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
|
|
||||||
|
|
||||||
/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module
|
/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@@ -195,7 +193,7 @@ pub struct CoinSelectionResult {
|
|||||||
impl CoinSelectionResult {
|
impl CoinSelectionResult {
|
||||||
/// The total value of the inputs selected.
|
/// The total value of the inputs selected.
|
||||||
pub fn selected_amount(&self) -> u64 {
|
pub fn selected_amount(&self) -> u64 {
|
||||||
self.selected.iter().map(|u| u.txout().value).sum()
|
self.selected.iter().map(|u| u.txout().value.to_sat()).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The total value of the inputs selected from the local wallet.
|
/// The total value of the inputs selected from the local wallet.
|
||||||
@@ -203,7 +201,7 @@ impl CoinSelectionResult {
|
|||||||
self.selected
|
self.selected
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|u| match u {
|
.filter_map(|u| match u {
|
||||||
Utxo::Local(_) => Some(u.txout().value),
|
Utxo::Local(_) => Some(u.txout().value.to_sat()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.sum()
|
.sum()
|
||||||
@@ -313,7 +311,8 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
|
|||||||
pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
|
pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
|
||||||
// drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
|
// drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
|
||||||
let drain_output_len = serialize(drain_script).len() + 8usize;
|
let drain_output_len = serialize(drain_script).len() + 8usize;
|
||||||
let change_fee = fee_rate.fee_vb(drain_output_len);
|
let change_fee =
|
||||||
|
(fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat();
|
||||||
let drain_val = remaining_amount.saturating_sub(change_fee);
|
let drain_val = remaining_amount.saturating_sub(change_fee);
|
||||||
|
|
||||||
if drain_val.is_dust(drain_script) {
|
if drain_val.is_dust(drain_script) {
|
||||||
@@ -344,10 +343,13 @@ fn select_sorted_utxos(
|
|||||||
(&mut selected_amount, &mut fee_amount),
|
(&mut selected_amount, &mut fee_amount),
|
||||||
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||||
if must_use || **selected_amount < target_amount + **fee_amount {
|
if must_use || **selected_amount < target_amount + **fee_amount {
|
||||||
**fee_amount += fee_rate.fee_wu(Weight::from_wu(
|
**fee_amount += (fee_rate
|
||||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
* Weight::from_wu(
|
||||||
));
|
TxIn::default().segwit_weight().to_wu()
|
||||||
**selected_amount += weighted_utxo.utxo.txout().value;
|
+ weighted_utxo.satisfaction_weight as u64,
|
||||||
|
))
|
||||||
|
.to_sat();
|
||||||
|
**selected_amount += weighted_utxo.utxo.txout().value.to_sat();
|
||||||
Some(weighted_utxo.utxo)
|
Some(weighted_utxo.utxo)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -387,10 +389,12 @@ struct OutputGroup {
|
|||||||
|
|
||||||
impl OutputGroup {
|
impl OutputGroup {
|
||||||
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||||
let fee = fee_rate.fee_wu(Weight::from_wu(
|
let fee = (fee_rate
|
||||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
* Weight::from_wu(
|
||||||
));
|
TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64,
|
||||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
))
|
||||||
|
.to_sat();
|
||||||
|
let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64;
|
||||||
OutputGroup {
|
OutputGroup {
|
||||||
weighted_utxo,
|
weighted_utxo,
|
||||||
fee,
|
fee,
|
||||||
@@ -456,7 +460,8 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
|
|||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.effective_value);
|
.fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
|
let cost_of_change =
|
||||||
|
(Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat();
|
||||||
|
|
||||||
// `curr_value` and `curr_available_value` are both the sum of *effective_values* of
|
// `curr_value` and `curr_available_value` are both the sum of *effective_values* of
|
||||||
// the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
|
// the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
|
||||||
@@ -480,7 +485,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
|
|||||||
.chain(optional_utxos.iter())
|
.chain(optional_utxos.iter())
|
||||||
.fold((0, 0), |(mut fees, mut value), utxo| {
|
.fold((0, 0), |(mut fees, mut value), utxo| {
|
||||||
fees += utxo.fee;
|
fees += utxo.fee;
|
||||||
value += utxo.weighted_utxo.utxo.txout().value;
|
value += utxo.weighted_utxo.utxo.txout().value.to_sat();
|
||||||
|
|
||||||
(fees, value)
|
(fees, value)
|
||||||
});
|
});
|
||||||
@@ -547,7 +552,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
mut curr_value: i64,
|
mut curr_value: i64,
|
||||||
mut curr_available_value: i64,
|
mut curr_available_value: i64,
|
||||||
target_amount: i64,
|
target_amount: i64,
|
||||||
cost_of_change: f32,
|
cost_of_change: u64,
|
||||||
drain_script: &Script,
|
drain_script: &Script,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
) -> Result<CoinSelectionResult, Error> {
|
) -> Result<CoinSelectionResult, Error> {
|
||||||
@@ -584,7 +589,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
// If we found a solution better than the previous one, or if there wasn't previous
|
// If we found a solution better than the previous one, or if there wasn't previous
|
||||||
// solution, update the best solution
|
// solution, update the best solution
|
||||||
if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
|
if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
|
||||||
best_selection = current_selection.clone();
|
best_selection.clone_from(¤t_selection);
|
||||||
best_selection_value = Some(curr_value);
|
best_selection_value = Some(curr_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,20 +743,19 @@ mod test {
|
|||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
use bdk_chain::ConfirmationTime;
|
use bdk_chain::ConfirmationTime;
|
||||||
use bitcoin::{OutPoint, ScriptBuf, TxOut};
|
use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use crate::wallet::coin_selection::filter_duplicates;
|
use crate::wallet::coin_selection::filter_duplicates;
|
||||||
use crate::wallet::Vbytes;
|
|
||||||
|
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use rand::{Rng, RngCore, SeedableRng};
|
use rand::{Rng, RngCore, SeedableRng};
|
||||||
|
|
||||||
// n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU)
|
// signature len (1WU) + signature and sighash (72WU)
|
||||||
// + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU)
|
// + pubkey len (1WU) + pubkey (33WU)
|
||||||
const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4;
|
const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33;
|
||||||
|
|
||||||
const FEE_AMOUNT: u64 = 50;
|
const FEE_AMOUNT: u64 = 50;
|
||||||
|
|
||||||
@@ -767,7 +771,7 @@ mod test {
|
|||||||
utxo: Utxo::Local(LocalOutput {
|
utxo: Utxo::Local(LocalOutput {
|
||||||
outpoint,
|
outpoint,
|
||||||
txout: TxOut {
|
txout: TxOut {
|
||||||
value,
|
value: Amount::from_sat(value),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
@@ -831,7 +835,7 @@ mod test {
|
|||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
txout: TxOut {
|
txout: TxOut {
|
||||||
value: rng.gen_range(0..200000000),
|
value: Amount::from_sat(rng.gen_range(0..200000000)),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
@@ -862,7 +866,7 @@ mod test {
|
|||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
txout: TxOut {
|
txout: TxOut {
|
||||||
value: utxos_value,
|
value: Amount::from_sat(utxos_value),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
@@ -879,7 +883,7 @@ mod test {
|
|||||||
utxos.shuffle(&mut rng);
|
utxos.shuffle(&mut rng);
|
||||||
utxos[..utxos_picked_len]
|
utxos[..utxos_picked_len]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|u| u.utxo.txout().value)
|
.map(|u| u.utxo.txout().value.to_sat())
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,7 +897,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -914,7 +918,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -935,7 +939,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -957,7 +961,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -975,7 +979,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1000.0),
|
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -992,7 +996,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1013,7 +1017,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1034,7 +1038,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1056,7 +1060,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1068,14 +1072,18 @@ mod test {
|
|||||||
fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
|
fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
|
||||||
let utxos = get_oldest_first_test_utxos();
|
let utxos = get_oldest_first_test_utxos();
|
||||||
|
|
||||||
let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
|
let target_amount: u64 = utxos
|
||||||
|
.iter()
|
||||||
|
.map(|wu| wu.utxo.txout().value.to_sat())
|
||||||
|
.sum::<u64>()
|
||||||
|
- 50;
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
|
|
||||||
OldestFirstCoinSelection
|
OldestFirstCoinSelection
|
||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1000.0),
|
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1096,7 +1104,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1117,7 +1125,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos.clone(),
|
utxos.clone(),
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1138,7 +1146,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1163,9 +1171,9 @@ mod test {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
|
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
|
||||||
let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
|
let amount: u64 = required.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
|
||||||
assert_eq!(amount, 100_000);
|
assert_eq!(amount, 100_000);
|
||||||
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
|
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
|
||||||
assert!(amount > 150_000);
|
assert!(amount > 150_000);
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
|
|
||||||
@@ -1175,7 +1183,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
required,
|
required,
|
||||||
optional,
|
optional,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1197,7 +1205,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb_unchecked(1),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1215,7 +1223,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1000.0),
|
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1227,22 +1235,19 @@ mod test {
|
|||||||
let utxos = get_test_utxos();
|
let utxos = get_test_utxos();
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
let target_amount = 99932; // first utxo's effective value
|
let target_amount = 99932; // first utxo's effective value
|
||||||
|
let feerate = FeeRate::BROADCAST_MIN;
|
||||||
|
|
||||||
let result = BranchAndBoundCoinSelection::new(0)
|
let result = BranchAndBoundCoinSelection::new(0)
|
||||||
.coin_select(
|
.coin_select(vec![], utxos, feerate, target_amount, &drain_script)
|
||||||
vec![],
|
|
||||||
utxos,
|
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
|
||||||
target_amount,
|
|
||||||
&drain_script,
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 1);
|
assert_eq!(result.selected.len(), 1);
|
||||||
assert_eq!(result.selected_amount(), 100_000);
|
assert_eq!(result.selected_amount(), 100_000);
|
||||||
let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes();
|
let input_weight =
|
||||||
|
TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64;
|
||||||
// the final fee rate should be exactly the same as the fee rate given
|
// the final fee rate should be exactly the same as the fee rate given
|
||||||
assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
|
let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight);
|
||||||
|
assert_eq!(result_feerate, feerate);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1258,7 +1263,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
optional_utxos,
|
optional_utxos,
|
||||||
FeeRate::from_sat_per_vb(0.0),
|
FeeRate::ZERO,
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1270,7 +1275,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "BnBNoExactMatch")]
|
#[should_panic(expected = "BnBNoExactMatch")]
|
||||||
fn test_bnb_function_no_exact_match() {
|
fn test_bnb_function_no_exact_match() {
|
||||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
|
||||||
let utxos: Vec<OutputGroup> = get_test_utxos()
|
let utxos: Vec<OutputGroup> = get_test_utxos()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
@@ -1279,7 +1284,7 @@ mod test {
|
|||||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let size_of_change = 31;
|
let size_of_change = 31;
|
||||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
||||||
|
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
let target_amount = 20_000 + FEE_AMOUNT;
|
let target_amount = 20_000 + FEE_AMOUNT;
|
||||||
@@ -1300,7 +1305,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "BnBTotalTriesExceeded")]
|
#[should_panic(expected = "BnBTotalTriesExceeded")]
|
||||||
fn test_bnb_function_tries_exceeded() {
|
fn test_bnb_function_tries_exceeded() {
|
||||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
|
||||||
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
@@ -1309,7 +1314,7 @@ mod test {
|
|||||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let size_of_change = 31;
|
let size_of_change = 31;
|
||||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
||||||
let target_amount = 20_000 + FEE_AMOUNT;
|
let target_amount = 20_000 + FEE_AMOUNT;
|
||||||
|
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
@@ -1331,9 +1336,9 @@ mod test {
|
|||||||
// The match won't be exact but still in the range
|
// The match won't be exact but still in the range
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bnb_function_almost_exact_match_with_fees() {
|
fn test_bnb_function_almost_exact_match_with_fees() {
|
||||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||||
let size_of_change = 31;
|
let size_of_change = 31;
|
||||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
||||||
|
|
||||||
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1346,7 +1351,7 @@ mod test {
|
|||||||
|
|
||||||
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
||||||
// cost_of_change + 5.
|
// cost_of_change + 5.
|
||||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
|
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5;
|
||||||
|
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
|
|
||||||
@@ -1371,7 +1376,7 @@ mod test {
|
|||||||
fn test_bnb_function_exact_match_more_utxos() {
|
fn test_bnb_function_exact_match_more_utxos() {
|
||||||
let seed = [0; 32];
|
let seed = [0; 32];
|
||||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||||
let fee_rate = FeeRate::from_sat_per_vb(0.0);
|
let fee_rate = FeeRate::ZERO;
|
||||||
|
|
||||||
for _ in 0..200 {
|
for _ in 0..200 {
|
||||||
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
||||||
@@ -1397,7 +1402,7 @@ mod test {
|
|||||||
curr_value,
|
curr_value,
|
||||||
curr_available_value,
|
curr_available_value,
|
||||||
target_amount,
|
target_amount,
|
||||||
0.0,
|
0,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
fee_rate,
|
fee_rate,
|
||||||
)
|
)
|
||||||
@@ -1413,7 +1418,7 @@ mod test {
|
|||||||
let mut utxos = generate_random_utxos(&mut rng, 300);
|
let mut utxos = generate_random_utxos(&mut rng, 300);
|
||||||
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
||||||
|
|
||||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||||
let utxos: Vec<OutputGroup> = utxos
|
let utxos: Vec<OutputGroup> = utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
@@ -1442,7 +1447,7 @@ mod test {
|
|||||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(10.0),
|
FeeRate::from_sat_per_vb_unchecked(10),
|
||||||
500_000,
|
500_000,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
);
|
);
|
||||||
@@ -1461,14 +1466,14 @@ mod test {
|
|||||||
let utxos = get_test_utxos();
|
let utxos = get_test_utxos();
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
|
|
||||||
let (required, optional) = utxos
|
let (required, optional) = utxos.into_iter().partition(
|
||||||
.into_iter()
|
|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000),
|
||||||
.partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
|
);
|
||||||
|
|
||||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||||
required,
|
required,
|
||||||
optional,
|
optional,
|
||||||
FeeRate::from_sat_per_vb(10.0),
|
FeeRate::from_sat_per_vb_unchecked(10),
|
||||||
500_000,
|
500_000,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
);
|
);
|
||||||
@@ -1490,7 +1495,7 @@ mod test {
|
|||||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb(10_000.0),
|
FeeRate::from_sat_per_vb_unchecked(10_000),
|
||||||
500_000,
|
500_000,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
);
|
);
|
||||||
@@ -1512,7 +1517,7 @@ mod test {
|
|||||||
utxo: Utxo::Local(LocalOutput {
|
utxo: Utxo::Local(LocalOutput {
|
||||||
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
|
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
|
||||||
txout: TxOut {
|
txout: TxOut {
|
||||||
value,
|
value: Amount::from_sat(value),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
use crate::descriptor::policy::PolicyError;
|
use crate::descriptor::policy::PolicyError;
|
||||||
use crate::descriptor::DescriptorError;
|
use crate::descriptor::DescriptorError;
|
||||||
use crate::wallet::coin_selection;
|
use crate::wallet::coin_selection;
|
||||||
use crate::{descriptor, FeeRate, KeychainKind};
|
use crate::{descriptor, KeychainKind};
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
|
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
@@ -83,8 +83,8 @@ pub enum CreateTxError<P> {
|
|||||||
},
|
},
|
||||||
/// When bumping a tx the fee rate requested is lower than required
|
/// When bumping a tx the fee rate requested is lower than required
|
||||||
FeeRateTooLow {
|
FeeRateTooLow {
|
||||||
/// Required fee rate (satoshi/vbyte)
|
/// Required fee rate
|
||||||
required: FeeRate,
|
required: bitcoin::FeeRate,
|
||||||
},
|
},
|
||||||
/// `manually_selected_only` option is selected but no utxo has been passed
|
/// `manually_selected_only` option is selected but no utxo has been passed
|
||||||
NoUtxosSelected,
|
NoUtxosSelected,
|
||||||
@@ -168,8 +168,10 @@ where
|
|||||||
CreateTxError::FeeRateTooLow { required } => {
|
CreateTxError::FeeRateTooLow { required } => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Fee rate too low: required {} sat/vbyte",
|
// Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
|
||||||
required.as_sat_per_vb()
|
//"Fee rate too low: required {required:#}"
|
||||||
|
"Fee rate too low: required {} sat/vb",
|
||||||
|
crate::floating_rate!(required)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CreateTxError::NoUtxosSelected => {
|
CreateTxError::NoUtxosSelected => {
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ mod test {
|
|||||||
|
|
||||||
use bdk_chain::{BlockId, ConfirmationTime};
|
use bdk_chain::{BlockId, ConfirmationTime};
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::{BlockHash, Network, Transaction};
|
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::wallet::Wallet;
|
use crate::wallet::Wallet;
|
||||||
@@ -230,7 +230,7 @@ mod test {
|
|||||||
let transaction = Transaction {
|
let transaction = Transaction {
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
version: 0,
|
version: transaction::Version::non_standard(0),
|
||||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||||
};
|
};
|
||||||
wallet
|
wallet
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
//! # use bdk::signer::SignerOrdering;
|
//! # use bdk::signer::SignerOrdering;
|
||||||
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||||
//! # use bdk::wallet::AddressIndex::New;
|
//! # use bdk::wallet::AddressIndex::New;
|
||||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||||
//! # use hwi::HWIClient;
|
//! # use hwi::HWIClient;
|
||||||
//! # use std::sync::Arc;
|
//! # use std::sync::Arc;
|
||||||
//! #
|
//! #
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use bitcoin::bip32::Fingerprint;
|
use bitcoin::bip32::Fingerprint;
|
||||||
use bitcoin::psbt::PartiallySignedTransaction;
|
|
||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
|
use bitcoin::Psbt;
|
||||||
|
|
||||||
use hwi::error::Error;
|
use hwi::error::Error;
|
||||||
use hwi::types::{HWIChain, HWIDevice};
|
use hwi::types::{HWIChain, HWIDevice};
|
||||||
@@ -87,7 +87,7 @@ impl SignerCommon for HWISigner {
|
|||||||
impl TransactionSigner for HWISigner {
|
impl TransactionSigner for HWISigner {
|
||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
_sign_options: &crate::SignOptions,
|
_sign_options: &crate::SignOptions,
|
||||||
_secp: &crate::wallet::utils::SecpCtx,
|
_secp: &crate::wallet::utils::SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ use bdk_chain::{
|
|||||||
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
|
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
|
||||||
IndexedTxGraph, Persist, PersistBackend,
|
IndexedTxGraph, Persist, PersistBackend,
|
||||||
};
|
};
|
||||||
|
use bitcoin::constants::genesis_block;
|
||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
absolute, Address, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut,
|
absolute, psbt, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence,
|
||||||
Txid, Weight, Witness,
|
Transaction, TxOut, Txid, Witness,
|
||||||
};
|
};
|
||||||
use bitcoin::{consensus::encode::serialize, BlockHash};
|
use bitcoin::{consensus::encode::serialize, transaction, Amount, BlockHash, Psbt};
|
||||||
use bitcoin::{constants::genesis_block, psbt};
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use descriptor::error::Error as DescriptorError;
|
use descriptor::error::Error as DescriptorError;
|
||||||
@@ -54,7 +54,6 @@ pub(crate) mod utils;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub use utils::IsDust;
|
pub use utils::IsDust;
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
use coin_selection::DefaultCoinSelectionAlgorithm;
|
use coin_selection::DefaultCoinSelectionAlgorithm;
|
||||||
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
|
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
|
||||||
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
|
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
|
||||||
@@ -942,15 +941,15 @@ impl<D> Wallet<D> {
|
|||||||
/// # let mut wallet: Wallet<()> = todo!();
|
/// # let mut wallet: Wallet<()> = todo!();
|
||||||
/// # let txid:Txid = todo!();
|
/// # let txid:Txid = todo!();
|
||||||
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
|
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
|
||||||
/// let fee = wallet.calculate_fee(tx).expect("fee");
|
/// let fee = wallet.calculate_fee(&tx).expect("fee");
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```rust, no_run
|
/// ```rust, no_run
|
||||||
/// # use bitcoin::psbt::PartiallySignedTransaction;
|
/// # use bitcoin::Psbt;
|
||||||
/// # use bdk::Wallet;
|
/// # use bdk::Wallet;
|
||||||
/// # let mut wallet: Wallet<()> = todo!();
|
/// # let mut wallet: Wallet<()> = todo!();
|
||||||
/// # let mut psbt: PartiallySignedTransaction = todo!();
|
/// # let mut psbt: Psbt = todo!();
|
||||||
/// let tx = &psbt.clone().extract_tx();
|
/// let tx = &psbt.clone().extract_tx().expect("tx");
|
||||||
/// let fee = wallet.calculate_fee(tx).expect("fee");
|
/// let fee = wallet.calculate_fee(tx).expect("fee");
|
||||||
/// ```
|
/// ```
|
||||||
/// [`insert_txout`]: Self::insert_txout
|
/// [`insert_txout`]: Self::insert_txout
|
||||||
@@ -973,23 +972,21 @@ impl<D> Wallet<D> {
|
|||||||
/// # let mut wallet: Wallet<()> = todo!();
|
/// # let mut wallet: Wallet<()> = todo!();
|
||||||
/// # let txid:Txid = todo!();
|
/// # let txid:Txid = todo!();
|
||||||
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
|
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
|
||||||
/// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate");
|
/// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate");
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```rust, no_run
|
/// ```rust, no_run
|
||||||
/// # use bitcoin::psbt::PartiallySignedTransaction;
|
/// # use bitcoin::Psbt;
|
||||||
/// # use bdk::Wallet;
|
/// # use bdk::Wallet;
|
||||||
/// # let mut wallet: Wallet<()> = todo!();
|
/// # let mut wallet: Wallet<()> = todo!();
|
||||||
/// # let mut psbt: PartiallySignedTransaction = todo!();
|
/// # let mut psbt: Psbt = todo!();
|
||||||
/// let tx = &psbt.clone().extract_tx();
|
/// let tx = &psbt.clone().extract_tx().expect("tx");
|
||||||
/// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate");
|
/// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate");
|
||||||
/// ```
|
/// ```
|
||||||
/// [`insert_txout`]: Self::insert_txout
|
/// [`insert_txout`]: Self::insert_txout
|
||||||
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
|
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
|
||||||
self.calculate_fee(tx).map(|fee| {
|
self.calculate_fee(tx)
|
||||||
let weight = tx.weight();
|
.map(|fee| bitcoin::Amount::from_sat(fee) / tx.weight())
|
||||||
FeeRate::from_wu(fee, weight)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the `tx`'s sent and received amounts (in satoshis).
|
/// Compute the `tx`'s sent and received amounts (in satoshis).
|
||||||
@@ -1005,16 +1002,16 @@ impl<D> Wallet<D> {
|
|||||||
/// # use bdk::Wallet;
|
/// # use bdk::Wallet;
|
||||||
/// # let mut wallet: Wallet<()> = todo!();
|
/// # let mut wallet: Wallet<()> = todo!();
|
||||||
/// # let txid:Txid = todo!();
|
/// # let txid:Txid = todo!();
|
||||||
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
|
/// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx;
|
||||||
/// let (sent, received) = wallet.sent_and_received(tx);
|
/// let (sent, received) = wallet.sent_and_received(&tx);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```rust, no_run
|
/// ```rust, no_run
|
||||||
/// # use bitcoin::psbt::PartiallySignedTransaction;
|
/// # use bitcoin::Psbt;
|
||||||
/// # use bdk::Wallet;
|
/// # use bdk::Wallet;
|
||||||
/// # let mut wallet: Wallet<()> = todo!();
|
/// # let mut wallet: Wallet<()> = todo!();
|
||||||
/// # let mut psbt: PartiallySignedTransaction = todo!();
|
/// # let mut psbt: Psbt = todo!();
|
||||||
/// let tx = &psbt.clone().extract_tx();
|
/// let tx = &psbt.clone().extract_tx().expect("tx");
|
||||||
/// let (sent, received) = wallet.sent_and_received(tx);
|
/// let (sent, received) = wallet.sent_and_received(tx);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
|
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
|
||||||
@@ -1067,7 +1064,7 @@ impl<D> Wallet<D> {
|
|||||||
pub fn get_tx(
|
pub fn get_tx(
|
||||||
&self,
|
&self,
|
||||||
txid: Txid,
|
txid: Txid,
|
||||||
) -> Option<CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> {
|
) -> Option<CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
|
||||||
let graph = self.indexed_graph.graph();
|
let graph = self.indexed_graph.graph();
|
||||||
|
|
||||||
Some(CanonicalTx {
|
Some(CanonicalTx {
|
||||||
@@ -1130,18 +1127,14 @@ impl<D> Wallet<D> {
|
|||||||
// anchor tx to checkpoint with lowest height that is >= position's height
|
// anchor tx to checkpoint with lowest height that is >= position's height
|
||||||
let anchor = self
|
let anchor = self
|
||||||
.chain
|
.chain
|
||||||
.blocks()
|
|
||||||
.range(height..)
|
.range(height..)
|
||||||
.next()
|
.last()
|
||||||
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
|
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
|
||||||
tip_height: self.chain.tip().height(),
|
tip_height: self.chain.tip().height(),
|
||||||
tx_height: height,
|
tx_height: height,
|
||||||
})
|
})
|
||||||
.map(|(&anchor_height, &hash)| ConfirmationTimeHeightAnchor {
|
.map(|anchor_cp| ConfirmationTimeHeightAnchor {
|
||||||
anchor_block: BlockId {
|
anchor_block: anchor_cp.block_id(),
|
||||||
height: anchor_height,
|
|
||||||
hash,
|
|
||||||
},
|
|
||||||
confirmation_height: height,
|
confirmation_height: height,
|
||||||
confirmation_time: time,
|
confirmation_time: time,
|
||||||
})?;
|
})?;
|
||||||
@@ -1169,7 +1162,8 @@ impl<D> Wallet<D> {
|
|||||||
/// Iterate over the transactions in the wallet.
|
/// Iterate over the transactions in the wallet.
|
||||||
pub fn transactions(
|
pub fn transactions(
|
||||||
&self,
|
&self,
|
||||||
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> + '_ {
|
) -> impl Iterator<Item = CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> + '_
|
||||||
|
{
|
||||||
self.indexed_graph
|
self.indexed_graph
|
||||||
.graph()
|
.graph()
|
||||||
.list_chain_txs(&self.chain, self.chain.tip().block_id())
|
.list_chain_txs(&self.chain, self.chain.tip().block_id())
|
||||||
@@ -1267,7 +1261,7 @@ impl<D> Wallet<D> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
coin_selection: Cs,
|
coin_selection: Cs,
|
||||||
params: TxParams,
|
params: TxParams,
|
||||||
) -> Result<psbt::PartiallySignedTransaction, CreateTxError<D::WriteError>>
|
) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||||
where
|
where
|
||||||
D: PersistBackend<ChangeSet>,
|
D: PersistBackend<ChangeSet>,
|
||||||
{
|
{
|
||||||
@@ -1432,37 +1426,36 @@ impl<D> Wallet<D> {
|
|||||||
(Some(rbf), _) => rbf.get_value(),
|
(Some(rbf), _) => rbf.get_value(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (fee_rate, mut fee_amount) = match params
|
let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() {
|
||||||
.fee_policy
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&FeePolicy::FeeRate(FeeRate::default()))
|
|
||||||
{
|
|
||||||
//FIXME: see https://github.com/bitcoindevkit/bdk/issues/256
|
//FIXME: see https://github.com/bitcoindevkit/bdk/issues/256
|
||||||
FeePolicy::FeeAmount(fee) => {
|
FeePolicy::FeeAmount(fee) => {
|
||||||
if let Some(previous_fee) = params.bumping_fee {
|
if let Some(previous_fee) = params.bumping_fee {
|
||||||
if *fee < previous_fee.absolute {
|
if fee < previous_fee.absolute {
|
||||||
return Err(CreateTxError::FeeTooLow {
|
return Err(CreateTxError::FeeTooLow {
|
||||||
required: previous_fee.absolute,
|
required: previous_fee.absolute,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(FeeRate::from_sat_per_vb(0.0), *fee)
|
(FeeRate::ZERO, fee)
|
||||||
}
|
}
|
||||||
FeePolicy::FeeRate(rate) => {
|
FeePolicy::FeeRate(rate) => {
|
||||||
if let Some(previous_fee) = params.bumping_fee {
|
if let Some(previous_fee) = params.bumping_fee {
|
||||||
let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0);
|
let required_feerate = FeeRate::from_sat_per_kwu(
|
||||||
if *rate < required_feerate {
|
previous_fee.rate.to_sat_per_kwu()
|
||||||
|
+ FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb
|
||||||
|
);
|
||||||
|
if rate < required_feerate {
|
||||||
return Err(CreateTxError::FeeRateTooLow {
|
return Err(CreateTxError::FeeRateTooLow {
|
||||||
required: required_feerate,
|
required: required_feerate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(*rate, 0)
|
(rate, 0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut tx = Transaction {
|
let mut tx = Transaction {
|
||||||
version,
|
version: transaction::Version::non_standard(version),
|
||||||
lock_time,
|
lock_time,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
@@ -1492,7 +1485,7 @@ impl<D> Wallet<D> {
|
|||||||
|
|
||||||
let new_out = TxOut {
|
let new_out = TxOut {
|
||||||
script_pubkey: script_pubkey.clone(),
|
script_pubkey: script_pubkey.clone(),
|
||||||
value,
|
value: Amount::from_sat(value),
|
||||||
};
|
};
|
||||||
|
|
||||||
tx.output.push(new_out);
|
tx.output.push(new_out);
|
||||||
@@ -1500,18 +1493,7 @@ impl<D> Wallet<D> {
|
|||||||
outgoing += value;
|
outgoing += value;
|
||||||
}
|
}
|
||||||
|
|
||||||
fee_amount += fee_rate.fee_wu(tx.weight());
|
fee_amount += (fee_rate * tx.weight()).to_sat();
|
||||||
|
|
||||||
// Segwit transactions' header is 2WU larger than legacy txs' header,
|
|
||||||
// as they contain a witness marker (1WU) and a witness flag (1WU) (see BIP144).
|
|
||||||
// At this point we really don't know if the resulting transaction will be segwit
|
|
||||||
// or legacy, so we just add this 2WU to the fee_amount - overshooting the fee amount
|
|
||||||
// is better than undershooting it.
|
|
||||||
// If we pass a fee_amount that is slightly higher than the final fee_amount, we
|
|
||||||
// end up with a transaction with a slightly higher fee rate than the requested one.
|
|
||||||
// If, instead, we undershoot, we may end up with a feerate lower than the requested one
|
|
||||||
// - we might come up with non broadcastable txs!
|
|
||||||
fee_amount += fee_rate.fee_wu(Weight::from_wu(2));
|
|
||||||
|
|
||||||
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
|
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
|
||||||
&& internal_descriptor.is_none()
|
&& internal_descriptor.is_none()
|
||||||
@@ -1601,7 +1583,7 @@ impl<D> Wallet<D> {
|
|||||||
|
|
||||||
// create drain output
|
// create drain output
|
||||||
let drain_output = TxOut {
|
let drain_output = TxOut {
|
||||||
value: *amount,
|
value: Amount::from_sat(*amount),
|
||||||
script_pubkey: drain_script,
|
script_pubkey: drain_script,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1647,12 +1629,12 @@ impl<D> Wallet<D> {
|
|||||||
/// builder.finish()?
|
/// builder.finish()?
|
||||||
/// };
|
/// };
|
||||||
/// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
|
/// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||||
/// let tx = psbt.extract_tx();
|
/// let tx = psbt.clone().extract_tx().expect("tx");
|
||||||
/// // broadcast tx but it's taking too long to confirm so we want to bump the fee
|
/// // broadcast tx but it's taking too long to confirm so we want to bump the fee
|
||||||
/// let mut psbt = {
|
/// let mut psbt = {
|
||||||
/// let mut builder = wallet.build_fee_bump(tx.txid())?;
|
/// let mut builder = wallet.build_fee_bump(tx.txid())?;
|
||||||
/// builder
|
/// builder
|
||||||
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0));
|
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"));
|
||||||
/// builder.finish()?
|
/// builder.finish()?
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
@@ -1673,6 +1655,7 @@ impl<D> Wallet<D> {
|
|||||||
let mut tx = graph
|
let mut tx = graph
|
||||||
.get_tx(txid)
|
.get_tx(txid)
|
||||||
.ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
|
.ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
|
||||||
|
.as_ref()
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let pos = graph
|
let pos = graph
|
||||||
@@ -1715,10 +1698,9 @@ impl<D> Wallet<D> {
|
|||||||
|
|
||||||
let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) {
|
let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) {
|
||||||
Some((keychain, derivation_index)) => {
|
Some((keychain, derivation_index)) => {
|
||||||
#[allow(deprecated)]
|
|
||||||
let satisfaction_weight = self
|
let satisfaction_weight = self
|
||||||
.get_descriptor_for_keychain(keychain)
|
.get_descriptor_for_keychain(keychain)
|
||||||
.max_satisfaction_weight()
|
.max_weight_to_satisfy()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
WeightedUtxo {
|
WeightedUtxo {
|
||||||
utxo: Utxo::Local(LocalOutput {
|
utxo: Utxo::Local(LocalOutput {
|
||||||
@@ -1736,16 +1718,16 @@ impl<D> Wallet<D> {
|
|||||||
let satisfaction_weight =
|
let satisfaction_weight =
|
||||||
serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len();
|
serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len();
|
||||||
WeightedUtxo {
|
WeightedUtxo {
|
||||||
satisfaction_weight,
|
|
||||||
utxo: Utxo::Foreign {
|
utxo: Utxo::Foreign {
|
||||||
outpoint: txin.previous_output,
|
outpoint: txin.previous_output,
|
||||||
sequence: Some(txin.sequence),
|
sequence: Some(txin.sequence),
|
||||||
psbt_input: Box::new(psbt::Input {
|
psbt_input: Box::new(psbt::Input {
|
||||||
witness_utxo: Some(txout.clone()),
|
witness_utxo: Some(txout.clone()),
|
||||||
non_witness_utxo: Some(prev_tx.clone()),
|
non_witness_utxo: Some(prev_tx.as_ref().clone()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
satisfaction_weight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1771,16 +1753,16 @@ impl<D> Wallet<D> {
|
|||||||
|
|
||||||
let params = TxParams {
|
let params = TxParams {
|
||||||
// TODO: figure out what rbf option should be?
|
// TODO: figure out what rbf option should be?
|
||||||
version: Some(tx_builder::Version(tx.version)),
|
version: Some(tx_builder::Version(tx.version.0)),
|
||||||
recipients: tx
|
recipients: tx
|
||||||
.output
|
.output
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|txout| (txout.script_pubkey, txout.value))
|
.map(|txout| (txout.script_pubkey, txout.value.to_sat()))
|
||||||
.collect(),
|
.collect(),
|
||||||
utxos: original_utxos,
|
utxos: original_utxos,
|
||||||
bumping_fee: Some(tx_builder::PreviousFee {
|
bumping_fee: Some(tx_builder::PreviousFee {
|
||||||
absolute: fee,
|
absolute: fee,
|
||||||
rate: fee_rate.as_sat_per_vb(),
|
rate: fee_rate,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1821,11 +1803,7 @@ impl<D> Wallet<D> {
|
|||||||
/// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
/// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||||
/// assert!(finalized, "we should have signed all the inputs");
|
/// assert!(finalized, "we should have signed all the inputs");
|
||||||
/// # Ok::<(),anyhow::Error>(())
|
/// # Ok::<(),anyhow::Error>(())
|
||||||
pub fn sign(
|
pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result<bool, SignerError> {
|
||||||
&self,
|
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
|
||||||
sign_options: SignOptions,
|
|
||||||
) -> Result<bool, SignerError> {
|
|
||||||
// This adds all the PSBT metadata for the inputs, which will help us later figure out how
|
// This adds all the PSBT metadata for the inputs, which will help us later figure out how
|
||||||
// to derive our keys
|
// to derive our keys
|
||||||
self.update_psbt_with_descriptor(psbt)
|
self.update_psbt_with_descriptor(psbt)
|
||||||
@@ -1905,7 +1883,7 @@ impl<D> Wallet<D> {
|
|||||||
/// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
|
/// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
|
||||||
pub fn finalize_psbt(
|
pub fn finalize_psbt(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
sign_options: SignOptions,
|
sign_options: SignOptions,
|
||||||
) -> Result<bool, SignerError> {
|
) -> Result<bool, SignerError> {
|
||||||
let chain_tip = self.chain.tip().block_id();
|
let chain_tip = self.chain.tip().block_id();
|
||||||
@@ -1974,6 +1952,15 @@ impl<D> Wallet<D> {
|
|||||||
if sign_options.remove_partial_sigs {
|
if sign_options.remove_partial_sigs {
|
||||||
psbt_input.partial_sigs.clear();
|
psbt_input.partial_sigs.clear();
|
||||||
}
|
}
|
||||||
|
if sign_options.remove_taproot_extras {
|
||||||
|
// We just constructed the final witness, clear these fields.
|
||||||
|
psbt_input.tap_key_sig = None;
|
||||||
|
psbt_input.tap_script_sigs.clear();
|
||||||
|
psbt_input.tap_scripts.clear();
|
||||||
|
psbt_input.tap_key_origins.clear();
|
||||||
|
psbt_input.tap_internal_key = None;
|
||||||
|
psbt_input.tap_merkle_root = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(_) => finished = false,
|
Err(_) => finished = false,
|
||||||
}
|
}
|
||||||
@@ -1982,6 +1969,12 @@ impl<D> Wallet<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if finished && sign_options.remove_taproot_extras {
|
||||||
|
for output in &mut psbt.outputs {
|
||||||
|
output.tap_key_origins.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(finished)
|
Ok(finished)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2045,13 +2038,11 @@ impl<D> Wallet<D> {
|
|||||||
self.list_unspent()
|
self.list_unspent()
|
||||||
.map(|utxo| {
|
.map(|utxo| {
|
||||||
let keychain = utxo.keychain;
|
let keychain = utxo.keychain;
|
||||||
#[allow(deprecated)]
|
(utxo, {
|
||||||
(
|
|
||||||
utxo,
|
|
||||||
self.get_descriptor_for_keychain(keychain)
|
self.get_descriptor_for_keychain(keychain)
|
||||||
.max_satisfaction_weight()
|
.max_weight_to_satisfy()
|
||||||
.unwrap(),
|
.unwrap()
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -2118,7 +2109,7 @@ impl<D> Wallet<D> {
|
|||||||
if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() {
|
if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if tx.is_coin_base() {
|
if tx.is_coinbase() {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
confirmation_time.is_confirmed(),
|
confirmation_time.is_confirmed(),
|
||||||
"coinbase must always be confirmed"
|
"coinbase must always be confirmed"
|
||||||
@@ -2167,11 +2158,11 @@ impl<D> Wallet<D> {
|
|||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
selected: Vec<Utxo>,
|
selected: Vec<Utxo>,
|
||||||
params: TxParams,
|
params: TxParams,
|
||||||
) -> Result<psbt::PartiallySignedTransaction, CreateTxError<D::WriteError>>
|
) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||||
where
|
where
|
||||||
D: PersistBackend<ChangeSet>,
|
D: PersistBackend<ChangeSet>,
|
||||||
{
|
{
|
||||||
let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?;
|
let mut psbt = Psbt::from_unsigned_tx(tx)?;
|
||||||
|
|
||||||
if params.add_global_xpubs {
|
if params.add_global_xpubs {
|
||||||
let all_xpubs = self
|
let all_xpubs = self
|
||||||
@@ -2227,7 +2218,7 @@ impl<D> Wallet<D> {
|
|||||||
let is_taproot = foreign_psbt_input
|
let is_taproot = foreign_psbt_input
|
||||||
.witness_utxo
|
.witness_utxo
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|txout| txout.script_pubkey.is_v1_p2tr())
|
.map(|txout| txout.script_pubkey.is_p2tr())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if !is_taproot
|
if !is_taproot
|
||||||
&& !params.only_witness_utxo
|
&& !params.only_witness_utxo
|
||||||
@@ -2283,16 +2274,13 @@ impl<D> Wallet<D> {
|
|||||||
psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
|
psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
|
||||||
}
|
}
|
||||||
if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) {
|
if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) {
|
||||||
psbt_input.non_witness_utxo = Some(prev_tx.clone());
|
psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(psbt_input)
|
Ok(psbt_input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_psbt_with_descriptor(
|
fn update_psbt_with_descriptor(&self, psbt: &mut Psbt) -> Result<(), MiniscriptPsbtError> {
|
||||||
&self,
|
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
|
||||||
) -> Result<(), MiniscriptPsbtError> {
|
|
||||||
// We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
|
// We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
|
||||||
// the input utxos and outputs
|
// the input utxos and outputs
|
||||||
let utxos = (0..psbt.inputs.len())
|
let utxos = (0..psbt.inputs.len())
|
||||||
@@ -2566,6 +2554,17 @@ fn create_signers<E: IntoWalletDescriptor>(
|
|||||||
Ok((signers, change_signers))
|
Ok((signers, change_signers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
|
||||||
|
#[macro_export]
|
||||||
|
#[doc(hidden)]
|
||||||
|
macro_rules! floating_rate {
|
||||||
|
($rate:expr) => {{
|
||||||
|
use $crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
|
||||||
|
// sat_kwu / 250.0 -> sat_vb
|
||||||
|
$rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// Macro for getting a wallet for use in a doctest
|
/// Macro for getting a wallet for use in a doctest
|
||||||
@@ -2585,11 +2584,11 @@ macro_rules! doctest_wallet {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let address = wallet.get_address(AddressIndex::New).address;
|
let address = wallet.get_address(AddressIndex::New).address;
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 1,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 500_000,
|
value: Amount::from_sat(500_000),
|
||||||
script_pubkey: address.script_pubkey(),
|
script_pubkey: address.script_pubkey(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,13 +19,12 @@
|
|||||||
//! # use core::str::FromStr;
|
//! # use core::str::FromStr;
|
||||||
//! # use bitcoin::secp256k1::{Secp256k1, All};
|
//! # use bitcoin::secp256k1::{Secp256k1, All};
|
||||||
//! # use bitcoin::*;
|
//! # use bitcoin::*;
|
||||||
//! # use bitcoin::psbt;
|
|
||||||
//! # use bdk::signer::*;
|
//! # use bdk::signer::*;
|
||||||
//! # use bdk::*;
|
//! # use bdk::*;
|
||||||
//! # #[derive(Debug)]
|
//! # #[derive(Debug)]
|
||||||
//! # struct CustomHSM;
|
//! # struct CustomHSM;
|
||||||
//! # impl CustomHSM {
|
//! # impl CustomHSM {
|
||||||
//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
|
//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> {
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! # fn connect() -> Self {
|
//! # fn connect() -> Self {
|
||||||
@@ -55,7 +54,7 @@
|
|||||||
//! impl InputSigner for CustomSigner {
|
//! impl InputSigner for CustomSigner {
|
||||||
//! fn sign_input(
|
//! fn sign_input(
|
||||||
//! &self,
|
//! &self,
|
||||||
//! psbt: &mut psbt::PartiallySignedTransaction,
|
//! psbt: &mut Psbt,
|
||||||
//! input_index: usize,
|
//! input_index: usize,
|
||||||
//! _sign_options: &SignOptions,
|
//! _sign_options: &SignOptions,
|
||||||
//! _secp: &Secp256k1<All>,
|
//! _secp: &Secp256k1<All>,
|
||||||
@@ -87,13 +86,13 @@ use core::cmp::Ordering;
|
|||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::ops::{Bound::Included, Deref};
|
use core::ops::{Bound::Included, Deref};
|
||||||
|
|
||||||
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
|
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv};
|
||||||
use bitcoin::hashes::hash160;
|
use bitcoin::hashes::hash160;
|
||||||
use bitcoin::secp256k1::Message;
|
use bitcoin::secp256k1::Message;
|
||||||
use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
|
use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
|
||||||
use bitcoin::{ecdsa, psbt, sighash, taproot};
|
use bitcoin::{ecdsa, psbt, sighash, taproot};
|
||||||
use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
|
use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
|
||||||
use bitcoin::{PrivateKey, PublicKey};
|
use bitcoin::{PrivateKey, Psbt, PublicKey};
|
||||||
|
|
||||||
use miniscript::descriptor::{
|
use miniscript::descriptor::{
|
||||||
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
|
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
|
||||||
@@ -264,7 +263,7 @@ pub trait InputSigner: SignerCommon {
|
|||||||
/// Sign a single psbt input
|
/// Sign a single psbt input
|
||||||
fn sign_input(
|
fn sign_input(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
sign_options: &SignOptions,
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
@@ -279,7 +278,7 @@ pub trait TransactionSigner: SignerCommon {
|
|||||||
/// Sign all the inputs of the psbt
|
/// Sign all the inputs of the psbt
|
||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
sign_options: &SignOptions,
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError>;
|
) -> Result<(), SignerError>;
|
||||||
@@ -288,7 +287,7 @@ pub trait TransactionSigner: SignerCommon {
|
|||||||
impl<T: InputSigner> TransactionSigner for T {
|
impl<T: InputSigner> TransactionSigner for T {
|
||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
sign_options: &SignOptions,
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
@@ -300,7 +299,7 @@ impl<T: InputSigner> TransactionSigner for T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
|
||||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||||
SignerId::from(self.root_fingerprint(secp))
|
SignerId::from(self.root_fingerprint(secp))
|
||||||
}
|
}
|
||||||
@@ -310,10 +309,10 @@ impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
impl InputSigner for SignerWrapper<DescriptorXKey<Xpriv>> {
|
||||||
fn sign_input(
|
fn sign_input(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
sign_options: &SignOptions,
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
@@ -396,7 +395,7 @@ fn multikey_to_xkeys<K: InnerXKey + Clone>(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignerCommon for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
|
impl SignerCommon for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
|
||||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||||
SignerId::from(self.root_fingerprint(secp))
|
SignerId::from(self.root_fingerprint(secp))
|
||||||
}
|
}
|
||||||
@@ -406,10 +405,10 @@ impl SignerCommon for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputSigner for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
|
impl InputSigner for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
|
||||||
fn sign_input(
|
fn sign_input(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
sign_options: &SignOptions,
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
@@ -438,7 +437,7 @@ impl SignerCommon for SignerWrapper<PrivateKey> {
|
|||||||
impl InputSigner for SignerWrapper<PrivateKey> {
|
impl InputSigner for SignerWrapper<PrivateKey> {
|
||||||
fn sign_input(
|
fn sign_input(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
sign_options: &SignOptions,
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
@@ -577,7 +576,7 @@ fn sign_psbt_schnorr(
|
|||||||
hash_ty: TapSighashType,
|
hash_ty: TapSighashType,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) {
|
) {
|
||||||
let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
|
let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
|
||||||
let keypair = match leaf_hash {
|
let keypair = match leaf_hash {
|
||||||
None => keypair
|
None => keypair
|
||||||
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
||||||
@@ -782,6 +781,16 @@ pub struct SignOptions {
|
|||||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||||
pub remove_partial_sigs: bool,
|
pub remove_partial_sigs: bool,
|
||||||
|
|
||||||
|
/// Whether to remove taproot specific fields from the PSBT on finalization.
|
||||||
|
///
|
||||||
|
/// For inputs this includes the taproot internal key, merkle root, and individual
|
||||||
|
/// scripts and signatures. For both inputs and outputs it includes key origin info.
|
||||||
|
///
|
||||||
|
/// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
|
||||||
|
///
|
||||||
|
/// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
|
||||||
|
pub remove_taproot_extras: bool,
|
||||||
|
|
||||||
/// Whether to try finalizing the PSBT after the inputs are signed.
|
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||||
///
|
///
|
||||||
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
||||||
@@ -827,6 +836,7 @@ impl Default for SignOptions {
|
|||||||
assume_height: None,
|
assume_height: None,
|
||||||
allow_all_sighashes: false,
|
allow_all_sighashes: false,
|
||||||
remove_partial_sigs: true,
|
remove_partial_sigs: true,
|
||||||
|
remove_taproot_extras: true,
|
||||||
try_finalize: true,
|
try_finalize: true,
|
||||||
tap_leaves_options: TapLeavesOptions::default(),
|
tap_leaves_options: TapLeavesOptions::default(),
|
||||||
sign_with_tap_internal_key: true,
|
sign_with_tap_internal_key: true,
|
||||||
@@ -841,7 +851,7 @@ pub(crate) trait ComputeSighash {
|
|||||||
type SighashType;
|
type SighashType;
|
||||||
|
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
extra: Self::Extra,
|
extra: Self::Extra,
|
||||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
|
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
|
||||||
@@ -853,7 +863,7 @@ impl ComputeSighash for Legacy {
|
|||||||
type SighashType = EcdsaSighashType;
|
type SighashType = EcdsaSighashType;
|
||||||
|
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
_extra: (),
|
_extra: (),
|
||||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||||
@@ -902,7 +912,7 @@ impl ComputeSighash for Segwitv0 {
|
|||||||
type SighashType = EcdsaSighashType;
|
type SighashType = EcdsaSighashType;
|
||||||
|
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
_extra: (),
|
_extra: (),
|
||||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||||
@@ -913,7 +923,7 @@ impl ComputeSighash for Segwitv0 {
|
|||||||
let psbt_input = &psbt.inputs[input_index];
|
let psbt_input = &psbt.inputs[input_index];
|
||||||
let tx_input = &psbt.unsigned_tx.input[input_index];
|
let tx_input = &psbt.unsigned_tx.input[input_index];
|
||||||
|
|
||||||
let sighash = psbt_input
|
let sighash_type = psbt_input
|
||||||
.sighash_type
|
.sighash_type
|
||||||
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
||||||
.ecdsa_hash_ty()
|
.ecdsa_hash_ty()
|
||||||
@@ -941,40 +951,39 @@ impl ComputeSighash for Segwitv0 {
|
|||||||
};
|
};
|
||||||
let value = utxo.value;
|
let value = utxo.value;
|
||||||
|
|
||||||
let script = match psbt_input.witness_script {
|
let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||||
Some(ref witness_script) => witness_script.clone(),
|
|
||||||
|
let sighash = match psbt_input.witness_script {
|
||||||
|
Some(ref witness_script) => {
|
||||||
|
sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
if utxo.script_pubkey.is_v0_p2wpkh() {
|
if utxo.script_pubkey.is_p2wpkh() {
|
||||||
utxo.script_pubkey
|
sighasher.p2wpkh_signature_hash(
|
||||||
.p2wpkh_script_code()
|
input_index,
|
||||||
.expect("We check above that the spk is a p2wpkh")
|
&utxo.script_pubkey,
|
||||||
|
value,
|
||||||
|
sighash_type,
|
||||||
|
)?
|
||||||
} else if psbt_input
|
} else if psbt_input
|
||||||
.redeem_script
|
.redeem_script
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.is_v0_p2wpkh())
|
.map(|s| s.is_p2wpkh())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
psbt_input
|
let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
|
||||||
.redeem_script
|
sighasher.p2wpkh_signature_hash(
|
||||||
.as_ref()
|
input_index,
|
||||||
.unwrap()
|
script_pubkey,
|
||||||
.p2wpkh_script_code()
|
value,
|
||||||
.expect("We check above that the spk is a p2wpkh")
|
sighash_type,
|
||||||
|
)?
|
||||||
} else {
|
} else {
|
||||||
return Err(SignerError::MissingWitnessScript);
|
return Err(SignerError::MissingWitnessScript);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Ok((sighash, sighash_type))
|
||||||
Ok((
|
|
||||||
sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(
|
|
||||||
input_index,
|
|
||||||
&script,
|
|
||||||
value,
|
|
||||||
sighash,
|
|
||||||
)?,
|
|
||||||
sighash,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,7 +993,7 @@ impl ComputeSighash for Tap {
|
|||||||
type SighashType = TapSighashType;
|
type SighashType = TapSighashType;
|
||||||
|
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &Psbt,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
extra: Self::Extra,
|
extra: Self::Extra,
|
||||||
) -> Result<(Self::Sighash, TapSighashType), SignerError> {
|
) -> Result<(Self::Sighash, TapSighashType), SignerError> {
|
||||||
@@ -1155,7 +1164,7 @@ mod signers_container_tests {
|
|||||||
impl TransactionSigner for DummySigner {
|
impl TransactionSigner for DummySigner {
|
||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
_psbt: &mut psbt::PartiallySignedTransaction,
|
_psbt: &mut Psbt,
|
||||||
_sign_options: &SignOptions,
|
_sign_options: &SignOptions,
|
||||||
_secp: &SecpCtx,
|
_secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
@@ -1173,8 +1182,8 @@ mod signers_container_tests {
|
|||||||
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
||||||
let secp: Secp256k1<All> = Secp256k1::new();
|
let secp: Secp256k1<All> = Secp256k1::new();
|
||||||
let path = bip32::DerivationPath::from_str(PATH).unwrap();
|
let path = bip32::DerivationPath::from_str(PATH).unwrap();
|
||||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
|
||||||
let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv);
|
let tpub = bip32::Xpub::from_priv(&secp, &tprv);
|
||||||
let fingerprint = tprv.fingerprint(&secp);
|
let fingerprint = tprv.fingerprint(&secp);
|
||||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
//! // 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(), 50_000)
|
||||||
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
||||||
//! .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||||
//! // Only spend non-change outputs
|
//! // Only spend non-change outputs
|
||||||
//! .do_not_spend_change()
|
//! .do_not_spend_change()
|
||||||
//! // Turn on RBF signaling
|
//! // Turn on RBF signaling
|
||||||
@@ -40,22 +40,20 @@
|
|||||||
//! # Ok::<(), anyhow::Error>(())
|
//! # Ok::<(), anyhow::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::collections::BTreeMap;
|
|
||||||
use crate::collections::HashSet;
|
|
||||||
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||||
use bdk_chain::PersistBackend;
|
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
|
use bdk_chain::PersistBackend;
|
||||||
use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
use bitcoin::psbt::{self, Psbt};
|
||||||
|
use bitcoin::script::PushBytes;
|
||||||
|
use bitcoin::{absolute, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||||
|
|
||||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||||
use super::ChangeSet;
|
use super::{ChangeSet, CreateTxError, Wallet};
|
||||||
use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo};
|
use crate::collections::{BTreeMap, HashSet};
|
||||||
use crate::wallet::CreateTxError;
|
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||||
use crate::{Utxo, Wallet};
|
|
||||||
|
|
||||||
/// Context in which the [`TxBuilder`] is valid
|
/// Context in which the [`TxBuilder`] is valid
|
||||||
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
||||||
@@ -163,7 +161,7 @@ pub(crate) struct TxParams {
|
|||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct PreviousFee {
|
pub(crate) struct PreviousFee {
|
||||||
pub absolute: u64,
|
pub absolute: u64,
|
||||||
pub rate: f32,
|
pub rate: FeeRate,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -174,7 +172,7 @@ pub(crate) enum FeePolicy {
|
|||||||
|
|
||||||
impl Default for FeePolicy {
|
impl Default for FeePolicy {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
|
FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,15 +188,13 @@ impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> {
|
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||||
/// Set a custom fee rate
|
/// Set a custom fee rate.
|
||||||
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size.
|
///
|
||||||
/// This means that the total fee paid is equal to this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu).
|
/// This method sets the mining fee paid by the transaction as a rate on its size.
|
||||||
/// This rate is internally expressed in satoshis-per-virtual-bytes (sats/vB) using FeeRate::from_sat_per_vb, but can also be set by:
|
/// This means that the total fee paid is equal to `fee_rate` times the size
|
||||||
/// * sats/kvB (1000 sats/kvB == 1 sats/vB) using FeeRate::from_sat_per_kvb
|
/// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
|
||||||
/// * btc/kvB (0.00001000 btc/kvB == 1 sats/vB) using FeeRate::from_btc_per_kvb
|
/// relay policy.
|
||||||
/// * sats/kwu (250 sats/kwu == 1 sats/vB) using FeeRate::from_sat_per_kwu
|
|
||||||
/// Default is 1 sat/vB (see min_relay_fee)
|
|
||||||
///
|
///
|
||||||
/// Note that this is really a minimum feerate -- it's possible to
|
/// Note that this is really a minimum feerate -- it's possible to
|
||||||
/// overshoot it slightly since adding a change output to drain the remaining
|
/// overshoot it slightly since adding a change output to drain the remaining
|
||||||
@@ -318,8 +314,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
|||||||
|
|
||||||
for utxo in utxos {
|
for utxo in utxos {
|
||||||
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
|
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
|
||||||
#[allow(deprecated)]
|
let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap();
|
||||||
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
|
|
||||||
self.params.utxos.push(WeightedUtxo {
|
self.params.utxos.push(WeightedUtxo {
|
||||||
satisfaction_weight,
|
satisfaction_weight,
|
||||||
utxo: Utxo::Local(utxo),
|
utxo: Utxo::Local(utxo),
|
||||||
@@ -360,9 +355,9 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
|||||||
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
|
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
|
||||||
/// of course check the real input weight matches the expected weight prior to broadcasting.
|
/// of course check the real input weight matches the expected weight prior to broadcasting.
|
||||||
///
|
///
|
||||||
/// To guarantee the `satisfaction_weight` is correct, you can require the party providing the
|
/// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the
|
||||||
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
|
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
|
||||||
/// `script_pubkey` and then ask it for the [`max_satisfaction_weight`].
|
/// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`].
|
||||||
///
|
///
|
||||||
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
||||||
///
|
///
|
||||||
@@ -383,7 +378,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
|||||||
///
|
///
|
||||||
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||||
/// [`finish`]: Self::finish
|
/// [`finish`]: Self::finish
|
||||||
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
|
/// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy
|
||||||
pub fn add_foreign_utxo(
|
pub fn add_foreign_utxo(
|
||||||
&mut self,
|
&mut self,
|
||||||
outpoint: OutPoint,
|
outpoint: OutPoint,
|
||||||
@@ -574,20 +569,6 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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>,
|
|
||||||
{
|
|
||||||
self.wallet
|
|
||||||
.borrow_mut()
|
|
||||||
.create_tx(self.coin_selection, self.params)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable signaling RBF
|
/// Enable signaling RBF
|
||||||
///
|
///
|
||||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||||
@@ -634,6 +615,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, D, 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>,
|
||||||
|
{
|
||||||
|
self.wallet
|
||||||
|
.borrow_mut()
|
||||||
|
.create_tx(self.coin_selection, self.params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
|
/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
|
||||||
pub enum AddUtxoError {
|
pub enum AddUtxoError {
|
||||||
@@ -779,7 +776,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
|||||||
/// .drain_wallet()
|
/// .drain_wallet()
|
||||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||||
/// .drain_to(to_address.script_pubkey())
|
/// .drain_to(to_address.script_pubkey())
|
||||||
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||||
/// .enable_rbf();
|
/// .enable_rbf();
|
||||||
/// let psbt = tx_builder.finish()?;
|
/// let psbt = tx_builder.finish()?;
|
||||||
/// # Ok::<(), anyhow::Error>(())
|
/// # Ok::<(), anyhow::Error>(())
|
||||||
@@ -930,7 +927,8 @@ mod test {
|
|||||||
|
|
||||||
use bdk_chain::ConfirmationTime;
|
use bdk_chain::ConfirmationTime;
|
||||||
use bitcoin::consensus::deserialize;
|
use bitcoin::consensus::deserialize;
|
||||||
use bitcoin::hashes::hex::FromHex;
|
use bitcoin::hex::FromHex;
|
||||||
|
use bitcoin::TxOut;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -1001,7 +999,7 @@ mod test {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(tx.output[0].value, 800);
|
assert_eq!(tx.output[0].value.to_sat(), 800);
|
||||||
assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
|
assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tx.output[2].script_pubkey,
|
tx.output[2].script_pubkey,
|
||||||
@@ -1018,7 +1016,7 @@ mod test {
|
|||||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||||
vout: 0,
|
vout: 0,
|
||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: TxOut::NULL,
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
is_spent: false,
|
is_spent: false,
|
||||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||||
@@ -1029,7 +1027,7 @@ mod test {
|
|||||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||||
vout: 1,
|
vout: 1,
|
||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: TxOut::NULL,
|
||||||
keychain: KeychainKind::Internal,
|
keychain: KeychainKind::Internal,
|
||||||
is_spent: false,
|
is_spent: false,
|
||||||
confirmation_time: ConfirmationTime::Confirmed {
|
confirmation_time: ConfirmationTime::Confirmed {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ mod test {
|
|||||||
.require_network(Network::Bitcoin)
|
.require_network(Network::Bitcoin)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.script_pubkey();
|
.script_pubkey();
|
||||||
assert!(script_p2wpkh.is_v0_p2wpkh());
|
assert!(script_p2wpkh.is_p2wpkh());
|
||||||
assert!(293.is_dust(&script_p2wpkh));
|
assert!(293.is_dust(&script_p2wpkh));
|
||||||
assert!(!294.is_dust(&script_p2wpkh));
|
assert!(!294.is_dust(&script_p2wpkh));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
|
|||||||
use bdk_chain::indexed_tx_graph::Indexer;
|
use bdk_chain::indexed_tx_graph::Indexer;
|
||||||
use bdk_chain::{BlockId, ConfirmationTime};
|
use bdk_chain::{BlockId, ConfirmationTime};
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
|
use bitcoin::{
|
||||||
|
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
|
||||||
|
Txid,
|
||||||
|
};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
// Return a fake wallet that appears to be funded for testing.
|
// Return a fake wallet that appears to be funded for testing.
|
||||||
@@ -24,7 +27,7 @@ pub fn get_funded_wallet_with_change(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let tx0 = Transaction {
|
let tx0 = Transaction {
|
||||||
version: 1,
|
version: transaction::Version::ONE,
|
||||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint {
|
previous_output: OutPoint {
|
||||||
@@ -36,13 +39,13 @@ pub fn get_funded_wallet_with_change(
|
|||||||
witness: Default::default(),
|
witness: Default::default(),
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 76_000,
|
value: Amount::from_sat(76_000),
|
||||||
script_pubkey: change_address.script_pubkey(),
|
script_pubkey: change_address.script_pubkey(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
let tx1 = Transaction {
|
let tx1 = Transaction {
|
||||||
version: 1,
|
version: transaction::Version::ONE,
|
||||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint {
|
previous_output: OutPoint {
|
||||||
@@ -55,11 +58,11 @@ pub fn get_funded_wallet_with_change(
|
|||||||
}],
|
}],
|
||||||
output: vec![
|
output: vec![
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 50_000,
|
value: Amount::from_sat(50_000),
|
||||||
script_pubkey: change_address.script_pubkey(),
|
script_pubkey: change_address.script_pubkey(),
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 25_000,
|
value: Amount::from_sat(25_000),
|
||||||
script_pubkey: sendto_address.script_pubkey(),
|
script_pubkey: sendto_address.script_pubkey(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -154,3 +157,16 @@ pub fn get_test_tr_with_taptree_xprv() -> &'static str {
|
|||||||
pub fn get_test_tr_dup_keys() -> &'static str {
|
pub fn get_test_tr_dup_keys() -> &'static str {
|
||||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is
|
||||||
|
/// useful in cases where we want to create a feerate from a `f64`, as the
|
||||||
|
/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer.
|
||||||
|
///
|
||||||
|
/// **Note** this 'quick and dirty' conversion should only be used when the input
|
||||||
|
/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow,
|
||||||
|
/// or else the resulting value will be inaccurate.
|
||||||
|
pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
|
||||||
|
// 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu
|
||||||
|
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
|
||||||
|
FeeRate::from_sat_per_kwu(sat_kwu)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use bdk::bitcoin::TxIn;
|
use bdk::bitcoin::{Amount, FeeRate, Psbt, TxIn};
|
||||||
use bdk::wallet::AddressIndex;
|
use bdk::wallet::AddressIndex;
|
||||||
use bdk::wallet::AddressIndex::New;
|
use bdk::wallet::AddressIndex::New;
|
||||||
use bdk::{psbt, FeeRate, SignOptions};
|
use bdk::{psbt, SignOptions};
|
||||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
mod common;
|
mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
@@ -82,13 +81,13 @@ fn test_psbt_sign_with_finalized() {
|
|||||||
fn test_psbt_fee_rate_with_witness_utxo() {
|
fn test_psbt_fee_rate_with_witness_utxo() {
|
||||||
use psbt::PsbtUtils;
|
use psbt::PsbtUtils;
|
||||||
|
|
||||||
let expected_fee_rate = 1.2345;
|
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||||
|
|
||||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New);
|
let addr = wallet.get_address(New);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
builder.fee_rate(expected_fee_rate);
|
||||||
let mut psbt = builder.finish().unwrap();
|
let mut psbt = builder.finish().unwrap();
|
||||||
let fee_amount = psbt.fee_amount();
|
let fee_amount = psbt.fee_amount();
|
||||||
assert!(fee_amount.is_some());
|
assert!(fee_amount.is_some());
|
||||||
@@ -99,21 +98,21 @@ fn test_psbt_fee_rate_with_witness_utxo() {
|
|||||||
assert!(finalized);
|
assert!(finalized);
|
||||||
|
|
||||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
assert!(finalized_fee_rate >= expected_fee_rate);
|
||||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||||
use psbt::PsbtUtils;
|
use psbt::PsbtUtils;
|
||||||
|
|
||||||
let expected_fee_rate = 1.2345;
|
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||||
|
|
||||||
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New);
|
let addr = wallet.get_address(New);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
builder.fee_rate(expected_fee_rate);
|
||||||
let mut psbt = builder.finish().unwrap();
|
let mut psbt = builder.finish().unwrap();
|
||||||
let fee_amount = psbt.fee_amount();
|
let fee_amount = psbt.fee_amount();
|
||||||
assert!(fee_amount.is_some());
|
assert!(fee_amount.is_some());
|
||||||
@@ -123,21 +122,21 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
|||||||
assert!(finalized);
|
assert!(finalized);
|
||||||
|
|
||||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
assert!(finalized_fee_rate >= expected_fee_rate);
|
||||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_psbt_fee_rate_with_missing_txout() {
|
fn test_psbt_fee_rate_with_missing_txout() {
|
||||||
use psbt::PsbtUtils;
|
use psbt::PsbtUtils;
|
||||||
|
|
||||||
let expected_fee_rate = 1.2345;
|
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||||
|
|
||||||
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wpkh_wallet.get_address(New);
|
let addr = wpkh_wallet.get_address(New);
|
||||||
let mut builder = wpkh_wallet.build_tx();
|
let mut builder = wpkh_wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
builder.fee_rate(expected_fee_rate);
|
||||||
let mut wpkh_psbt = builder.finish().unwrap();
|
let mut wpkh_psbt = builder.finish().unwrap();
|
||||||
|
|
||||||
wpkh_psbt.inputs[0].witness_utxo = None;
|
wpkh_psbt.inputs[0].witness_utxo = None;
|
||||||
@@ -149,7 +148,7 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
|||||||
let addr = pkh_wallet.get_address(New);
|
let addr = pkh_wallet.get_address(New);
|
||||||
let mut builder = pkh_wallet.build_tx();
|
let mut builder = pkh_wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
builder.fee_rate(expected_fee_rate);
|
||||||
let mut pkh_psbt = builder.finish().unwrap();
|
let mut pkh_psbt = builder.finish().unwrap();
|
||||||
|
|
||||||
pkh_psbt.inputs[0].non_witness_utxo = None;
|
pkh_psbt.inputs[0].non_witness_utxo = None;
|
||||||
@@ -161,16 +160,26 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
|||||||
fn test_psbt_multiple_internalkey_signers() {
|
fn test_psbt_multiple_internalkey_signers() {
|
||||||
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||||
use bdk::KeychainKind;
|
use bdk::KeychainKind;
|
||||||
use bitcoin::{secp256k1::Secp256k1, PrivateKey};
|
use bitcoin::key::TapTweak;
|
||||||
use miniscript::psbt::PsbtExt;
|
use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
|
||||||
|
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
|
||||||
|
use bitcoin::{PrivateKey, TxOut};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
|
let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
|
||||||
|
let desc = format!("tr({})", wif);
|
||||||
|
let prv = PrivateKey::from_wif(wif).unwrap();
|
||||||
|
let keypair = Keypair::from_secret_key(&secp, &prv.inner);
|
||||||
|
|
||||||
|
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.get_address(AddressIndex::New);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
builder.drain_to(send_to.script_pubkey()).drain_wallet();
|
||||||
let mut psbt = builder.finish().unwrap();
|
let mut psbt = builder.finish().unwrap();
|
||||||
|
let unsigned_tx = psbt.unsigned_tx.clone();
|
||||||
|
|
||||||
// Adds a signer for the wrong internal key, bdk should not use this key to sign
|
// Adds a signer for the wrong internal key, bdk should not use this key to sign
|
||||||
wallet.add_signer(
|
wallet.add_signer(
|
||||||
KeychainKind::External,
|
KeychainKind::External,
|
||||||
@@ -183,10 +192,32 @@ fn test_psbt_multiple_internalkey_signers() {
|
|||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||||
// Checks that we signed using the right key
|
assert!(finalized);
|
||||||
assert!(
|
|
||||||
psbt.finalize_mut(&secp).is_ok(),
|
// To verify, we need the signature, message, and pubkey
|
||||||
"The wrong internal key was used"
|
let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
|
||||||
);
|
assert!(!witness.is_empty());
|
||||||
|
let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap();
|
||||||
|
|
||||||
|
// the prevout we're spending
|
||||||
|
let prevouts = &[TxOut {
|
||||||
|
script_pubkey: send_to.script_pubkey(),
|
||||||
|
value: Amount::from_sat(to_spend),
|
||||||
|
}];
|
||||||
|
let prevouts = Prevouts::All(prevouts);
|
||||||
|
let input_index = 0;
|
||||||
|
let mut sighash_cache = SighashCache::new(unsigned_tx);
|
||||||
|
let sighash = sighash_cache
|
||||||
|
.taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default)
|
||||||
|
.unwrap();
|
||||||
|
let message = Message::from(sighash);
|
||||||
|
|
||||||
|
// add tweak. this was taken from `signer::sign_psbt_schnorr`
|
||||||
|
let keypair = keypair.tap_tweak(&secp, None).to_inner();
|
||||||
|
let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair);
|
||||||
|
|
||||||
|
// Must verify if we used the correct key to sign
|
||||||
|
let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey);
|
||||||
|
assert!(verify_res.is_ok(), "The wrong internal key was used");
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_bitcoind_rpc"
|
name = "bdk_bitcoind_rpc"
|
||||||
version = "0.5.0"
|
version = "0.8.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.63"
|
rust-version = "1.63"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
@@ -14,12 +14,12 @@ readme = "README.md"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# For no-std, remember to enable the bitcoin/no-std feature
|
# For no-std, remember to enable the bitcoin/no-std feature
|
||||||
bitcoin = { version = "0.30", default-features = false }
|
bitcoin = { version = "0.31", default-features = false }
|
||||||
bitcoincore-rpc = { version = "0.17" }
|
bitcoincore-rpc = { version = "0.18" }
|
||||||
bdk_chain = { path = "../chain", version = "0.10", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.12", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bitcoind = { version = "0.33", features = ["25_0"] }
|
bdk_testenv = { path = "../testenv", default_features = false }
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -2,160 +2,14 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
|
|
||||||
use bdk_bitcoind_rpc::Emitter;
|
use bdk_bitcoind_rpc::Emitter;
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{Address, Amount, BlockHash, Txid},
|
bitcoin::{Address, Amount, Txid},
|
||||||
keychain::Balance,
|
keychain::Balance,
|
||||||
local_chain::{self, CheckPoint, LocalChain},
|
local_chain::{self, CheckPoint, LocalChain},
|
||||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||||
};
|
};
|
||||||
use bitcoin::{
|
use bdk_testenv::TestEnv;
|
||||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
|
||||||
secp256k1::rand::random, Block, CompactTarget, OutPoint, ScriptBuf, ScriptHash, Transaction,
|
use bitcoincore_rpc::RpcApi;
|
||||||
TxIn, TxOut, WScriptHash,
|
|
||||||
};
|
|
||||||
use bitcoincore_rpc::{
|
|
||||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
|
||||||
RpcApi,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TestEnv {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
daemon: bitcoind::BitcoinD,
|
|
||||||
client: bitcoincore_rpc::Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestEnv {
|
|
||||||
fn new() -> anyhow::Result<Self> {
|
|
||||||
let daemon = match std::env::var_os("TEST_BITCOIND") {
|
|
||||||
Some(bitcoind_path) => bitcoind::BitcoinD::new(bitcoind_path),
|
|
||||||
None => bitcoind::BitcoinD::from_downloaded(),
|
|
||||||
}?;
|
|
||||||
let client = bitcoincore_rpc::Client::new(
|
|
||||||
&daemon.rpc_url(),
|
|
||||||
bitcoincore_rpc::Auth::CookieFile(daemon.params.cookie_file.clone()),
|
|
||||||
)?;
|
|
||||||
Ok(Self { daemon, client })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mine_blocks(
|
|
||||||
&self,
|
|
||||||
count: usize,
|
|
||||||
address: Option<Address>,
|
|
||||||
) -> anyhow::Result<Vec<BlockHash>> {
|
|
||||||
let coinbase_address = match address {
|
|
||||||
Some(address) => address,
|
|
||||||
None => self.client.get_new_address(None, None)?.assume_checked(),
|
|
||||||
};
|
|
||||||
let block_hashes = self
|
|
||||||
.client
|
|
||||||
.generate_to_address(count as _, &coinbase_address)?;
|
|
||||||
Ok(block_hashes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
|
||||||
let bt = self.client.get_block_template(
|
|
||||||
GetBlockTemplateModes::Template,
|
|
||||||
&[GetBlockTemplateRules::SegWit],
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let txdata = vec![Transaction {
|
|
||||||
version: 1,
|
|
||||||
lock_time: bitcoin::absolute::LockTime::from_height(0)?,
|
|
||||||
input: vec![TxIn {
|
|
||||||
previous_output: bitcoin::OutPoint::default(),
|
|
||||||
script_sig: ScriptBuf::builder()
|
|
||||||
.push_int(bt.height as _)
|
|
||||||
// randomn number so that re-mining creates unique block
|
|
||||||
.push_int(random())
|
|
||||||
.into_script(),
|
|
||||||
sequence: bitcoin::Sequence::default(),
|
|
||||||
witness: bitcoin::Witness::new(),
|
|
||||||
}],
|
|
||||||
output: vec![TxOut {
|
|
||||||
value: 0,
|
|
||||||
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
|
|
||||||
}],
|
|
||||||
}];
|
|
||||||
|
|
||||||
let bits: [u8; 4] = bt
|
|
||||||
.bits
|
|
||||||
.clone()
|
|
||||||
.try_into()
|
|
||||||
.expect("rpc provided us with invalid bits");
|
|
||||||
|
|
||||||
let mut block = Block {
|
|
||||||
header: Header {
|
|
||||||
version: bitcoin::block::Version::default(),
|
|
||||||
prev_blockhash: bt.previous_block_hash,
|
|
||||||
merkle_root: TxMerkleNode::all_zeros(),
|
|
||||||
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
|
|
||||||
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
|
|
||||||
nonce: 0,
|
|
||||||
},
|
|
||||||
txdata,
|
|
||||||
};
|
|
||||||
|
|
||||||
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
|
|
||||||
|
|
||||||
for nonce in 0..=u32::MAX {
|
|
||||||
block.header.nonce = nonce;
|
|
||||||
if block.header.target().is_met_by(block.block_hash()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.submit_block(&block)?;
|
|
||||||
Ok((bt.height as usize, block.block_hash()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
|
||||||
let mut hash = self.client.get_best_block_hash()?;
|
|
||||||
for _ in 0..count {
|
|
||||||
let prev_hash = self.client.get_block_info(&hash)?.previousblockhash;
|
|
||||||
self.client.invalidate_block(&hash)?;
|
|
||||||
match prev_hash {
|
|
||||||
Some(prev_hash) => hash = prev_hash,
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
|
||||||
let start_height = self.client.get_block_count()?;
|
|
||||||
self.invalidate_blocks(count)?;
|
|
||||||
|
|
||||||
let res = self.mine_blocks(count, None);
|
|
||||||
assert_eq!(
|
|
||||||
self.client.get_block_count()?,
|
|
||||||
start_height,
|
|
||||||
"reorg should not result in height change"
|
|
||||||
);
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
|
||||||
let start_height = self.client.get_block_count()?;
|
|
||||||
self.invalidate_blocks(count)?;
|
|
||||||
|
|
||||||
let res = (0..count)
|
|
||||||
.map(|_| self.mine_empty_block())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
assert_eq!(
|
|
||||||
self.client.get_block_count()?,
|
|
||||||
start_height,
|
|
||||||
"reorg should not result in height change"
|
|
||||||
);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
|
||||||
let txid = self
|
|
||||||
.client
|
|
||||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
|
||||||
Ok(txid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that blocks are emitted in order even after reorg.
|
/// Ensure that blocks are emitted in order even after reorg.
|
||||||
///
|
///
|
||||||
@@ -166,17 +20,22 @@ impl TestEnv {
|
|||||||
#[test]
|
#[test]
|
||||||
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
let network_tip = env.rpc_client().get_block_count()?;
|
||||||
let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
|
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||||
|
let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
|
||||||
|
|
||||||
// mine some blocks and returned the actual block hashes
|
// Mine some blocks and return the actual block hashes.
|
||||||
|
// Because initializing `ElectrsD` already mines some blocks, we must include those too when
|
||||||
|
// returning block hashes.
|
||||||
let exp_hashes = {
|
let exp_hashes = {
|
||||||
let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
|
let mut hashes = (0..=network_tip)
|
||||||
hashes.extend(env.mine_blocks(101, None)?);
|
.map(|height| env.rpc_client().get_block_hash(height))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
hashes.extend(env.mine_blocks(101 - network_tip as usize, None)?);
|
||||||
hashes
|
hashes
|
||||||
};
|
};
|
||||||
|
|
||||||
// see if the emitter outputs the right blocks
|
// See if the emitter outputs the right blocks.
|
||||||
println!("first sync:");
|
println!("first sync:");
|
||||||
while let Some(emission) = emitter.next_block()? {
|
while let Some(emission) = emitter.next_block()? {
|
||||||
let height = emission.block_height();
|
let height = emission.block_height();
|
||||||
@@ -198,16 +57,19 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
local_chain.blocks(),
|
local_chain
|
||||||
&exp_hashes
|
.iter_checkpoints()
|
||||||
|
.map(|cp| (cp.height(), cp.hash()))
|
||||||
|
.collect::<BTreeSet<_>>(),
|
||||||
|
exp_hashes
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, hash)| (i as u32, *hash))
|
.map(|(i, hash)| (i as u32, *hash))
|
||||||
.collect(),
|
.collect::<BTreeSet<_>>(),
|
||||||
"final local_chain state is unexpected",
|
"final local_chain state is unexpected",
|
||||||
);
|
);
|
||||||
|
|
||||||
// perform reorg
|
// Perform reorg.
|
||||||
let reorged_blocks = env.reorg(6)?;
|
let reorged_blocks = env.reorg(6)?;
|
||||||
let exp_hashes = exp_hashes
|
let exp_hashes = exp_hashes
|
||||||
.iter()
|
.iter()
|
||||||
@@ -216,7 +78,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// see if the emitter outputs the right blocks
|
// See if the emitter outputs the right blocks.
|
||||||
println!("after reorg:");
|
println!("after reorg:");
|
||||||
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
|
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
|
||||||
while let Some(emission) = emitter.next_block()? {
|
while let Some(emission) = emitter.next_block()? {
|
||||||
@@ -251,12 +113,15 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
local_chain.blocks(),
|
local_chain
|
||||||
&exp_hashes
|
.iter_checkpoints()
|
||||||
|
.map(|cp| (cp.height(), cp.hash()))
|
||||||
|
.collect::<BTreeSet<_>>(),
|
||||||
|
exp_hashes
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, hash)| (i as u32, *hash))
|
.map(|(i, hash)| (i as u32, *hash))
|
||||||
.collect(),
|
.collect::<BTreeSet<_>>(),
|
||||||
"final local_chain state is unexpected after reorg",
|
"final local_chain state is unexpected after reorg",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -272,16 +137,25 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
|
|
||||||
println!("getting new addresses!");
|
println!("getting new addresses!");
|
||||||
let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
|
let addr_0 = env
|
||||||
let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
|
.rpc_client()
|
||||||
let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
|
let addr_1 = env
|
||||||
|
.rpc_client()
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
|
let addr_2 = env
|
||||||
|
.rpc_client()
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
println!("got new addresses!");
|
println!("got new addresses!");
|
||||||
|
|
||||||
println!("mining block!");
|
println!("mining block!");
|
||||||
env.mine_blocks(101, None)?;
|
env.mine_blocks(101, None)?;
|
||||||
println!("mined blocks!");
|
println!("mined blocks!");
|
||||||
|
|
||||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||||
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
|
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||||
let mut index = SpkTxOutIndex::<usize>::default();
|
let mut index = SpkTxOutIndex::<usize>::default();
|
||||||
index.insert_spk(0, addr_0.script_pubkey());
|
index.insert_spk(0, addr_0.script_pubkey());
|
||||||
@@ -290,7 +164,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
index
|
index
|
||||||
});
|
});
|
||||||
|
|
||||||
let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
|
let emitter = &mut Emitter::new(env.rpc_client(), chain.tip(), 0);
|
||||||
|
|
||||||
while let Some(emission) = emitter.next_block()? {
|
while let Some(emission) = emitter.next_block()? {
|
||||||
let height = emission.block_height();
|
let height = emission.block_height();
|
||||||
@@ -306,7 +180,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
let exp_txids = {
|
let exp_txids = {
|
||||||
let mut txids = BTreeSet::new();
|
let mut txids = BTreeSet::new();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
txids.insert(env.client.send_to_address(
|
txids.insert(env.rpc_client().send_to_address(
|
||||||
&addr_0,
|
&addr_0,
|
||||||
Amount::from_sat(10_000),
|
Amount::from_sat(10_000),
|
||||||
None,
|
None,
|
||||||
@@ -342,7 +216,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// mine a block that confirms the 3 txs
|
// mine a block that confirms the 3 txs
|
||||||
let exp_block_hash = env.mine_blocks(1, None)?[0];
|
let exp_block_hash = env.mine_blocks(1, None)?[0];
|
||||||
let exp_block_height = env.client.get_block_info(&exp_block_hash)?.height as u32;
|
let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32;
|
||||||
let exp_anchors = exp_txids
|
let exp_anchors = exp_txids
|
||||||
.iter()
|
.iter()
|
||||||
.map({
|
.map({
|
||||||
@@ -386,10 +260,10 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
&env.client,
|
env.rpc_client(),
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.client.get_block_hash(0)?,
|
hash: env.rpc_client().get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
EMITTER_START_HEIGHT as _,
|
EMITTER_START_HEIGHT as _,
|
||||||
);
|
);
|
||||||
@@ -463,21 +337,24 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
&env.client,
|
env.rpc_client(),
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.client.get_block_hash(0)?,
|
hash: env.rpc_client().get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// setup addresses
|
// setup addresses
|
||||||
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
|
let addr_to_mine = env
|
||||||
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
.rpc_client()
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
|
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
|
||||||
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
||||||
|
|
||||||
// setup receiver
|
// setup receiver
|
||||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||||
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
|
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||||
let mut recv_index = SpkTxOutIndex::default();
|
let mut recv_index = SpkTxOutIndex::default();
|
||||||
recv_index.insert_spk((), spk_to_track.clone());
|
recv_index.insert_spk((), spk_to_track.clone());
|
||||||
@@ -493,7 +370,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// lock outputs that send to `addr_to_track`
|
// lock outputs that send to `addr_to_track`
|
||||||
let outpoints_to_lock = env
|
let outpoints_to_lock = env
|
||||||
.client
|
.rpc_client()
|
||||||
.get_transaction(&txid, None)?
|
.get_transaction(&txid, None)?
|
||||||
.transaction()?
|
.transaction()?
|
||||||
.output
|
.output
|
||||||
@@ -502,7 +379,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
|
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
|
||||||
.map(|(vout, _)| OutPoint::new(txid, vout as _))
|
.map(|(vout, _)| OutPoint::new(txid, vout as _))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
env.client.lock_unspent(&outpoints_to_lock)?;
|
env.rpc_client().lock_unspent(&outpoints_to_lock)?;
|
||||||
|
|
||||||
let _ = env.mine_blocks(1, None)?;
|
let _ = env.mine_blocks(1, None)?;
|
||||||
}
|
}
|
||||||
@@ -551,16 +428,19 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
&env.client,
|
env.rpc_client(),
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.client.get_block_hash(0)?,
|
hash: env.rpc_client().get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// mine blocks and sync up emitter
|
// mine blocks and sync up emitter
|
||||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
let addr = env
|
||||||
|
.rpc_client()
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
|
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
|
||||||
while emitter.next_header()?.is_some() {}
|
while emitter.next_header()?.is_some() {}
|
||||||
|
|
||||||
@@ -613,16 +493,19 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
&env.client,
|
env.rpc_client(),
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.client.get_block_hash(0)?,
|
hash: env.rpc_client().get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// mine blocks to get initial balance, sync emitter up to tip
|
// mine blocks to get initial balance, sync emitter up to tip
|
||||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
let addr = env
|
||||||
|
.rpc_client()
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||||
while emitter.next_header()?.is_some() {}
|
while emitter.next_header()?.is_some() {}
|
||||||
|
|
||||||
@@ -698,16 +581,19 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
&env.client,
|
env.rpc_client(),
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.client.get_block_hash(0)?,
|
hash: env.rpc_client().get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// mine blocks to get initial balance
|
// mine blocks to get initial balance
|
||||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
let addr = env
|
||||||
|
.rpc_client()
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||||
|
|
||||||
// introduce mempool tx at each block extension
|
// introduce mempool tx at each block extension
|
||||||
@@ -725,7 +611,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(tx, _)| tx.txid())
|
.map(|(tx, _)| tx.txid())
|
||||||
.collect::<BTreeSet<_>>(),
|
.collect::<BTreeSet<_>>(),
|
||||||
env.client
|
env.rpc_client()
|
||||||
.get_raw_mempool()?
|
.get_raw_mempool()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<BTreeSet<_>>(),
|
.collect::<BTreeSet<_>>(),
|
||||||
@@ -744,7 +630,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
|||||||
// emission.
|
// emission.
|
||||||
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
|
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
|
||||||
let tx_introductions = dbg!(env
|
let tx_introductions = dbg!(env
|
||||||
.client
|
.rpc_client()
|
||||||
.get_raw_mempool_verbose()?
|
.get_raw_mempool_verbose()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(txid, entry)| (txid, entry.height as usize))
|
.map(|(txid, entry)| (txid, entry.height as usize))
|
||||||
@@ -821,10 +707,10 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// start height is 99
|
// start height is 99
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
&env.client,
|
env.rpc_client(),
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.client.get_block_hash(0)?,
|
hash: env.rpc_client().get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
(PREMINE_COUNT - 2) as u32,
|
(PREMINE_COUNT - 2) as u32,
|
||||||
);
|
);
|
||||||
@@ -842,12 +728,12 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
|||||||
let block_hash_100a = block_header_100a.block_hash();
|
let block_hash_100a = block_header_100a.block_hash();
|
||||||
|
|
||||||
// get hash for block 101a
|
// get hash for block 101a
|
||||||
let block_hash_101a = env.client.get_block_hash(101)?;
|
let block_hash_101a = env.rpc_client().get_block_hash(101)?;
|
||||||
|
|
||||||
// invalidate blocks 99a, 100a, 101a
|
// invalidate blocks 99a, 100a, 101a
|
||||||
env.client.invalidate_block(&block_hash_99a)?;
|
env.rpc_client().invalidate_block(&block_hash_99a)?;
|
||||||
env.client.invalidate_block(&block_hash_100a)?;
|
env.rpc_client().invalidate_block(&block_hash_100a)?;
|
||||||
env.client.invalidate_block(&block_hash_101a)?;
|
env.rpc_client().invalidate_block(&block_hash_101a)?;
|
||||||
|
|
||||||
// mine new blocks 99b, 100b, 101b
|
// mine new blocks 99b, 100b, 101b
|
||||||
env.mine_blocks(3, None)?;
|
env.mine_blocks(3, None)?;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_chain"
|
name = "bdk_chain"
|
||||||
version = "0.10.0"
|
version = "0.12.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.63"
|
rust-version = "1.63"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
@@ -14,16 +14,16 @@ readme = "README.md"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# For no-std, remember to enable the bitcoin/no-std feature
|
# For no-std, remember to enable the bitcoin/no-std feature
|
||||||
bitcoin = { version = "0.30.0", default-features = false }
|
bitcoin = { version = "0.31.0", default-features = false }
|
||||||
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
|
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive", "rc"] }
|
||||||
|
|
||||||
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
|
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
|
||||||
# note versions > 0.9.1 breaks ours 1.57.0 MSRV.
|
|
||||||
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
||||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
miniscript = { version = "11.0.0", optional = true, default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
proptest = "1.2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
|
|||||||
@@ -326,12 +326,17 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
self.lookahead
|
self.lookahead
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store lookahead scripts until `target_index`.
|
/// Store lookahead scripts until `target_index` (inclusive).
|
||||||
///
|
///
|
||||||
/// This does not change the `lookahead` setting.
|
/// This does not change the global `lookahead` setting.
|
||||||
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
||||||
let next_index = self.next_store_index(keychain);
|
let (next_index, _) = self.next_index(keychain);
|
||||||
if let Some(temp_lookahead) = target_index.checked_sub(next_index).filter(|&v| v > 0) {
|
|
||||||
|
let temp_lookahead = (target_index + 1)
|
||||||
|
.checked_sub(next_index)
|
||||||
|
.filter(|&index| index > 0);
|
||||||
|
|
||||||
|
if let Some(temp_lookahead) = temp_lookahead {
|
||||||
self.replenish_lookahead(keychain, temp_lookahead);
|
self.replenish_lookahead(keychain, temp_lookahead);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
|
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
|
||||||
|
|
||||||
use core::convert::Infallible;
|
use core::convert::Infallible;
|
||||||
|
use core::ops::RangeBounds;
|
||||||
|
|
||||||
use crate::collections::BTreeMap;
|
use crate::collections::BTreeMap;
|
||||||
use crate::{BlockId, ChainOracle};
|
use crate::{BlockId, ChainOracle};
|
||||||
@@ -34,6 +35,14 @@ struct CPInner {
|
|||||||
prev: Option<Arc<CPInner>>,
|
prev: Option<Arc<CPInner>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CheckPoint {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
let self_cps = self.iter().map(|cp| cp.block_id());
|
||||||
|
let other_cps = other.iter().map(|cp| cp.block_id());
|
||||||
|
self_cps.eq(other_cps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl CheckPoint {
|
impl CheckPoint {
|
||||||
/// Construct a new base block at the front of a linked list.
|
/// Construct a new base block at the front of a linked list.
|
||||||
pub fn new(block: BlockId) -> Self {
|
pub fn new(block: BlockId) -> Self {
|
||||||
@@ -148,6 +157,36 @@ impl CheckPoint {
|
|||||||
pub fn iter(&self) -> CheckPointIter {
|
pub fn iter(&self) -> CheckPointIter {
|
||||||
self.clone().into_iter()
|
self.clone().into_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get checkpoint at `height`.
|
||||||
|
///
|
||||||
|
/// Returns `None` if checkpoint at `height` does not exist`.
|
||||||
|
pub fn get(&self, height: u32) -> Option<Self> {
|
||||||
|
self.range(height..=height).next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate checkpoints over a height range.
|
||||||
|
///
|
||||||
|
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||||
|
/// height).
|
||||||
|
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||||
|
where
|
||||||
|
R: RangeBounds<u32>,
|
||||||
|
{
|
||||||
|
let start_bound = range.start_bound().cloned();
|
||||||
|
let end_bound = range.end_bound().cloned();
|
||||||
|
self.iter()
|
||||||
|
.skip_while(move |cp| match end_bound {
|
||||||
|
core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound,
|
||||||
|
core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound,
|
||||||
|
core::ops::Bound::Unbounded => false,
|
||||||
|
})
|
||||||
|
.take_while(move |cp| match start_bound {
|
||||||
|
core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound,
|
||||||
|
core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound,
|
||||||
|
core::ops::Bound::Unbounded => true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates over checkpoints backwards.
|
/// Iterates over checkpoints backwards.
|
||||||
@@ -188,7 +227,7 @@ impl IntoIterator for CheckPoint {
|
|||||||
/// Script-pubkey based syncing mechanisms may not introduce transactions in a chronological order
|
/// 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
|
/// so some updates require introducing older blocks (to anchor older transactions). For
|
||||||
/// script-pubkey based syncing, `introduce_older_blocks` would typically be `true`.
|
/// script-pubkey based syncing, `introduce_older_blocks` would typically be `true`.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Update {
|
pub struct Update {
|
||||||
/// The update chain's new tip.
|
/// The update chain's new tip.
|
||||||
pub tip: CheckPoint,
|
pub tip: CheckPoint,
|
||||||
@@ -202,22 +241,9 @@ pub struct Update {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// This is a local implementation of [`ChainOracle`].
|
/// This is a local implementation of [`ChainOracle`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct LocalChain {
|
pub struct LocalChain {
|
||||||
tip: CheckPoint,
|
tip: CheckPoint,
|
||||||
index: BTreeMap<u32, BlockHash>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for LocalChain {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.index == other.index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
|
||||||
fn from(value: LocalChain) -> Self {
|
|
||||||
value.index
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChainOracle for LocalChain {
|
impl ChainOracle for LocalChain {
|
||||||
@@ -228,18 +254,16 @@ impl ChainOracle for LocalChain {
|
|||||||
block: BlockId,
|
block: BlockId,
|
||||||
chain_tip: BlockId,
|
chain_tip: BlockId,
|
||||||
) -> Result<Option<bool>, Self::Error> {
|
) -> Result<Option<bool>, Self::Error> {
|
||||||
if block.height > chain_tip.height {
|
let chain_tip_cp = match self.tip.get(chain_tip.height) {
|
||||||
return Ok(None);
|
// we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can
|
||||||
|
// be identified in chain
|
||||||
|
Some(cp) if cp.hash() == chain_tip.hash => cp,
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
match chain_tip_cp.get(block.height) {
|
||||||
|
Some(cp) => Ok(Some(cp.hash() == block.hash)),
|
||||||
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
Ok(
|
|
||||||
match (
|
|
||||||
self.index.get(&block.height),
|
|
||||||
self.index.get(&chain_tip.height),
|
|
||||||
) {
|
|
||||||
(Some(cp), Some(tip_cp)) => Some(*cp == block.hash && *tip_cp == chain_tip.hash),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
||||||
@@ -250,7 +274,7 @@ impl ChainOracle for LocalChain {
|
|||||||
impl LocalChain {
|
impl LocalChain {
|
||||||
/// Get the genesis hash.
|
/// Get the genesis hash.
|
||||||
pub fn genesis_hash(&self) -> BlockHash {
|
pub fn genesis_hash(&self) -> BlockHash {
|
||||||
self.index.get(&0).copied().expect("must have genesis hash")
|
self.tip.get(0).expect("genesis must exist").hash()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct [`LocalChain`] from genesis `hash`.
|
/// Construct [`LocalChain`] from genesis `hash`.
|
||||||
@@ -259,7 +283,6 @@ impl LocalChain {
|
|||||||
let height = 0;
|
let height = 0;
|
||||||
let chain = Self {
|
let chain = Self {
|
||||||
tip: CheckPoint::new(BlockId { height, hash }),
|
tip: CheckPoint::new(BlockId { height, hash }),
|
||||||
index: core::iter::once((height, hash)).collect(),
|
|
||||||
};
|
};
|
||||||
let changeset = chain.initial_changeset();
|
let changeset = chain.initial_changeset();
|
||||||
(chain, changeset)
|
(chain, changeset)
|
||||||
@@ -276,7 +299,6 @@ impl LocalChain {
|
|||||||
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
||||||
chain.apply_changeset(&changeset)?;
|
chain.apply_changeset(&changeset)?;
|
||||||
|
|
||||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
|
||||||
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
||||||
|
|
||||||
Ok(chain)
|
Ok(chain)
|
||||||
@@ -284,18 +306,11 @@ impl LocalChain {
|
|||||||
|
|
||||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||||
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
||||||
let mut chain = Self {
|
let genesis_cp = tip.iter().last().expect("must have at least one element");
|
||||||
tip,
|
if genesis_cp.height() != 0 {
|
||||||
index: BTreeMap::new(),
|
|
||||||
};
|
|
||||||
chain.reindex(0);
|
|
||||||
|
|
||||||
if chain.index.get(&0).copied().is_none() {
|
|
||||||
return Err(MissingGenesisError);
|
return Err(MissingGenesisError);
|
||||||
}
|
}
|
||||||
|
Ok(Self { tip })
|
||||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
|
||||||
Ok(chain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||||
@@ -303,12 +318,11 @@ impl LocalChain {
|
|||||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||||
/// all of the same chain.
|
/// all of the same chain.
|
||||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
||||||
if !blocks.contains_key(&0) {
|
if blocks.get(&0).is_none() {
|
||||||
return Err(MissingGenesisError);
|
return Err(MissingGenesisError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tip: Option<CheckPoint> = None;
|
let mut tip: Option<CheckPoint> = None;
|
||||||
|
|
||||||
for block in &blocks {
|
for block in &blocks {
|
||||||
match tip {
|
match tip {
|
||||||
Some(curr) => {
|
Some(curr) => {
|
||||||
@@ -321,13 +335,9 @@ impl LocalChain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let chain = Self {
|
Ok(Self {
|
||||||
index: blocks,
|
|
||||||
tip: tip.expect("already checked to have genesis"),
|
tip: tip.expect("already checked to have genesis"),
|
||||||
};
|
})
|
||||||
|
|
||||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
|
||||||
Ok(chain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the highest checkpoint.
|
/// Get the highest checkpoint.
|
||||||
@@ -494,9 +504,7 @@ impl LocalChain {
|
|||||||
None => LocalChain::from_blocks(extension)?.tip(),
|
None => LocalChain::from_blocks(extension)?.tip(),
|
||||||
};
|
};
|
||||||
self.tip = new_tip;
|
self.tip = new_tip;
|
||||||
self.reindex(start_height);
|
|
||||||
|
|
||||||
debug_assert!(self._check_index_is_consistent_with_tip());
|
|
||||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,16 +517,16 @@ impl LocalChain {
|
|||||||
///
|
///
|
||||||
/// Replacing the block hash of an existing checkpoint will result in an error.
|
/// Replacing the block hash of an existing checkpoint will result in an error.
|
||||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
|
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
|
||||||
if let Some(&original_hash) = self.index.get(&block_id.height) {
|
if let Some(original_cp) = self.tip.get(block_id.height) {
|
||||||
|
let original_hash = original_cp.hash();
|
||||||
if original_hash != block_id.hash {
|
if original_hash != block_id.hash {
|
||||||
return Err(AlterCheckPointError {
|
return Err(AlterCheckPointError {
|
||||||
height: block_id.height,
|
height: block_id.height,
|
||||||
original_hash,
|
original_hash,
|
||||||
update_hash: Some(block_id.hash),
|
update_hash: Some(block_id.hash),
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return Ok(ChangeSet::default());
|
|
||||||
}
|
}
|
||||||
|
return Ok(ChangeSet::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut changeset = ChangeSet::default();
|
let mut changeset = ChangeSet::default();
|
||||||
@@ -542,33 +550,41 @@ impl LocalChain {
|
|||||||
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
|
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
|
||||||
/// genesis block.
|
/// genesis block.
|
||||||
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
|
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
|
||||||
if self.index.get(&block_id.height) != Some(&block_id.hash) {
|
let mut remove_from = Option::<CheckPoint>::None;
|
||||||
return Ok(ChangeSet::default());
|
let mut changeset = ChangeSet::default();
|
||||||
}
|
for cp in self.tip().iter() {
|
||||||
|
let cp_id = cp.block_id();
|
||||||
let changeset = self
|
if cp_id.height < block_id.height {
|
||||||
.index
|
|
||||||
.range(block_id.height..)
|
|
||||||
.map(|(&height, _)| (height, None))
|
|
||||||
.collect::<ChangeSet>();
|
|
||||||
self.apply_changeset(&changeset).map(|_| changeset)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reindex the heights in the chain from (and including) `from` height
|
|
||||||
fn reindex(&mut self, from: u32) {
|
|
||||||
let _ = self.index.split_off(&from);
|
|
||||||
for cp in self.iter_checkpoints() {
|
|
||||||
if cp.height() < from {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
self.index.insert(cp.height(), cp.hash());
|
changeset.insert(cp_id.height, None);
|
||||||
|
if cp_id == block_id {
|
||||||
|
remove_from = Some(cp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.tip = match remove_from.map(|cp| cp.prev()) {
|
||||||
|
// The checkpoint below the earliest checkpoint to remove will be the new tip.
|
||||||
|
Some(Some(new_tip)) => new_tip,
|
||||||
|
// If there is no checkpoint below the earliest checkpoint to remove, it means the
|
||||||
|
// "earliest checkpoint to remove" is the genesis block. We disallow removing the
|
||||||
|
// genesis block.
|
||||||
|
Some(None) => return Err(MissingGenesisError),
|
||||||
|
// If there is nothing to remove, we return an empty changeset.
|
||||||
|
None => return Ok(ChangeSet::default()),
|
||||||
|
};
|
||||||
|
Ok(changeset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
||||||
/// recover the current chain.
|
/// recover the current chain.
|
||||||
pub fn initial_changeset(&self) -> ChangeSet {
|
pub fn initial_changeset(&self) -> ChangeSet {
|
||||||
self.index.iter().map(|(k, v)| (*k, Some(*v))).collect()
|
self.tip
|
||||||
|
.iter()
|
||||||
|
.map(|cp| {
|
||||||
|
let block_id = cp.block_id();
|
||||||
|
(block_id.height, Some(block_id.hash))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over checkpoints in descending height order.
|
/// Iterate over checkpoints in descending height order.
|
||||||
@@ -578,28 +594,49 @@ impl LocalChain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the internal index mapping the height to block hash.
|
|
||||||
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
|
|
||||||
&self.index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _check_index_is_consistent_with_tip(&self) -> bool {
|
|
||||||
let tip_history = self
|
|
||||||
.tip
|
|
||||||
.iter()
|
|
||||||
.map(|cp| (cp.height(), cp.hash()))
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
self.index == tip_history
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
||||||
for (height, exp_hash) in changeset {
|
let mut curr_cp = self.tip.clone();
|
||||||
if self.index.get(height) != exp_hash.as_ref() {
|
for (height, exp_hash) in changeset.iter().rev() {
|
||||||
return false;
|
match curr_cp.get(*height) {
|
||||||
|
Some(query_cp) => {
|
||||||
|
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
curr_cp = query_cp;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if exp_hash.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get checkpoint at given `height` (if it exists).
|
||||||
|
///
|
||||||
|
/// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
|
||||||
|
///
|
||||||
|
/// [`tip`]: LocalChain::tip
|
||||||
|
pub fn get(&self, height: u32) -> Option<CheckPoint> {
|
||||||
|
self.tip.get(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate checkpoints over a height range.
|
||||||
|
///
|
||||||
|
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||||
|
/// height).
|
||||||
|
///
|
||||||
|
/// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
|
||||||
|
///
|
||||||
|
/// [`tip`]: LocalChain::tip
|
||||||
|
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||||
|
where
|
||||||
|
R: RangeBounds<u32>,
|
||||||
|
{
|
||||||
|
self.tip.range(range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||||
indexed_tx_graph::Indexer,
|
indexed_tx_graph::Indexer,
|
||||||
};
|
};
|
||||||
use bitcoin::{self, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
|
use bitcoin::{OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
|
||||||
|
|
||||||
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
|
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
|
||||||
///
|
///
|
||||||
@@ -281,12 +281,12 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
|
|||||||
|
|
||||||
for txin in &tx.input {
|
for txin in &tx.input {
|
||||||
if let Some((_, txout)) = self.txout(txin.previous_output) {
|
if let Some((_, txout)) = self.txout(txin.previous_output) {
|
||||||
sent += txout.value;
|
sent += txout.value.to_sat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for txout in &tx.output {
|
for txout in &tx.output {
|
||||||
if self.index_of_spk(&txout.script_pubkey).is_some() {
|
if self.index_of_spk(&txout.script_pubkey).is_some() {
|
||||||
received += txout.value;
|
received += txout.value.to_sat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
//! Module for structures that store and traverse transactions.
|
//! Module for structures that store and traverse transactions.
|
||||||
//!
|
//!
|
||||||
//! [`TxGraph`] contains transactions and indexes them so you can easily traverse the graph of those transactions.
|
//! [`TxGraph`] contains transactions and indexes them so you can easily traverse the graph of
|
||||||
//! `TxGraph` is *monotone* in that you can always insert a transaction -- it doesn't care whether that
|
//! those transactions. `TxGraph` is *monotone* in that you can always insert a transaction -- it
|
||||||
//! transaction is in the current best chain or whether it conflicts with any of the
|
//! does not care whether that transaction is in the current best chain or whether it conflicts with
|
||||||
//! existing transactions or what order you insert the transactions. This means that you can always
|
//! any of the existing transactions or what order you insert the transactions. This means that you
|
||||||
//! combine two [`TxGraph`]s together, without resulting in inconsistencies.
|
//! can always combine two [`TxGraph`]s together, without resulting in inconsistencies. Furthermore,
|
||||||
//! Furthermore, there is currently no way to delete a transaction.
|
//! there is currently no way to delete a transaction.
|
||||||
//!
|
//!
|
||||||
//! Transactions can be either whole or partial (i.e., transactions for which we only
|
//! Transactions can be either whole or partial (i.e., transactions for which we only know some
|
||||||
//! know some outputs, which we usually call "floating outputs"; these are usually inserted
|
//! outputs, which we usually call "floating outputs"; these are usually inserted using the
|
||||||
//! using the [`insert_txout`] method.).
|
//! [`insert_txout`] method.).
|
||||||
//!
|
//!
|
||||||
//! The graph contains transactions in the form of [`TxNode`]s. Each node contains the
|
//! The graph contains transactions in the form of [`TxNode`]s. Each node contains the txid, the
|
||||||
//! txid, the transaction (whole or partial), the blocks it's anchored in (see the [`Anchor`]
|
//! transaction (whole or partial), the blocks that it is anchored to (see the [`Anchor`]
|
||||||
//! documentation for more details), and the timestamp of the last time we saw
|
//! documentation for more details), and the timestamp of the last time we saw the transaction as
|
||||||
//! the transaction as unconfirmed.
|
//! unconfirmed.
|
||||||
//!
|
//!
|
||||||
//! Conflicting transactions are allowed to coexist within a [`TxGraph`]. This is useful for
|
//! Conflicting transactions are allowed to coexist within a [`TxGraph`]. This is useful for
|
||||||
//! identifying and traversing conflicts and descendants of a given transaction. Some [`TxGraph`]
|
//! identifying and traversing conflicts and descendants of a given transaction. Some [`TxGraph`]
|
||||||
//! methods only consider "canonical" (i.e., in the best chain or in mempool) transactions,
|
//! methods only consider transactions that are "canonical" (i.e., in the best chain or in mempool).
|
||||||
//! we decide which transactions are canonical based on anchors `last_seen_unconfirmed`;
|
//! We decide which transactions are canonical based on the transaction's anchors and the
|
||||||
//! see the [`try_get_chain_position`] documentation for more details.
|
//! `last_seen` (as unconfirmed) timestamp; see the [`try_get_chain_position`] documentation for
|
||||||
|
//! more details.
|
||||||
//!
|
//!
|
||||||
//! The [`ChangeSet`] reports changes made to a [`TxGraph`]; it can be used to either save to
|
//! The [`ChangeSet`] reports changes made to a [`TxGraph`]; it can be used to either save to
|
||||||
//! persistent storage, or to be applied to another [`TxGraph`].
|
//! persistent storage, or to be applied to another [`TxGraph`].
|
||||||
@@ -30,10 +31,22 @@
|
|||||||
//!
|
//!
|
||||||
//! # Applying changes
|
//! # Applying changes
|
||||||
//!
|
//!
|
||||||
//! Methods that apply changes to [`TxGraph`] will return [`ChangeSet`].
|
//! Methods that change the state of [`TxGraph`] will return [`ChangeSet`]s.
|
||||||
//! [`ChangeSet`] can be applied back to a [`TxGraph`] or be used to inform persistent storage
|
//! [`ChangeSet`]s can be applied back to a [`TxGraph`] or be used to inform persistent storage
|
||||||
//! of the changes to [`TxGraph`].
|
//! of the changes to [`TxGraph`].
|
||||||
//!
|
//!
|
||||||
|
//! # Generics
|
||||||
|
//!
|
||||||
|
//! Anchors are represented as generics within `TxGraph<A>`. To make use of all functionality of the
|
||||||
|
//! `TxGraph`, anchors (`A`) should implement [`Anchor`].
|
||||||
|
//!
|
||||||
|
//! Anchors are made generic so that different types of data can be stored with how a transaction is
|
||||||
|
//! *anchored* to a given block. An example of this is storing a merkle proof of the transaction to
|
||||||
|
//! the confirmation block - this can be done with a custom [`Anchor`] type. The minimal [`Anchor`]
|
||||||
|
//! type would just be a [`BlockId`] which just represents the height and hash of the block which
|
||||||
|
//! the transaction is contained in. Note that a transaction can be contained in multiple
|
||||||
|
//! conflicting blocks (by nature of the Bitcoin network).
|
||||||
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! # use bdk_chain::BlockId;
|
//! # use bdk_chain::BlockId;
|
||||||
//! # use bdk_chain::tx_graph::TxGraph;
|
//! # use bdk_chain::tx_graph::TxGraph;
|
||||||
@@ -80,6 +93,7 @@ use crate::{
|
|||||||
ChainOracle, ChainPosition, FullTxOut,
|
ChainOracle, ChainPosition, FullTxOut,
|
||||||
};
|
};
|
||||||
use alloc::collections::vec_deque::VecDeque;
|
use alloc::collections::vec_deque::VecDeque;
|
||||||
|
use alloc::sync::Arc;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||||
use core::fmt::{self, Formatter};
|
use core::fmt::{self, Formatter};
|
||||||
@@ -122,7 +136,7 @@ pub struct TxNode<'a, T, A> {
|
|||||||
/// Txid of the transaction.
|
/// Txid of the transaction.
|
||||||
pub txid: Txid,
|
pub txid: Txid,
|
||||||
/// A partial or full representation of the transaction.
|
/// A partial or full representation of the transaction.
|
||||||
pub tx: &'a T,
|
pub tx: T,
|
||||||
/// The blocks that the transaction is "anchored" in.
|
/// The blocks that the transaction is "anchored" in.
|
||||||
pub anchors: &'a BTreeSet<A>,
|
pub anchors: &'a BTreeSet<A>,
|
||||||
/// The last-seen unix timestamp of the transaction as unconfirmed.
|
/// The last-seen unix timestamp of the transaction as unconfirmed.
|
||||||
@@ -133,7 +147,7 @@ impl<'a, T, A> Deref for TxNode<'a, T, A> {
|
|||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
self.tx
|
&self.tx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +157,7 @@ impl<'a, T, A> Deref for TxNode<'a, T, A> {
|
|||||||
/// outputs).
|
/// outputs).
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
enum TxNodeInternal {
|
enum TxNodeInternal {
|
||||||
Whole(Transaction),
|
Whole(Arc<Transaction>),
|
||||||
Partial(BTreeMap<u32, TxOut>),
|
Partial(BTreeMap<u32, TxOut>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +212,7 @@ impl<A> TxGraph<A> {
|
|||||||
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||||
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
|
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
|
||||||
TxNodeInternal::Whole(tx) => tx
|
TxNodeInternal::Whole(tx) => tx
|
||||||
|
.as_ref()
|
||||||
.output
|
.output
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@@ -229,13 +244,13 @@ impl<A> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over all full transactions in the graph.
|
/// Iterate over all full transactions in the graph.
|
||||||
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Transaction, A>> {
|
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
|
||||||
self.txs
|
self.txs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
|
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
|
||||||
TxNodeInternal::Whole(tx) => Some(TxNode {
|
TxNodeInternal::Whole(tx) => Some(TxNode {
|
||||||
txid,
|
txid,
|
||||||
tx,
|
tx: tx.clone(),
|
||||||
anchors,
|
anchors,
|
||||||
last_seen_unconfirmed: *last_seen,
|
last_seen_unconfirmed: *last_seen,
|
||||||
}),
|
}),
|
||||||
@@ -248,16 +263,16 @@ impl<A> TxGraph<A> {
|
|||||||
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
|
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
|
||||||
///
|
///
|
||||||
/// [`get_txout`]: Self::get_txout
|
/// [`get_txout`]: Self::get_txout
|
||||||
pub fn get_tx(&self, txid: Txid) -> Option<&Transaction> {
|
pub fn get_tx(&self, txid: Txid) -> Option<Arc<Transaction>> {
|
||||||
self.get_tx_node(txid).map(|n| n.tx)
|
self.get_tx_node(txid).map(|n| n.tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a transaction node by txid. This only returns `Some` for full transactions.
|
/// Get a transaction node by txid. This only returns `Some` for full transactions.
|
||||||
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Transaction, A>> {
|
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
|
||||||
match &self.txs.get(&txid)? {
|
match &self.txs.get(&txid)? {
|
||||||
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
|
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
|
||||||
txid,
|
txid,
|
||||||
tx,
|
tx: tx.clone(),
|
||||||
anchors,
|
anchors,
|
||||||
last_seen_unconfirmed: *last_seen,
|
last_seen_unconfirmed: *last_seen,
|
||||||
}),
|
}),
|
||||||
@@ -268,7 +283,7 @@ impl<A> TxGraph<A> {
|
|||||||
/// Obtains a single tx output (if any) at the specified outpoint.
|
/// Obtains a single tx output (if any) at the specified outpoint.
|
||||||
pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> {
|
pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> {
|
||||||
match &self.txs.get(&outpoint.txid)?.0 {
|
match &self.txs.get(&outpoint.txid)?.0 {
|
||||||
TxNodeInternal::Whole(tx) => tx.output.get(outpoint.vout as usize),
|
TxNodeInternal::Whole(tx) => tx.as_ref().output.get(outpoint.vout as usize),
|
||||||
TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout),
|
TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,6 +294,7 @@ impl<A> TxGraph<A> {
|
|||||||
pub fn tx_outputs(&self, txid: Txid) -> Option<BTreeMap<u32, &TxOut>> {
|
pub fn tx_outputs(&self, txid: Txid) -> Option<BTreeMap<u32, &TxOut>> {
|
||||||
Some(match &self.txs.get(&txid)?.0 {
|
Some(match &self.txs.get(&txid)?.0 {
|
||||||
TxNodeInternal::Whole(tx) => tx
|
TxNodeInternal::Whole(tx) => tx
|
||||||
|
.as_ref()
|
||||||
.output
|
.output
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@@ -303,7 +319,7 @@ impl<A> TxGraph<A> {
|
|||||||
///
|
///
|
||||||
/// [`insert_txout`]: Self::insert_txout
|
/// [`insert_txout`]: Self::insert_txout
|
||||||
pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> {
|
pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> {
|
||||||
if tx.is_coin_base() {
|
if tx.is_coinbase() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +331,7 @@ impl<A> TxGraph<A> {
|
|||||||
(sum, missing_outpoints)
|
(sum, missing_outpoints)
|
||||||
}
|
}
|
||||||
Some(txout) => {
|
Some(txout) => {
|
||||||
sum += txout.value as i64;
|
sum += txout.value.to_sat() as i64;
|
||||||
(sum, missing_outpoints)
|
(sum, missing_outpoints)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -327,7 +343,7 @@ impl<A> TxGraph<A> {
|
|||||||
let outputs_sum = tx
|
let outputs_sum = tx
|
||||||
.output
|
.output
|
||||||
.iter()
|
.iter()
|
||||||
.map(|txout| txout.value as i64)
|
.map(|txout| txout.value.to_sat() as i64)
|
||||||
.sum::<i64>();
|
.sum::<i64>();
|
||||||
|
|
||||||
let fee = inputs_sum - outputs_sum;
|
let fee = inputs_sum - outputs_sum;
|
||||||
@@ -356,16 +372,15 @@ impl<A> TxGraph<A> {
|
|||||||
&self,
|
&self,
|
||||||
txid: Txid,
|
txid: Txid,
|
||||||
) -> impl DoubleEndedIterator<Item = (u32, &HashSet<Txid>)> + '_ {
|
) -> impl DoubleEndedIterator<Item = (u32, &HashSet<Txid>)> + '_ {
|
||||||
let start = OutPoint { txid, vout: 0 };
|
let start = OutPoint::new(txid, 0);
|
||||||
let end = OutPoint {
|
let end = OutPoint::new(txid, u32::MAX);
|
||||||
txid,
|
|
||||||
vout: u32::MAX,
|
|
||||||
};
|
|
||||||
self.spends
|
self.spends
|
||||||
.range(start..=end)
|
.range(start..=end)
|
||||||
.map(|(outpoint, spends)| (outpoint.vout, spends))
|
.map(|(outpoint, spends)| (outpoint.vout, spends))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Clone + Ord> TxGraph<A> {
|
||||||
/// Creates an iterator that filters and maps ancestor transactions.
|
/// Creates an iterator that filters and maps ancestor transactions.
|
||||||
///
|
///
|
||||||
/// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx`
|
/// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx`
|
||||||
@@ -379,13 +394,10 @@ impl<A> TxGraph<A> {
|
|||||||
///
|
///
|
||||||
/// The supplied closure returns an `Option<T>`, allowing the caller to map each `Transaction`
|
/// The supplied closure returns an `Option<T>`, allowing the caller to map each `Transaction`
|
||||||
/// it visits and decide whether to visit ancestors.
|
/// it visits and decide whether to visit ancestors.
|
||||||
pub fn walk_ancestors<'g, F, O>(
|
pub fn walk_ancestors<'g, T, F, O>(&'g self, tx: T, walk_map: F) -> TxAncestors<'g, A, F>
|
||||||
&'g self,
|
|
||||||
tx: &'g Transaction,
|
|
||||||
walk_map: F,
|
|
||||||
) -> TxAncestors<'g, A, F>
|
|
||||||
where
|
where
|
||||||
F: FnMut(usize, &'g Transaction) -> Option<O> + 'g,
|
T: Into<Arc<Transaction>>,
|
||||||
|
F: FnMut(usize, Arc<Transaction>) -> Option<O> + 'g,
|
||||||
{
|
{
|
||||||
TxAncestors::new_exclude_root(self, tx, walk_map)
|
TxAncestors::new_exclude_root(self, tx, walk_map)
|
||||||
}
|
}
|
||||||
@@ -406,7 +418,9 @@ impl<A> TxGraph<A> {
|
|||||||
{
|
{
|
||||||
TxDescendants::new_exclude_root(self, txid, walk_map)
|
TxDescendants::new_exclude_root(self, txid, walk_map)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> TxGraph<A> {
|
||||||
/// Creates an iterator that both filters and maps conflicting transactions (this includes
|
/// Creates an iterator that both filters and maps conflicting transactions (this includes
|
||||||
/// descendants of directly-conflicting transactions, which are also considered conflicts).
|
/// descendants of directly-conflicting transactions, which are also considered conflicts).
|
||||||
///
|
///
|
||||||
@@ -419,7 +433,7 @@ impl<A> TxGraph<A> {
|
|||||||
where
|
where
|
||||||
F: FnMut(usize, Txid) -> Option<O> + 'g,
|
F: FnMut(usize, Txid) -> Option<O> + 'g,
|
||||||
{
|
{
|
||||||
let txids = self.direct_conflitcs(tx).map(|(_, txid)| txid);
|
let txids = self.direct_conflicts(tx).map(|(_, txid)| txid);
|
||||||
TxDescendants::from_multiple_include_root(self, txids, walk_map)
|
TxDescendants::from_multiple_include_root(self, txids, walk_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +444,7 @@ impl<A> TxGraph<A> {
|
|||||||
/// Note that this only returns directly conflicting txids and won't include:
|
/// Note that this only returns directly conflicting txids and won't include:
|
||||||
/// - descendants of conflicting transactions (which are technically also conflicting)
|
/// - descendants of conflicting transactions (which are technically also conflicting)
|
||||||
/// - transactions conflicting with the given transaction's ancestors
|
/// - transactions conflicting with the given transaction's ancestors
|
||||||
pub fn direct_conflitcs<'g>(
|
pub fn direct_conflicts<'g>(
|
||||||
&'g self,
|
&'g self,
|
||||||
tx: &'g Transaction,
|
tx: &'g Transaction,
|
||||||
) -> impl Iterator<Item = (usize, Txid)> + '_ {
|
) -> impl Iterator<Item = (usize, Txid)> + '_ {
|
||||||
@@ -455,6 +469,19 @@ impl<A> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Clone + Ord> TxGraph<A> {
|
impl<A: Clone + Ord> TxGraph<A> {
|
||||||
|
/// Transform the [`TxGraph`] to have [`Anchor`]s of another type.
|
||||||
|
///
|
||||||
|
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||||
|
/// transform it.
|
||||||
|
pub fn map_anchors<A2: Clone + Ord, F>(self, f: F) -> TxGraph<A2>
|
||||||
|
where
|
||||||
|
F: FnMut(A) -> A2,
|
||||||
|
{
|
||||||
|
let mut new_graph = TxGraph::<A2>::default();
|
||||||
|
new_graph.apply_changeset(self.initial_changeset().map_anchors(f));
|
||||||
|
new_graph
|
||||||
|
}
|
||||||
|
|
||||||
/// Construct a new [`TxGraph`] from a list of transactions.
|
/// Construct a new [`TxGraph`] from a list of transactions.
|
||||||
pub fn new(txs: impl IntoIterator<Item = Transaction>) -> Self {
|
pub fn new(txs: impl IntoIterator<Item = Transaction>) -> Self {
|
||||||
let mut new = Self::default();
|
let mut new = Self::default();
|
||||||
@@ -491,9 +518,10 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
|
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
|
||||||
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A> {
|
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A> {
|
||||||
let mut update = Self::default();
|
let mut update = Self::default();
|
||||||
update
|
update.txs.insert(
|
||||||
.txs
|
tx.txid(),
|
||||||
.insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
|
(TxNodeInternal::Whole(tx.into()), BTreeSet::new(), 0),
|
||||||
|
);
|
||||||
self.apply_update(update)
|
self.apply_update(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +554,11 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
|
|
||||||
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
|
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
|
||||||
///
|
///
|
||||||
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
|
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch
|
||||||
|
/// update all unconfirmed transactions with the latest `seen_at`, see
|
||||||
|
/// [`update_last_seen_unconfirmed`].
|
||||||
|
///
|
||||||
|
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
|
||||||
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
|
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
|
||||||
let mut update = Self::default();
|
let mut update = Self::default();
|
||||||
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
|
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
|
||||||
@@ -534,6 +566,65 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
self.apply_update(update)
|
self.apply_update(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the last seen time for all unconfirmed transactions.
|
||||||
|
///
|
||||||
|
/// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting
|
||||||
|
/// the given `seen_at` for every transaction not yet anchored to a confirmed block,
|
||||||
|
/// and returns the [`ChangeSet`] after applying all updates to `self`.
|
||||||
|
///
|
||||||
|
/// This is useful for keeping track of the latest time a transaction was seen
|
||||||
|
/// unconfirmed, which is important for evaluating transaction conflicts in the same
|
||||||
|
/// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for
|
||||||
|
/// [`try_get_chain_position`].
|
||||||
|
///
|
||||||
|
/// A normal use of this method is to call it with the current system time. Although
|
||||||
|
/// block headers contain a timestamp, using the header time would be less effective
|
||||||
|
/// at tracking mempool transactions, because it can drift from actual clock time, plus
|
||||||
|
/// we may want to update a transaction's last seen time repeatedly between blocks.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use bdk_chain::example_utils::*;
|
||||||
|
/// # use std::time::UNIX_EPOCH;
|
||||||
|
/// # let tx = tx_from_hex(RAW_TX_1);
|
||||||
|
/// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]);
|
||||||
|
/// let now = std::time::SystemTime::now()
|
||||||
|
/// .duration_since(UNIX_EPOCH)
|
||||||
|
/// .expect("valid duration")
|
||||||
|
/// .as_secs();
|
||||||
|
/// let changeset = tx_graph.update_last_seen_unconfirmed(now);
|
||||||
|
/// assert!(!changeset.last_seen.is_empty());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must
|
||||||
|
/// by strictly greater than what is currently stored for a transaction to have an effect.
|
||||||
|
/// To insert a last seen time for a single txid, see [`insert_seen_at`].
|
||||||
|
///
|
||||||
|
/// [`insert_seen_at`]: Self::insert_seen_at
|
||||||
|
/// [`try_get_chain_position`]: Self::try_get_chain_position
|
||||||
|
pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet<A> {
|
||||||
|
let mut changeset = ChangeSet::default();
|
||||||
|
let unanchored_txs: Vec<Txid> = self
|
||||||
|
.txs
|
||||||
|
.iter()
|
||||||
|
.filter_map(
|
||||||
|
|(&txid, (_, anchors, _))| {
|
||||||
|
if anchors.is_empty() {
|
||||||
|
Some(txid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for txid in unanchored_txs {
|
||||||
|
changeset.append(self.insert_seen_at(txid, seen_at));
|
||||||
|
}
|
||||||
|
changeset
|
||||||
|
}
|
||||||
|
|
||||||
/// Extends this graph with another so that `self` becomes the union of the two sets of
|
/// Extends this graph with another so that `self` becomes the union of the two sets of
|
||||||
/// transactions.
|
/// transactions.
|
||||||
///
|
///
|
||||||
@@ -552,7 +643,8 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
|
|
||||||
/// Applies [`ChangeSet`] to [`TxGraph`].
|
/// Applies [`ChangeSet`] to [`TxGraph`].
|
||||||
pub fn apply_changeset(&mut self, changeset: ChangeSet<A>) {
|
pub fn apply_changeset(&mut self, changeset: ChangeSet<A>) {
|
||||||
for tx in changeset.txs {
|
for wrapped_tx in changeset.txs {
|
||||||
|
let tx = wrapped_tx.as_ref();
|
||||||
let txid = tx.txid();
|
let txid = tx.txid();
|
||||||
|
|
||||||
tx.input
|
tx.input
|
||||||
@@ -567,18 +659,20 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
|
|
||||||
match self.txs.get_mut(&txid) {
|
match self.txs.get_mut(&txid) {
|
||||||
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
|
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
|
||||||
*tx_node = TxNodeInternal::Whole(tx);
|
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
|
||||||
}
|
}
|
||||||
Some((TxNodeInternal::Whole(tx), _, _)) => {
|
Some((TxNodeInternal::Whole(tx), _, _)) => {
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
tx.txid(),
|
tx.as_ref().txid(),
|
||||||
txid,
|
txid,
|
||||||
"tx should produce txid that is same as key"
|
"tx should produce txid that is same as key"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.txs
|
self.txs.insert(
|
||||||
.insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
|
txid,
|
||||||
|
(TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -615,7 +709,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
/// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that
|
/// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that
|
||||||
/// exist in `update` but not in `self`).
|
/// exist in `update` but not in `self`).
|
||||||
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
|
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
|
||||||
let mut changeset = ChangeSet::default();
|
let mut changeset = ChangeSet::<A>::default();
|
||||||
|
|
||||||
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
|
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
|
||||||
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
|
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
|
||||||
@@ -694,13 +788,13 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
};
|
};
|
||||||
let mut has_missing_height = false;
|
let mut has_missing_height = false;
|
||||||
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
|
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
|
||||||
match chain.blocks().get(&anchor_block.height) {
|
match chain.get(anchor_block.height) {
|
||||||
None => {
|
None => {
|
||||||
has_missing_height = true;
|
has_missing_height = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Some(chain_hash) => {
|
Some(chain_cp) => {
|
||||||
if chain_hash == &anchor_block.hash {
|
if chain_cp.hash() == anchor_block.hash {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,7 +812,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
.filter_map(move |(a, _)| {
|
.filter_map(move |(a, _)| {
|
||||||
let anchor_block = a.anchor_block();
|
let anchor_block = a.anchor_block();
|
||||||
if Some(anchor_block.height) != last_height_emitted
|
if Some(anchor_block.height) != last_height_emitted
|
||||||
&& !chain.blocks().contains_key(&anchor_block.height)
|
&& chain.get(anchor_block.height).is_none()
|
||||||
{
|
{
|
||||||
last_height_emitted = Some(anchor_block.height);
|
last_height_emitted = Some(anchor_block.height);
|
||||||
Some(anchor_block.height)
|
Some(anchor_block.height)
|
||||||
@@ -776,10 +870,10 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
TxNodeInternal::Whole(tx) => {
|
TxNodeInternal::Whole(tx) => {
|
||||||
// A coinbase tx that is not anchored in the best chain cannot be unconfirmed and
|
// A coinbase tx that is not anchored in the best chain cannot be unconfirmed and
|
||||||
// should always be filtered out.
|
// should always be filtered out.
|
||||||
if tx.is_coin_base() {
|
if tx.is_coinbase() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
tx
|
tx.clone()
|
||||||
}
|
}
|
||||||
TxNodeInternal::Partial(_) => {
|
TxNodeInternal::Partial(_) => {
|
||||||
// Partial transactions (outputs only) cannot have conflicts.
|
// Partial transactions (outputs only) cannot have conflicts.
|
||||||
@@ -796,8 +890,8 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
// First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
|
// First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
|
||||||
// resulting array will also include `tx`
|
// resulting array will also include `tx`
|
||||||
let unconfirmed_ancestor_txs =
|
let unconfirmed_ancestor_txs =
|
||||||
TxAncestors::new_include_root(self, tx, |_, ancestor_tx: &Transaction| {
|
TxAncestors::new_include_root(self, tx.clone(), |_, ancestor_tx: Arc<Transaction>| {
|
||||||
let tx_node = self.get_tx_node(ancestor_tx.txid())?;
|
let tx_node = self.get_tx_node(ancestor_tx.as_ref().txid())?;
|
||||||
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
||||||
// the best chain)
|
// the best chain)
|
||||||
for block in tx_node.anchors {
|
for block in tx_node.anchors {
|
||||||
@@ -813,8 +907,10 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
|
|
||||||
// We determine our tx's last seen, which is the max between our last seen,
|
// We determine our tx's last seen, which is the max between our last seen,
|
||||||
// and our unconf descendants' last seen.
|
// and our unconf descendants' last seen.
|
||||||
let unconfirmed_descendants_txs =
|
let unconfirmed_descendants_txs = TxDescendants::new_include_root(
|
||||||
TxDescendants::new_include_root(self, tx.txid(), |_, descendant_txid: Txid| {
|
self,
|
||||||
|
tx.as_ref().txid(),
|
||||||
|
|_, descendant_txid: Txid| {
|
||||||
let tx_node = self.get_tx_node(descendant_txid)?;
|
let tx_node = self.get_tx_node(descendant_txid)?;
|
||||||
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
||||||
// the best chain)
|
// the best chain)
|
||||||
@@ -826,8 +922,9 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(tx_node))
|
Some(Ok(tx_node))
|
||||||
})
|
},
|
||||||
.collect::<Result<Vec<_>, C::Error>>()?;
|
)
|
||||||
|
.collect::<Result<Vec<_>, C::Error>>()?;
|
||||||
|
|
||||||
let tx_last_seen = unconfirmed_descendants_txs
|
let tx_last_seen = unconfirmed_descendants_txs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -838,7 +935,8 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
// Now we traverse our ancestors and consider all their conflicts
|
// Now we traverse our ancestors and consider all their conflicts
|
||||||
for tx_node in unconfirmed_ancestor_txs {
|
for tx_node in unconfirmed_ancestor_txs {
|
||||||
// We retrieve all the transactions conflicting with this specific ancestor
|
// We retrieve all the transactions conflicting with this specific ancestor
|
||||||
let conflicting_txs = self.walk_conflicts(tx_node.tx, |_, txid| self.get_tx_node(txid));
|
let conflicting_txs =
|
||||||
|
self.walk_conflicts(tx_node.tx.as_ref(), |_, txid| self.get_tx_node(txid));
|
||||||
|
|
||||||
// If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
|
// If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
|
||||||
// this tx cannot exist in the best chain
|
// this tx cannot exist in the best chain
|
||||||
@@ -852,7 +950,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if conflicting_tx.last_seen_unconfirmed == *last_seen
|
if conflicting_tx.last_seen_unconfirmed == *last_seen
|
||||||
&& conflicting_tx.txid() > tx.txid()
|
&& conflicting_tx.as_ref().txid() > tx.as_ref().txid()
|
||||||
{
|
{
|
||||||
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
|
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -945,7 +1043,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
&'a self,
|
&'a self,
|
||||||
chain: &'a C,
|
chain: &'a C,
|
||||||
chain_tip: BlockId,
|
chain_tip: BlockId,
|
||||||
) -> impl Iterator<Item = Result<CanonicalTx<'a, Transaction, A>, C::Error>> {
|
) -> impl Iterator<Item = Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error>> {
|
||||||
self.full_txs().filter_map(move |tx| {
|
self.full_txs().filter_map(move |tx| {
|
||||||
self.try_get_chain_position(chain, chain_tip, tx.txid)
|
self.try_get_chain_position(chain, chain_tip, tx.txid)
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
@@ -967,7 +1065,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
&'a self,
|
&'a self,
|
||||||
chain: &'a C,
|
chain: &'a C,
|
||||||
chain_tip: BlockId,
|
chain_tip: BlockId,
|
||||||
) -> impl Iterator<Item = CanonicalTx<'a, Transaction, A>> {
|
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
|
||||||
self.try_list_chain_txs(chain, chain_tip)
|
self.try_list_chain_txs(chain, chain_tip)
|
||||||
.map(|r| r.expect("oracle is infallible"))
|
.map(|r| r.expect("oracle is infallible"))
|
||||||
}
|
}
|
||||||
@@ -1006,7 +1104,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let txout = match tx_node.tx.output.get(op.vout as usize) {
|
let txout = match tx_node.tx.as_ref().output.get(op.vout as usize) {
|
||||||
Some(txout) => txout.clone(),
|
Some(txout) => txout.clone(),
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
@@ -1028,7 +1126,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
txout,
|
txout,
|
||||||
chain_position,
|
chain_position,
|
||||||
spent_by,
|
spent_by,
|
||||||
is_on_coinbase: tx_node.tx.is_coin_base(),
|
is_on_coinbase: tx_node.tx.is_coinbase(),
|
||||||
},
|
},
|
||||||
)))
|
)))
|
||||||
},
|
},
|
||||||
@@ -1131,16 +1229,16 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
match &txout.chain_position {
|
match &txout.chain_position {
|
||||||
ChainPosition::Confirmed(_) => {
|
ChainPosition::Confirmed(_) => {
|
||||||
if txout.is_confirmed_and_spendable(chain_tip.height) {
|
if txout.is_confirmed_and_spendable(chain_tip.height) {
|
||||||
confirmed += txout.txout.value;
|
confirmed += txout.txout.value.to_sat();
|
||||||
} else if !txout.is_mature(chain_tip.height) {
|
} else if !txout.is_mature(chain_tip.height) {
|
||||||
immature += txout.txout.value;
|
immature += txout.txout.value.to_sat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChainPosition::Unconfirmed(_) => {
|
ChainPosition::Unconfirmed(_) => {
|
||||||
if trust_predicate(&spk_i, &txout.txout.script_pubkey) {
|
if trust_predicate(&spk_i, &txout.txout.script_pubkey) {
|
||||||
trusted_pending += txout.txout.value;
|
trusted_pending += txout.txout.value.to_sat();
|
||||||
} else {
|
} else {
|
||||||
untrusted_pending += txout.txout.value;
|
untrusted_pending += txout.txout.value.to_sat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1194,7 +1292,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct ChangeSet<A = ()> {
|
pub struct ChangeSet<A = ()> {
|
||||||
/// Added transactions.
|
/// Added transactions.
|
||||||
pub txs: BTreeSet<Transaction>,
|
pub txs: BTreeSet<Arc<Transaction>>,
|
||||||
/// Added txouts.
|
/// Added txouts.
|
||||||
pub txouts: BTreeMap<OutPoint, TxOut>,
|
pub txouts: BTreeMap<OutPoint, TxOut>,
|
||||||
/// Added anchors.
|
/// Added anchors.
|
||||||
@@ -1264,7 +1362,7 @@ impl<A> ChangeSet<A> {
|
|||||||
A: Anchor,
|
A: Anchor,
|
||||||
{
|
{
|
||||||
self.anchor_heights()
|
self.anchor_heights()
|
||||||
.filter(move |height| !local_chain.blocks().contains_key(height))
|
.filter(move |&height| local_chain.get(height).is_none())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1294,6 +1392,26 @@ impl<A: Ord> Append for ChangeSet<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<A: Ord> ChangeSet<A> {
|
||||||
|
/// Transform the [`ChangeSet`] to have [`Anchor`]s of another type.
|
||||||
|
///
|
||||||
|
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||||
|
/// transform it.
|
||||||
|
pub fn map_anchors<A2: Ord, F>(self, mut f: F) -> ChangeSet<A2>
|
||||||
|
where
|
||||||
|
F: FnMut(A) -> A2,
|
||||||
|
{
|
||||||
|
ChangeSet {
|
||||||
|
txs: self.txs,
|
||||||
|
txouts: self.txouts,
|
||||||
|
anchors: BTreeSet::<(A2, Txid)>::from_iter(
|
||||||
|
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
|
||||||
|
),
|
||||||
|
last_seen: self.last_seen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
||||||
fn as_ref(&self) -> &TxGraph<A> {
|
fn as_ref(&self) -> &TxGraph<A> {
|
||||||
self
|
self
|
||||||
@@ -1310,7 +1428,7 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
|||||||
pub struct TxAncestors<'g, A, F> {
|
pub struct TxAncestors<'g, A, F> {
|
||||||
graph: &'g TxGraph<A>,
|
graph: &'g TxGraph<A>,
|
||||||
visited: HashSet<Txid>,
|
visited: HashSet<Txid>,
|
||||||
queue: VecDeque<(usize, &'g Transaction)>,
|
queue: VecDeque<(usize, Arc<Transaction>)>,
|
||||||
filter_map: F,
|
filter_map: F,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,13 +1436,13 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
|||||||
/// Creates a `TxAncestors` that includes the starting `Transaction` when iterating.
|
/// Creates a `TxAncestors` that includes the starting `Transaction` when iterating.
|
||||||
pub(crate) fn new_include_root(
|
pub(crate) fn new_include_root(
|
||||||
graph: &'g TxGraph<A>,
|
graph: &'g TxGraph<A>,
|
||||||
tx: &'g Transaction,
|
tx: impl Into<Arc<Transaction>>,
|
||||||
filter_map: F,
|
filter_map: F,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
graph,
|
graph,
|
||||||
visited: Default::default(),
|
visited: Default::default(),
|
||||||
queue: [(0, tx)].into(),
|
queue: [(0, tx.into())].into(),
|
||||||
filter_map,
|
filter_map,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1332,7 +1450,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
|||||||
/// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating.
|
/// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating.
|
||||||
pub(crate) fn new_exclude_root(
|
pub(crate) fn new_exclude_root(
|
||||||
graph: &'g TxGraph<A>,
|
graph: &'g TxGraph<A>,
|
||||||
tx: &'g Transaction,
|
tx: impl Into<Arc<Transaction>>,
|
||||||
filter_map: F,
|
filter_map: F,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut ancestors = Self {
|
let mut ancestors = Self {
|
||||||
@@ -1341,7 +1459,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
|||||||
queue: Default::default(),
|
queue: Default::default(),
|
||||||
filter_map,
|
filter_map,
|
||||||
};
|
};
|
||||||
ancestors.populate_queue(1, tx);
|
ancestors.populate_queue(1, tx.into());
|
||||||
ancestors
|
ancestors
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1354,12 +1472,13 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
|||||||
filter_map: F,
|
filter_map: F,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = &'g Transaction>,
|
I: IntoIterator,
|
||||||
|
I::Item: Into<Arc<Transaction>>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
graph,
|
graph,
|
||||||
visited: Default::default(),
|
visited: Default::default(),
|
||||||
queue: txs.into_iter().map(|tx| (0, tx)).collect(),
|
queue: txs.into_iter().map(|tx| (0, tx.into())).collect(),
|
||||||
filter_map,
|
filter_map,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1373,7 +1492,8 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
|||||||
filter_map: F,
|
filter_map: F,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = &'g Transaction>,
|
I: IntoIterator,
|
||||||
|
I::Item: Into<Arc<Transaction>>,
|
||||||
{
|
{
|
||||||
let mut ancestors = Self {
|
let mut ancestors = Self {
|
||||||
graph,
|
graph,
|
||||||
@@ -1382,12 +1502,12 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
|||||||
filter_map,
|
filter_map,
|
||||||
};
|
};
|
||||||
for tx in txs {
|
for tx in txs {
|
||||||
ancestors.populate_queue(1, tx);
|
ancestors.populate_queue(1, tx.into());
|
||||||
}
|
}
|
||||||
ancestors
|
ancestors
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populate_queue(&mut self, depth: usize, tx: &'g Transaction) {
|
fn populate_queue(&mut self, depth: usize, tx: Arc<Transaction>) {
|
||||||
let ancestors = tx
|
let ancestors = tx
|
||||||
.input
|
.input
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1401,7 +1521,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
|||||||
|
|
||||||
impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F>
|
impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F>
|
||||||
where
|
where
|
||||||
F: FnMut(usize, &'g Transaction) -> Option<O>,
|
F: FnMut(usize, Arc<Transaction>) -> Option<O>,
|
||||||
{
|
{
|
||||||
type Item = O;
|
type Item = O;
|
||||||
|
|
||||||
@@ -1410,7 +1530,7 @@ where
|
|||||||
// we have exhausted all paths when queue is empty
|
// we have exhausted all paths when queue is empty
|
||||||
let (ancestor_depth, tx) = self.queue.pop_front()?;
|
let (ancestor_depth, tx) = self.queue.pop_front()?;
|
||||||
// ignore paths when user filters them out
|
// ignore paths when user filters them out
|
||||||
let item = match (self.filter_map)(ancestor_depth, tx) {
|
let item = match (self.filter_map)(ancestor_depth, tx.clone()) {
|
||||||
Some(item) => item,
|
Some(item) => item,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ macro_rules! changeset {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn new_tx(lt: u32) -> bitcoin::Transaction {
|
pub fn new_tx(lt: u32) -> bitcoin::Transaction {
|
||||||
bitcoin::Transaction {
|
bitcoin::Transaction {
|
||||||
version: 0x00,
|
version: bitcoin::transaction::Version::non_standard(0x00),
|
||||||
lock_time: bitcoin::absolute::LockTime::from_consensus(lt),
|
lock_time: bitcoin::absolute::LockTime::from_consensus(lt),
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use rand::distributions::{Alphanumeric, DistString};
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex};
|
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction,
|
locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
|
||||||
TxIn, TxOut, Txid, Witness,
|
Sequence, Transaction, TxIn, TxOut, Txid, Witness,
|
||||||
};
|
};
|
||||||
use miniscript::Descriptor;
|
use miniscript::Descriptor;
|
||||||
|
|
||||||
@@ -49,11 +49,11 @@ impl TxOutTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn init_graph<'a>(
|
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, BlockId>>,
|
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
|
||||||
) -> (TxGraph<BlockId>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
) -> (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(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||||
let mut graph = TxGraph::<BlockId>::default();
|
let mut graph = TxGraph::<A>::default();
|
||||||
let mut spk_index = SpkTxOutIndex::default();
|
let mut spk_index = SpkTxOutIndex::default();
|
||||||
(0..10).for_each(|index| {
|
(0..10).for_each(|index| {
|
||||||
spk_index.insert_spk(
|
spk_index.insert_spk(
|
||||||
@@ -68,7 +68,7 @@ pub fn init_graph<'a>(
|
|||||||
|
|
||||||
for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() {
|
for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() {
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 0,
|
version: transaction::Version::non_standard(0),
|
||||||
lock_time: LockTime::ZERO,
|
lock_time: LockTime::ZERO,
|
||||||
input: tx_tmp
|
input: tx_tmp
|
||||||
.inputs
|
.inputs
|
||||||
@@ -111,11 +111,11 @@ pub fn init_graph<'a>(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|output| match &output.spk_index {
|
.map(|output| match &output.spk_index {
|
||||||
None => TxOut {
|
None => TxOut {
|
||||||
value: output.value,
|
value: Amount::from_sat(output.value),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
Some(index) => TxOut {
|
Some(index) => TxOut {
|
||||||
value: output.value,
|
value: Amount::from_sat(output.value),
|
||||||
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
|
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -126,7 +126,7 @@ pub fn init_graph<'a>(
|
|||||||
spk_index.scan(&tx);
|
spk_index.scan(&tx);
|
||||||
let _ = graph.insert_tx(tx.clone());
|
let _ = graph.insert_tx(tx.clone());
|
||||||
for anchor in tx_tmp.anchors.iter() {
|
for anchor in tx_tmp.anchors.iter() {
|
||||||
let _ = graph.insert_anchor(tx.txid(), *anchor);
|
let _ = graph.insert_anchor(tx.txid(), anchor.clone());
|
||||||
}
|
}
|
||||||
if let Some(seen_at) = tx_tmp.last_seen {
|
if let Some(seen_at) = tx_tmp.last_seen {
|
||||||
let _ = graph.insert_seen_at(tx.txid(), seen_at);
|
let _ = graph.insert_seen_at(tx.txid(), seen_at);
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::{collections::BTreeSet, sync::Arc};
|
||||||
|
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
indexed_tx_graph::{self, IndexedTxGraph},
|
indexed_tx_graph::{self, IndexedTxGraph},
|
||||||
keychain::{self, Balance, KeychainTxOutIndex},
|
keychain::{self, Balance, KeychainTxOutIndex},
|
||||||
local_chain::LocalChain,
|
local_chain::LocalChain,
|
||||||
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
tx_graph, ChainPosition, ConfirmationHeightAnchor,
|
||||||
|
};
|
||||||
|
use bitcoin::{
|
||||||
|
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
|
||||||
};
|
};
|
||||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
|
|
||||||
use miniscript::Descriptor;
|
use miniscript::Descriptor;
|
||||||
|
|
||||||
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
||||||
@@ -35,11 +37,11 @@ fn insert_relevant_txs() {
|
|||||||
let tx_a = Transaction {
|
let tx_a = Transaction {
|
||||||
output: vec![
|
output: vec![
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
script_pubkey: spk_0,
|
script_pubkey: spk_0,
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 20_000,
|
value: Amount::from_sat(20_000),
|
||||||
script_pubkey: spk_1,
|
script_pubkey: spk_1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -66,7 +68,7 @@ fn insert_relevant_txs() {
|
|||||||
|
|
||||||
let changeset = indexed_tx_graph::ChangeSet {
|
let changeset = indexed_tx_graph::ChangeSet {
|
||||||
graph: tx_graph::ChangeSet {
|
graph: tx_graph::ChangeSet {
|
||||||
txs: txs.clone().into(),
|
txs: txs.iter().cloned().map(Arc::new).collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
indexer: keychain::ChangeSet([((), 9_u32)].into()),
|
indexer: keychain::ChangeSet([((), 9_u32)].into()),
|
||||||
@@ -80,7 +82,6 @@ fn insert_relevant_txs() {
|
|||||||
assert_eq!(graph.initial_changeset(), changeset,);
|
assert_eq!(graph.initial_changeset(), changeset,);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
|
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
|
||||||
/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain).
|
/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain).
|
||||||
///
|
///
|
||||||
@@ -108,7 +109,7 @@ fn insert_relevant_txs() {
|
|||||||
///
|
///
|
||||||
/// Finally Add more blocks to local chain until tx1 coinbase maturity hits.
|
/// Finally Add more blocks to local chain until tx1 coinbase maturity hits.
|
||||||
/// Assert maturity at coinbase maturity inflection height. Block height 98 and 99.
|
/// Assert maturity at coinbase maturity inflection height. Block height 98 and 99.
|
||||||
|
#[test]
|
||||||
fn test_list_owned_txouts() {
|
fn test_list_owned_txouts() {
|
||||||
// Create Local chains
|
// Create Local chains
|
||||||
let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
|
let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
|
||||||
@@ -155,7 +156,7 @@ fn test_list_owned_txouts() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 70000,
|
value: Amount::from_sat(70000),
|
||||||
script_pubkey: trusted_spks[0].to_owned(),
|
script_pubkey: trusted_spks[0].to_owned(),
|
||||||
}],
|
}],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
@@ -164,7 +165,7 @@ fn test_list_owned_txouts() {
|
|||||||
// tx2 is an incoming transaction received at untrusted keychain at block 1.
|
// tx2 is an incoming transaction received at untrusted keychain at block 1.
|
||||||
let tx2 = Transaction {
|
let tx2 = Transaction {
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 30000,
|
value: Amount::from_sat(30000),
|
||||||
script_pubkey: untrusted_spks[0].to_owned(),
|
script_pubkey: untrusted_spks[0].to_owned(),
|
||||||
}],
|
}],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
@@ -177,7 +178,7 @@ fn test_list_owned_txouts() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 10000,
|
value: Amount::from_sat(10000),
|
||||||
script_pubkey: trusted_spks[1].to_owned(),
|
script_pubkey: trusted_spks[1].to_owned(),
|
||||||
}],
|
}],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
@@ -186,7 +187,7 @@ fn test_list_owned_txouts() {
|
|||||||
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
|
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
|
||||||
let tx4 = Transaction {
|
let tx4 = Transaction {
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 20000,
|
value: Amount::from_sat(20000),
|
||||||
script_pubkey: untrusted_spks[1].to_owned(),
|
script_pubkey: untrusted_spks[1].to_owned(),
|
||||||
}],
|
}],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
@@ -195,7 +196,7 @@ fn test_list_owned_txouts() {
|
|||||||
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
||||||
let tx5 = Transaction {
|
let tx5 = Transaction {
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 15000,
|
value: Amount::from_sat(15000),
|
||||||
script_pubkey: trusted_spks[2].to_owned(),
|
script_pubkey: trusted_spks[2].to_owned(),
|
||||||
}],
|
}],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
@@ -213,10 +214,8 @@ fn test_list_owned_txouts() {
|
|||||||
(
|
(
|
||||||
*tx,
|
*tx,
|
||||||
local_chain
|
local_chain
|
||||||
.blocks()
|
.get(height)
|
||||||
.get(&height)
|
.map(|cp| cp.block_id())
|
||||||
.cloned()
|
|
||||||
.map(|hash| BlockId { height, hash })
|
|
||||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
.map(|anchor_block| ConfirmationHeightAnchor {
|
||||||
anchor_block,
|
anchor_block,
|
||||||
confirmation_height: anchor_block.height,
|
confirmation_height: anchor_block.height,
|
||||||
@@ -231,9 +230,8 @@ fn test_list_owned_txouts() {
|
|||||||
|height: u32,
|
|height: u32,
|
||||||
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
||||||
let chain_tip = local_chain
|
let chain_tip = local_chain
|
||||||
.blocks()
|
.get(height)
|
||||||
.get(&height)
|
.map(|cp| cp.block_id())
|
||||||
.map(|&hash| BlockId { height, hash })
|
|
||||||
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
||||||
let txouts = graph
|
let txouts = graph
|
||||||
.graph()
|
.graph()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use bdk_chain::{
|
|||||||
Append,
|
Append,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, ScriptBuf, Transaction, TxOut};
|
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||||
use miniscript::{Descriptor, DescriptorPublicKey};
|
use miniscript::{Descriptor, DescriptorPublicKey};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
||||||
@@ -176,14 +176,14 @@ fn test_lookahead() {
|
|||||||
.at_derivation_index(external_index)
|
.at_derivation_index(external_index)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.script_pubkey(),
|
.script_pubkey(),
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
script_pubkey: internal_desc
|
script_pubkey: internal_desc
|
||||||
.at_derivation_index(internal_index)
|
.at_derivation_index(internal_index)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.script_pubkey(),
|
.script_pubkey(),
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..common::new_tx(external_index)
|
..common::new_tx(external_index)
|
||||||
@@ -238,7 +238,7 @@ fn test_scan_with_lookahead() {
|
|||||||
let op = OutPoint::new(h!("fake tx"), spk_i);
|
let op = OutPoint::new(h!("fake tx"), spk_i);
|
||||||
let txout = TxOut {
|
let txout = TxOut {
|
||||||
script_pubkey: spk.clone(),
|
script_pubkey: spk.clone(),
|
||||||
value: 0,
|
value: Amount::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
let changeset = txout_index.index_txout(op, &txout);
|
let changeset = txout_index.index_txout(op, &txout);
|
||||||
@@ -264,7 +264,7 @@ fn test_scan_with_lookahead() {
|
|||||||
let op = OutPoint::new(h!("fake tx"), 41);
|
let op = OutPoint::new(h!("fake tx"), 41);
|
||||||
let txout = TxOut {
|
let txout = TxOut {
|
||||||
script_pubkey: spk_41,
|
script_pubkey: spk_41,
|
||||||
value: 0,
|
value: Amount::ZERO,
|
||||||
};
|
};
|
||||||
let changeset = txout_index.index_txout(op, &txout);
|
let changeset = txout_index.index_txout(op, &txout);
|
||||||
assert!(changeset.is_empty());
|
assert!(changeset.is_empty());
|
||||||
@@ -386,3 +386,103 @@ fn test_non_wildcard_derivations() {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that calling `lookahead_to_target` stores the expected spks.
|
||||||
|
#[test]
|
||||||
|
fn lookahead_to_target() {
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestCase {
|
||||||
|
/// Global lookahead value.
|
||||||
|
lookahead: u32,
|
||||||
|
/// Last revealed index for external keychain.
|
||||||
|
external_last_revealed: Option<u32>,
|
||||||
|
/// Last revealed index for internal keychain.
|
||||||
|
internal_last_revealed: Option<u32>,
|
||||||
|
/// Call `lookahead_to_target(External, u32)`.
|
||||||
|
external_target: Option<u32>,
|
||||||
|
/// Call `lookahead_to_target(Internal, u32)`.
|
||||||
|
internal_target: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_cases = &[
|
||||||
|
TestCase {
|
||||||
|
lookahead: 0,
|
||||||
|
external_target: Some(100),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
lookahead: 10,
|
||||||
|
internal_target: Some(99),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
lookahead: 100,
|
||||||
|
internal_target: Some(9),
|
||||||
|
external_target: Some(10),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
lookahead: 12,
|
||||||
|
external_last_revealed: Some(2),
|
||||||
|
internal_last_revealed: Some(2),
|
||||||
|
internal_target: Some(15),
|
||||||
|
external_target: Some(13),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
lookahead: 13,
|
||||||
|
external_last_revealed: Some(100),
|
||||||
|
internal_last_revealed: Some(21),
|
||||||
|
internal_target: Some(120),
|
||||||
|
external_target: Some(130),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for t in test_cases {
|
||||||
|
let (mut index, _, _) = init_txout_index(t.lookahead);
|
||||||
|
|
||||||
|
if let Some(last_revealed) = t.external_last_revealed {
|
||||||
|
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
|
||||||
|
}
|
||||||
|
if let Some(last_revealed) = t.internal_last_revealed {
|
||||||
|
let _ = index.reveal_to_target(&TestKeychain::Internal, last_revealed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let keychain_test_cases = [
|
||||||
|
(
|
||||||
|
TestKeychain::External,
|
||||||
|
t.external_last_revealed,
|
||||||
|
t.external_target,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TestKeychain::Internal,
|
||||||
|
t.internal_last_revealed,
|
||||||
|
t.internal_target,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (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),
|
||||||
|
None => t.lookahead.checked_sub(1),
|
||||||
|
};
|
||||||
|
let exp_last_stored_index = match original_last_stored_index {
|
||||||
|
Some(original_last_stored_index) => {
|
||||||
|
Ord::max(target, original_last_stored_index)
|
||||||
|
}
|
||||||
|
None => target,
|
||||||
|
};
|
||||||
|
index.lookahead_to_target(&keychain, target);
|
||||||
|
let keys = index
|
||||||
|
.inner()
|
||||||
|
.all_spks()
|
||||||
|
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
|
||||||
|
.map(|(k, _)| k.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let exp_keys = core::iter::repeat(keychain)
|
||||||
|
.zip(0_u32..=exp_last_stored_index)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(keys, exp_keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::ops::{Bound, RangeBounds};
|
||||||
|
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
local_chain::{
|
local_chain::{
|
||||||
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
||||||
@@ -6,6 +8,7 @@ use bdk_chain::{
|
|||||||
BlockId,
|
BlockId,
|
||||||
};
|
};
|
||||||
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod common;
|
mod common;
|
||||||
@@ -528,6 +531,52 @@ fn checkpoint_from_block_ids() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checkpoint_query() {
|
||||||
|
struct TestCase {
|
||||||
|
chain: LocalChain,
|
||||||
|
/// The heights we want to call [`CheckPoint::query`] with, represented as an inclusive
|
||||||
|
/// range.
|
||||||
|
///
|
||||||
|
/// If a [`CheckPoint`] exists at that height, we expect [`CheckPoint::query`] to return
|
||||||
|
/// it. If not, [`CheckPoint::query`] should return `None`.
|
||||||
|
query_range: (u32, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_cases = [
|
||||||
|
TestCase {
|
||||||
|
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
||||||
|
query_range: (0, 2),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||||
|
query_range: (0, 3),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for t in test_cases.into_iter() {
|
||||||
|
let tip = t.chain.tip();
|
||||||
|
for h in t.query_range.0..=t.query_range.1 {
|
||||||
|
let query_result = tip.get(h);
|
||||||
|
|
||||||
|
// perform an exhausitive search for the checkpoint at height `h`
|
||||||
|
let exp_hash = t
|
||||||
|
.chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.find(|cp| cp.height() == h)
|
||||||
|
.map(|cp| cp.hash());
|
||||||
|
|
||||||
|
match query_result {
|
||||||
|
Some(cp) => {
|
||||||
|
assert_eq!(Some(cp.hash()), exp_hash);
|
||||||
|
assert_eq!(cp.height(), h);
|
||||||
|
}
|
||||||
|
None => assert!(exp_hash.is_none()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_chain_apply_header_connected_to() {
|
fn local_chain_apply_header_connected_to() {
|
||||||
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
||||||
@@ -679,3 +728,48 @@ fn local_chain_apply_header_connected_to() {
|
|||||||
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
|
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_height_range_bounds(
|
||||||
|
height_upper_bound: u32,
|
||||||
|
) -> impl Strategy<Value = (Bound<u32>, Bound<u32>)> {
|
||||||
|
fn generate_height_bound(height_upper_bound: u32) -> impl Strategy<Value = Bound<u32>> {
|
||||||
|
prop_oneof![
|
||||||
|
(0..height_upper_bound).prop_map(Bound::Included),
|
||||||
|
(0..height_upper_bound).prop_map(Bound::Excluded),
|
||||||
|
Just(Bound::Unbounded),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
(
|
||||||
|
generate_height_bound(height_upper_bound),
|
||||||
|
generate_height_bound(height_upper_bound),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_checkpoints(max_height: u32, max_count: usize) -> impl Strategy<Value = CheckPoint> {
|
||||||
|
proptest::collection::btree_set(1..max_height, 0..max_count).prop_map(|mut heights| {
|
||||||
|
heights.insert(0); // must have genesis
|
||||||
|
CheckPoint::from_block_ids(heights.into_iter().map(|height| {
|
||||||
|
let hash = bitcoin::hashes::Hash::hash(height.to_le_bytes().as_slice());
|
||||||
|
BlockId { height, hash }
|
||||||
|
}))
|
||||||
|
.expect("blocks must be in order as it comes from btreeset")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig {
|
||||||
|
..Default::default()
|
||||||
|
})]
|
||||||
|
|
||||||
|
/// Ensure that [`CheckPoint::range`] returns the expected checkpoint heights by comparing it
|
||||||
|
/// against a more primitive approach.
|
||||||
|
#[test]
|
||||||
|
fn checkpoint_range(
|
||||||
|
range in generate_height_range_bounds(21_000),
|
||||||
|
cp in generate_checkpoints(21_000, 2100)
|
||||||
|
) {
|
||||||
|
let exp_heights = cp.iter().map(|cp| cp.height()).filter(|h| range.contains(h)).collect::<Vec<u32>>();
|
||||||
|
let heights = cp.range(range).map(|cp| cp.height()).collect::<Vec<u32>>();
|
||||||
|
prop_assert_eq!(heights, exp_heights);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
|
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
|
||||||
use bitcoin::{absolute, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
|
use bitcoin::{absolute, transaction, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spk_txout_sent_and_received() {
|
fn spk_txout_sent_and_received() {
|
||||||
@@ -11,11 +11,11 @@ fn spk_txout_sent_and_received() {
|
|||||||
index.insert_spk(1, spk2.clone());
|
index.insert_spk(1, spk2.clone());
|
||||||
|
|
||||||
let tx1 = Transaction {
|
let tx1 = Transaction {
|
||||||
version: 0x02,
|
version: transaction::Version::TWO,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 42_000,
|
value: Amount::from_sat(42_000),
|
||||||
script_pubkey: spk1.clone(),
|
script_pubkey: spk1.clone(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
@@ -30,7 +30,7 @@ fn spk_txout_sent_and_received() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let tx2 = Transaction {
|
let tx2 = Transaction {
|
||||||
version: 0x1,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint {
|
previous_output: OutPoint {
|
||||||
@@ -41,12 +41,12 @@ fn spk_txout_sent_and_received() {
|
|||||||
}],
|
}],
|
||||||
output: vec![
|
output: vec![
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 20_000,
|
value: Amount::from_sat(20_000),
|
||||||
script_pubkey: spk2,
|
script_pubkey: spk2,
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
script_pubkey: spk1,
|
script_pubkey: spk1,
|
||||||
value: 30_000,
|
value: Amount::from_sat(30_000),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -73,11 +73,11 @@ fn mark_used() {
|
|||||||
assert!(spk_index.is_used(&1));
|
assert!(spk_index.is_used(&1));
|
||||||
|
|
||||||
let tx1 = Transaction {
|
let tx1 = Transaction {
|
||||||
version: 0x02,
|
version: transaction::Version::TWO,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 42_000,
|
value: Amount::from_sat(42_000),
|
||||||
script_pubkey: spk1,
|
script_pubkey: spk1,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ use bdk_chain::{
|
|||||||
Anchor, Append, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor,
|
Anchor, Append, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor,
|
||||||
};
|
};
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
absolute, hashes::Hash, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid,
|
absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn,
|
||||||
|
TxOut, Txid,
|
||||||
};
|
};
|
||||||
|
use common::*;
|
||||||
use core::iter;
|
use core::iter;
|
||||||
|
use rand::RngCore;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -20,14 +24,14 @@ fn insert_txouts() {
|
|||||||
(
|
(
|
||||||
OutPoint::new(h!("tx1"), 1),
|
OutPoint::new(h!("tx1"), 1),
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
OutPoint::new(h!("tx1"), 2),
|
OutPoint::new(h!("tx1"), 2),
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 20_000,
|
value: Amount::from_sat(20_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -37,21 +41,21 @@ fn insert_txouts() {
|
|||||||
let update_ops = [(
|
let update_ops = [(
|
||||||
OutPoint::new(h!("tx2"), 0),
|
OutPoint::new(h!("tx2"), 0),
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 20_000,
|
value: Amount::from_sat(20_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
)];
|
)];
|
||||||
|
|
||||||
// One full transaction to be included in the update
|
// One full transaction to be included in the update
|
||||||
let update_txs = Transaction {
|
let update_txs = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint::null(),
|
previous_output: OutPoint::null(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 30_000,
|
value: Amount::from_sat(30_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
@@ -117,7 +121,7 @@ fn insert_txouts() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
graph.insert_tx(update_txs.clone()),
|
graph.insert_tx(update_txs.clone()),
|
||||||
ChangeSet {
|
ChangeSet {
|
||||||
txs: [update_txs.clone()].into(),
|
txs: [Arc::new(update_txs.clone())].into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -141,7 +145,7 @@ fn insert_txouts() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
changeset,
|
changeset,
|
||||||
ChangeSet {
|
ChangeSet {
|
||||||
txs: [update_txs.clone()].into(),
|
txs: [Arc::new(update_txs.clone())].into(),
|
||||||
txouts: update_ops.clone().into(),
|
txouts: update_ops.clone().into(),
|
||||||
anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(),
|
anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(),
|
||||||
last_seen: [(h!("tx2"), 1000000)].into()
|
last_seen: [(h!("tx2"), 1000000)].into()
|
||||||
@@ -161,14 +165,14 @@ fn insert_txouts() {
|
|||||||
(
|
(
|
||||||
1u32,
|
1u32,
|
||||||
&TxOut {
|
&TxOut {
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
2u32,
|
2u32,
|
||||||
&TxOut {
|
&TxOut {
|
||||||
value: 20_000,
|
value: Amount::from_sat(20_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -181,7 +185,7 @@ fn insert_txouts() {
|
|||||||
[(
|
[(
|
||||||
0u32,
|
0u32,
|
||||||
&TxOut {
|
&TxOut {
|
||||||
value: 30_000,
|
value: Amount::from_sat(30_000),
|
||||||
script_pubkey: ScriptBuf::new()
|
script_pubkey: ScriptBuf::new()
|
||||||
}
|
}
|
||||||
)]
|
)]
|
||||||
@@ -192,7 +196,7 @@ fn insert_txouts() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
graph.initial_changeset(),
|
graph.initial_changeset(),
|
||||||
ChangeSet {
|
ChangeSet {
|
||||||
txs: [update_txs.clone()].into(),
|
txs: [Arc::new(update_txs.clone())].into(),
|
||||||
txouts: update_ops.into_iter().chain(original_ops).collect(),
|
txouts: update_ops.into_iter().chain(original_ops).collect(),
|
||||||
anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(),
|
anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(),
|
||||||
last_seen: [(h!("tx2"), 1000000)].into()
|
last_seen: [(h!("tx2"), 1000000)].into()
|
||||||
@@ -203,7 +207,7 @@ fn insert_txouts() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
|
fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint::null(),
|
previous_output: OutPoint::null(),
|
||||||
@@ -222,10 +226,10 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn insert_tx_graph_keeps_track_of_spend() {
|
fn insert_tx_graph_keeps_track_of_spend() {
|
||||||
let tx1 = Transaction {
|
let tx1 = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
};
|
};
|
||||||
|
|
||||||
let op = OutPoint {
|
let op = OutPoint {
|
||||||
@@ -234,7 +238,7 @@ fn insert_tx_graph_keeps_track_of_spend() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let tx2 = Transaction {
|
let tx2 = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: op,
|
previous_output: op,
|
||||||
@@ -263,30 +267,33 @@ fn insert_tx_graph_keeps_track_of_spend() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn insert_tx_can_retrieve_full_tx_from_graph() {
|
fn insert_tx_can_retrieve_full_tx_from_graph() {
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint::null(),
|
previous_output: OutPoint::null(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut graph = TxGraph::<()>::default();
|
let mut graph = TxGraph::<()>::default();
|
||||||
let _ = graph.insert_tx(tx.clone());
|
let _ = graph.insert_tx(tx.clone());
|
||||||
assert_eq!(graph.get_tx(tx.txid()), Some(&tx));
|
assert_eq!(
|
||||||
|
graph.get_tx(tx.txid()).map(|tx| tx.as_ref().clone()),
|
||||||
|
Some(tx)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_tx_displaces_txouts() {
|
fn insert_tx_displaces_txouts() {
|
||||||
let mut tx_graph = TxGraph::<()>::default();
|
let mut tx_graph = TxGraph::<()>::default();
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 42_000,
|
value: Amount::from_sat(42_000),
|
||||||
script_pubkey: ScriptBuf::default(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -296,7 +303,7 @@ fn insert_tx_displaces_txouts() {
|
|||||||
vout: 0,
|
vout: 0,
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 1_337_000,
|
value: Amount::from_sat(1_337_000),
|
||||||
script_pubkey: ScriptBuf::default(),
|
script_pubkey: ScriptBuf::default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -309,8 +316,8 @@ fn insert_tx_displaces_txouts() {
|
|||||||
vout: 0,
|
vout: 0,
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 1_000_000_000,
|
value: Amount::from_sat(1_000_000_000),
|
||||||
script_pubkey: ScriptBuf::default(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -324,7 +331,7 @@ fn insert_tx_displaces_txouts() {
|
|||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.value,
|
.value,
|
||||||
42_000
|
Amount::from_sat(42_000)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tx_graph.get_txout(OutPoint {
|
tx_graph.get_txout(OutPoint {
|
||||||
@@ -339,12 +346,12 @@ fn insert_tx_displaces_txouts() {
|
|||||||
fn insert_txout_does_not_displace_tx() {
|
fn insert_txout_does_not_displace_tx() {
|
||||||
let mut tx_graph = TxGraph::<()>::default();
|
let mut tx_graph = TxGraph::<()>::default();
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 42_000,
|
value: Amount::from_sat(42_000),
|
||||||
script_pubkey: ScriptBuf::default(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -356,8 +363,8 @@ fn insert_txout_does_not_displace_tx() {
|
|||||||
vout: 0,
|
vout: 0,
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 1_337_000,
|
value: Amount::from_sat(1_337_000),
|
||||||
script_pubkey: ScriptBuf::default(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -367,8 +374,8 @@ fn insert_txout_does_not_displace_tx() {
|
|||||||
vout: 0,
|
vout: 0,
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 1_000_000_000,
|
value: Amount::from_sat(1_000_000_000),
|
||||||
script_pubkey: ScriptBuf::default(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -380,7 +387,7 @@ fn insert_txout_does_not_displace_tx() {
|
|||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.value,
|
.value,
|
||||||
42_000
|
Amount::from_sat(42_000)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tx_graph.get_txout(OutPoint {
|
tx_graph.get_txout(OutPoint {
|
||||||
@@ -395,21 +402,21 @@ fn insert_txout_does_not_displace_tx() {
|
|||||||
fn test_calculate_fee() {
|
fn test_calculate_fee() {
|
||||||
let mut graph = TxGraph::<()>::default();
|
let mut graph = TxGraph::<()>::default();
|
||||||
let intx1 = Transaction {
|
let intx1 = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 100,
|
value: Amount::from_sat(100),
|
||||||
..Default::default()
|
script_pubkey: ScriptBuf::new(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
let intx2 = Transaction {
|
let intx2 = Transaction {
|
||||||
version: 0x02,
|
version: transaction::Version::TWO,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 200,
|
value: Amount::from_sat(200),
|
||||||
..Default::default()
|
script_pubkey: ScriptBuf::new(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -419,8 +426,8 @@ fn test_calculate_fee() {
|
|||||||
vout: 0,
|
vout: 0,
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 300,
|
value: Amount::from_sat(300),
|
||||||
..Default::default()
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -429,7 +436,7 @@ fn test_calculate_fee() {
|
|||||||
let _ = graph.insert_txout(intxout1.0, intxout1.1);
|
let _ = graph.insert_txout(intxout1.0, intxout1.1);
|
||||||
|
|
||||||
let mut tx = Transaction {
|
let mut tx = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![
|
input: vec![
|
||||||
TxIn {
|
TxIn {
|
||||||
@@ -452,8 +459,8 @@ fn test_calculate_fee() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: 500,
|
value: Amount::from_sat(500),
|
||||||
..Default::default()
|
script_pubkey: ScriptBuf::new(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -485,13 +492,13 @@ fn test_calculate_fee() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_calculate_fee_on_coinbase() {
|
fn test_calculate_fee_on_coinbase() {
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 0x01,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint::null(),
|
previous_output: OutPoint::null(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
};
|
};
|
||||||
|
|
||||||
let graph = TxGraph::<()>::default();
|
let graph = TxGraph::<()>::default();
|
||||||
@@ -527,7 +534,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(h!("op0"), 0),
|
previous_output: OutPoint::new(h!("op0"), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default(), TxOut::default()],
|
output: vec![TxOut::NULL, TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -537,7 +544,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(tx_a0.txid(), 0),
|
previous_output: OutPoint::new(tx_a0.txid(), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default(), TxOut::default()],
|
output: vec![TxOut::NULL, TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -547,7 +554,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(tx_a0.txid(), 1),
|
previous_output: OutPoint::new(tx_a0.txid(), 1),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -556,7 +563,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(h!("op1"), 0),
|
previous_output: OutPoint::new(h!("op1"), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -566,7 +573,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(tx_b0.txid(), 0),
|
previous_output: OutPoint::new(tx_b0.txid(), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -576,7 +583,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(tx_b0.txid(), 1),
|
previous_output: OutPoint::new(tx_b0.txid(), 1),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -592,7 +599,7 @@ fn test_walk_ancestors() {
|
|||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -601,7 +608,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(h!("op2"), 0),
|
previous_output: OutPoint::new(h!("op2"), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -611,7 +618,7 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(tx_c1.txid(), 0),
|
previous_output: OutPoint::new(tx_c1.txid(), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -627,7 +634,7 @@ fn test_walk_ancestors() {
|
|||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -637,11 +644,11 @@ fn test_walk_ancestors() {
|
|||||||
previous_output: OutPoint::new(tx_d1.txid(), 0),
|
previous_output: OutPoint::new(tx_d1.txid(), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut graph = TxGraph::<BlockId>::new(vec![
|
let mut graph = TxGraph::<BlockId>::new([
|
||||||
tx_a0.clone(),
|
tx_a0.clone(),
|
||||||
tx_b0.clone(),
|
tx_b0.clone(),
|
||||||
tx_b1.clone(),
|
tx_b1.clone(),
|
||||||
@@ -662,17 +669,17 @@ fn test_walk_ancestors() {
|
|||||||
|
|
||||||
let ancestors = [
|
let ancestors = [
|
||||||
graph
|
graph
|
||||||
.walk_ancestors(&tx_c0, |depth, tx| Some((depth, tx)))
|
.walk_ancestors(tx_c0.clone(), |depth, tx| Some((depth, tx)))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
graph
|
graph
|
||||||
.walk_ancestors(&tx_d0, |depth, tx| Some((depth, tx)))
|
.walk_ancestors(tx_d0.clone(), |depth, tx| Some((depth, tx)))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
graph
|
graph
|
||||||
.walk_ancestors(&tx_e0, |depth, tx| Some((depth, tx)))
|
.walk_ancestors(tx_e0.clone(), |depth, tx| Some((depth, tx)))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
// Only traverse unconfirmed ancestors of tx_e0 this time
|
// Only traverse unconfirmed ancestors of tx_e0 this time
|
||||||
graph
|
graph
|
||||||
.walk_ancestors(&tx_e0, |depth, tx| {
|
.walk_ancestors(tx_e0.clone(), |depth, tx| {
|
||||||
let tx_node = graph.get_tx_node(tx.txid())?;
|
let tx_node = graph.get_tx_node(tx.txid())?;
|
||||||
for block in tx_node.anchors {
|
for block in tx_node.anchors {
|
||||||
match local_chain.is_block_in_chain(block.anchor_block(), tip.block_id()) {
|
match local_chain.is_block_in_chain(block.anchor_block(), tip.block_id()) {
|
||||||
@@ -699,8 +706,14 @@ fn test_walk_ancestors() {
|
|||||||
vec![(1, &tx_d1), (2, &tx_c2), (2, &tx_c3), (3, &tx_b2)],
|
vec![(1, &tx_d1), (2, &tx_c2), (2, &tx_c3), (3, &tx_b2)],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (txids, expected_txids) in ancestors.iter().zip(expected_ancestors.iter()) {
|
for (txids, expected_txids) in ancestors.into_iter().zip(expected_ancestors) {
|
||||||
assert_eq!(txids, expected_txids);
|
assert_eq!(
|
||||||
|
txids,
|
||||||
|
expected_txids
|
||||||
|
.into_iter()
|
||||||
|
.map(|(i, tx)| (i, Arc::new(tx.clone())))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,7 +727,7 @@ fn test_conflicting_descendants() {
|
|||||||
previous_output,
|
previous_output,
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -724,7 +737,7 @@ fn test_conflicting_descendants() {
|
|||||||
previous_output,
|
previous_output,
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default(), TxOut::default()],
|
output: vec![TxOut::NULL, TxOut::NULL],
|
||||||
..common::new_tx(1)
|
..common::new_tx(1)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -734,7 +747,7 @@ fn test_conflicting_descendants() {
|
|||||||
previous_output: OutPoint::new(tx_a.txid(), 0),
|
previous_output: OutPoint::new(tx_a.txid(), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(2)
|
..common::new_tx(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -756,7 +769,7 @@ fn test_conflicting_descendants() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_descendants_no_repeat() {
|
fn test_descendants_no_repeat() {
|
||||||
let tx_a = Transaction {
|
let tx_a = Transaction {
|
||||||
output: vec![TxOut::default(), TxOut::default(), TxOut::default()],
|
output: vec![TxOut::NULL, TxOut::NULL, TxOut::NULL],
|
||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -766,7 +779,7 @@ fn test_descendants_no_repeat() {
|
|||||||
previous_output: OutPoint::new(tx_a.txid(), vout),
|
previous_output: OutPoint::new(tx_a.txid(), vout),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(1)
|
..common::new_tx(1)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -777,7 +790,7 @@ fn test_descendants_no_repeat() {
|
|||||||
previous_output: OutPoint::new(txs_b[vout as usize].txid(), vout),
|
previous_output: OutPoint::new(txs_b[vout as usize].txid(), vout),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(2)
|
..common::new_tx(2)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -793,7 +806,7 @@ fn test_descendants_no_repeat() {
|
|||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(3)
|
..common::new_tx(3)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -802,7 +815,7 @@ fn test_descendants_no_repeat() {
|
|||||||
previous_output: OutPoint::new(tx_d.txid(), 0),
|
previous_output: OutPoint::new(tx_d.txid(), 0),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(4)
|
..common::new_tx(4)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -812,7 +825,7 @@ fn test_descendants_no_repeat() {
|
|||||||
previous_output: OutPoint::new(h!("tx_does_not_exist"), v),
|
previous_output: OutPoint::new(h!("tx_does_not_exist"), v),
|
||||||
..TxIn::default()
|
..TxIn::default()
|
||||||
}],
|
}],
|
||||||
output: vec![TxOut::default()],
|
output: vec![TxOut::NULL],
|
||||||
..common::new_tx(v)
|
..common::new_tx(v)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -859,11 +872,11 @@ fn test_chain_spends() {
|
|||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![
|
output: vec![
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 20_000,
|
value: Amount::from_sat(20_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -878,11 +891,11 @@ fn test_chain_spends() {
|
|||||||
}],
|
}],
|
||||||
output: vec![
|
output: vec![
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 5_000,
|
value: Amount::from_sat(5_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 5_000,
|
value: Amount::from_sat(5_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -897,11 +910,11 @@ fn test_chain_spends() {
|
|||||||
}],
|
}],
|
||||||
output: vec![
|
output: vec![
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
value: 10_000,
|
value: Amount::from_sat(10_000),
|
||||||
script_pubkey: ScriptBuf::new(),
|
script_pubkey: ScriptBuf::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1046,6 +1059,34 @@ fn test_changeset_last_seen_append() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_last_seen_unconfirmed() {
|
||||||
|
let mut graph = TxGraph::<()>::default();
|
||||||
|
let tx = new_tx(0);
|
||||||
|
let txid = tx.txid();
|
||||||
|
|
||||||
|
// insert a new tx
|
||||||
|
// initially we have a last_seen of 0, and no anchors
|
||||||
|
let _ = graph.insert_tx(tx);
|
||||||
|
let tx = graph.full_txs().next().unwrap();
|
||||||
|
assert_eq!(tx.last_seen_unconfirmed, 0);
|
||||||
|
assert!(tx.anchors.is_empty());
|
||||||
|
|
||||||
|
// higher timestamp should update last seen
|
||||||
|
let changeset = graph.update_last_seen_unconfirmed(2);
|
||||||
|
assert_eq!(changeset.last_seen.get(&txid).unwrap(), &2);
|
||||||
|
|
||||||
|
// lower timestamp has no effect
|
||||||
|
let changeset = graph.update_last_seen_unconfirmed(1);
|
||||||
|
assert!(changeset.last_seen.is_empty());
|
||||||
|
|
||||||
|
// once anchored, last seen is not updated
|
||||||
|
let _ = graph.insert_anchor(txid, ());
|
||||||
|
let changeset = graph.update_last_seen_unconfirmed(4);
|
||||||
|
assert!(changeset.is_empty());
|
||||||
|
assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_missing_blocks() {
|
fn test_missing_blocks() {
|
||||||
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
|
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
|
||||||
@@ -1178,3 +1219,86 @@ fn test_missing_blocks() {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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.
|
||||||
|
fn call_map_anchors_with_non_deterministic_anchor() {
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||||
|
/// A non-deterministic anchor
|
||||||
|
pub struct NonDeterministicAnchor {
|
||||||
|
pub anchor_block: BlockId,
|
||||||
|
pub non_deterministic_field: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = [
|
||||||
|
TxTemplate {
|
||||||
|
tx_name: "tx1",
|
||||||
|
inputs: &[TxInTemplate::Bogus],
|
||||||
|
outputs: &[TxOutTemplate::new(10000, Some(1))],
|
||||||
|
anchors: &[block_id!(1, "A")],
|
||||||
|
last_seen: None,
|
||||||
|
},
|
||||||
|
TxTemplate {
|
||||||
|
tx_name: "tx2",
|
||||||
|
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||||
|
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||||
|
anchors: &[block_id!(2, "B")],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
TxTemplate {
|
||||||
|
tx_name: "tx3",
|
||||||
|
inputs: &[TxInTemplate::PrevTx("tx2", 0)],
|
||||||
|
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||||
|
anchors: &[block_id!(3, "C"), block_id!(4, "D")],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let (graph, _, _) = init_graph(&template);
|
||||||
|
let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor {
|
||||||
|
anchor_block: a,
|
||||||
|
// A non-deterministic value
|
||||||
|
non_deterministic_field: rand::thread_rng().next_u32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check all the details in new_graph reconstruct as well
|
||||||
|
|
||||||
|
let mut full_txs_vec: Vec<_> = graph.full_txs().collect();
|
||||||
|
full_txs_vec.sort();
|
||||||
|
let mut new_txs_vec: Vec<_> = new_graph.full_txs().collect();
|
||||||
|
new_txs_vec.sort();
|
||||||
|
let mut new_txs = new_txs_vec.iter();
|
||||||
|
|
||||||
|
for tx_node in full_txs_vec.iter() {
|
||||||
|
let new_txnode = new_txs.next().unwrap();
|
||||||
|
assert_eq!(new_txnode.txid, tx_node.txid);
|
||||||
|
assert_eq!(new_txnode.tx, tx_node.tx);
|
||||||
|
assert_eq!(
|
||||||
|
new_txnode.last_seen_unconfirmed,
|
||||||
|
tx_node.last_seen_unconfirmed
|
||||||
|
);
|
||||||
|
assert_eq!(new_txnode.anchors.len(), tx_node.anchors.len());
|
||||||
|
|
||||||
|
let mut new_anchors: Vec<_> = new_txnode.anchors.iter().map(|a| a.anchor_block).collect();
|
||||||
|
new_anchors.sort();
|
||||||
|
let mut old_anchors: Vec<_> = tx_node.anchors.iter().copied().collect();
|
||||||
|
old_anchors.sort();
|
||||||
|
assert_eq!(new_anchors, old_anchors);
|
||||||
|
}
|
||||||
|
assert!(new_txs.next().is_none());
|
||||||
|
|
||||||
|
let new_graph_anchors: Vec<_> = new_graph
|
||||||
|
.all_anchors()
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.0.anchor_block)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
new_graph_anchors,
|
||||||
|
vec![
|
||||||
|
block_id!(1, "A"),
|
||||||
|
block_id!(2, "B"),
|
||||||
|
block_id!(3, "C"),
|
||||||
|
block_id!(4, "D"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_electrum"
|
name = "bdk_electrum"
|
||||||
version = "0.8.0"
|
version = "0.11.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -12,6 +12,11 @@ readme = "README.md"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk_chain = { path = "../chain", version = "0.10.0", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.12.0", default-features = false }
|
||||||
electrum-client = { version = "0.18" }
|
electrum-client = { version = "0.19" }
|
||||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
#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"
|
||||||
@@ -40,15 +40,11 @@ impl RelevantTxids {
|
|||||||
pub fn into_tx_graph(
|
pub fn into_tx_graph(
|
||||||
self,
|
self,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
seen_at: Option<u64>,
|
|
||||||
missing: Vec<Txid>,
|
missing: Vec<Txid>,
|
||||||
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
|
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
|
||||||
let new_txs = client.batch_transaction_get(&missing)?;
|
let new_txs = client.batch_transaction_get(&missing)?;
|
||||||
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
|
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
|
||||||
for (txid, anchors) in self.0 {
|
for (txid, anchors) in self.0 {
|
||||||
if let Some(seen_at) = seen_at {
|
|
||||||
let _ = graph.insert_seen_at(txid, seen_at);
|
|
||||||
}
|
|
||||||
for anchor in anchors {
|
for anchor in anchors {
|
||||||
let _ = graph.insert_anchor(txid, anchor);
|
let _ = graph.insert_anchor(txid, anchor);
|
||||||
}
|
}
|
||||||
@@ -67,10 +63,9 @@ impl RelevantTxids {
|
|||||||
pub fn into_confirmation_time_tx_graph(
|
pub fn into_confirmation_time_tx_graph(
|
||||||
self,
|
self,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
seen_at: Option<u64>,
|
|
||||||
missing: Vec<Txid>,
|
missing: Vec<Txid>,
|
||||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||||
let graph = self.into_tx_graph(client, seen_at, missing)?;
|
let graph = self.into_tx_graph(client, missing)?;
|
||||||
|
|
||||||
let relevant_heights = {
|
let relevant_heights = {
|
||||||
let mut visited_heights = HashSet::new();
|
let mut visited_heights = HashSet::new();
|
||||||
@@ -189,7 +184,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
|
|||||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
||||||
let mut request_spks = keychain_spks
|
let mut request_spks = keychain_spks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, s)| (k.clone(), s.into_iter()))
|
.map(|(k, s)| (k, s.into_iter()))
|
||||||
.collect::<BTreeMap<K, _>>();
|
.collect::<BTreeMap<K, _>>();
|
||||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||||
|
|
||||||
|
|||||||
191
crates/electrum/tests/test_electrum.rs
Normal file
191
crates/electrum/tests/test_electrum.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use bdk_chain::{
|
||||||
|
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
|
||||||
|
keychain::Balance,
|
||||||
|
local_chain::LocalChain,
|
||||||
|
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
|
||||||
|
};
|
||||||
|
use bdk_electrum::{ElectrumExt, ElectrumUpdate};
|
||||||
|
use bdk_testenv::TestEnv;
|
||||||
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
|
|
||||||
|
fn get_balance(
|
||||||
|
recv_chain: &LocalChain,
|
||||||
|
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
|
||||||
|
) -> Result<Balance> {
|
||||||
|
let chain_tip = recv_chain.tip().block_id();
|
||||||
|
let outpoints = recv_graph.index.outpoints().clone();
|
||||||
|
let balance = recv_graph
|
||||||
|
.graph()
|
||||||
|
.balance(recv_chain, chain_tip, outpoints, |_, _| true);
|
||||||
|
Ok(balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that [`ElectrumExt`] can sync properly.
|
||||||
|
///
|
||||||
|
/// 1. Mine 101 blocks.
|
||||||
|
/// 2. Send a tx.
|
||||||
|
/// 3. Mine extra block to confirm sent tx.
|
||||||
|
/// 4. Check [`Balance`] to ensure tx is confirmed.
|
||||||
|
#[test]
|
||||||
|
fn scan_detects_confirmed_tx() -> Result<()> {
|
||||||
|
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||||
|
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||||
|
|
||||||
|
// Setup addresses.
|
||||||
|
let addr_to_mine = env
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
|
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
|
||||||
|
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
|
||||||
|
|
||||||
|
// Setup receiver.
|
||||||
|
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||||
|
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||||
|
let mut recv_index = SpkTxOutIndex::default();
|
||||||
|
recv_index.insert_spk((), spk_to_track.clone());
|
||||||
|
recv_index
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mine some blocks.
|
||||||
|
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||||
|
|
||||||
|
// Create transaction that is tracked by our receiver.
|
||||||
|
env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||||
|
|
||||||
|
// Mine a block to confirm sent tx.
|
||||||
|
env.mine_blocks(1, None)?;
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||||
|
let _ = recv_graph.apply_update(graph_update);
|
||||||
|
|
||||||
|
// Check to see if tx is confirmed.
|
||||||
|
assert_eq!(
|
||||||
|
get_balance(&recv_chain, &recv_graph)?,
|
||||||
|
Balance {
|
||||||
|
confirmed: SEND_AMOUNT.to_sat(),
|
||||||
|
..Balance::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that confirmed txs that are reorged become unconfirmed.
|
||||||
|
///
|
||||||
|
/// 1. Mine 101 blocks.
|
||||||
|
/// 2. Mine 8 blocks with a confirmed tx in each.
|
||||||
|
/// 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<()> {
|
||||||
|
const REORG_COUNT: usize = 8;
|
||||||
|
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||||
|
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||||
|
|
||||||
|
// Setup addresses.
|
||||||
|
let addr_to_mine = env
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked();
|
||||||
|
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
|
||||||
|
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
|
||||||
|
|
||||||
|
// Setup receiver.
|
||||||
|
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||||
|
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||||
|
let mut recv_index = SpkTxOutIndex::default();
|
||||||
|
recv_index.insert_spk((), spk_to_track.clone());
|
||||||
|
recv_index
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mine some blocks.
|
||||||
|
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||||
|
|
||||||
|
// Create transactions that are tracked by our receiver.
|
||||||
|
for _ in 0..REORG_COUNT {
|
||||||
|
env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||||
|
env.mine_blocks(1, None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||||
|
let _ = recv_graph.apply_update(graph_update.clone());
|
||||||
|
|
||||||
|
// Retain a snapshot of all anchors before reorg process.
|
||||||
|
let initial_anchors = 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,
|
||||||
|
..Balance::default()
|
||||||
|
},
|
||||||
|
"initial balance must be correct",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform reorgs with different depths.
|
||||||
|
for depth in 1..=REORG_COUNT {
|
||||||
|
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 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)
|
||||||
|
.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()) {
|
||||||
|
println!("New anchor added at reorg depth {}", depth);
|
||||||
|
}
|
||||||
|
let _ = recv_graph.apply_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,
|
||||||
|
..Balance::default()
|
||||||
|
},
|
||||||
|
"reorg_count: {}",
|
||||||
|
depth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_esplora"
|
name = "bdk_esplora"
|
||||||
version = "0.8.0"
|
version = "0.11.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -12,17 +12,18 @@ readme = "README.md"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk_chain = { path = "../chain", version = "0.10.0", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.12.0", default-features = false }
|
||||||
esplora-client = { version = "0.6.0", default-features = false }
|
esplora-client = { version = "0.7.0", default-features = false }
|
||||||
async-trait = { version = "0.1.66", optional = true }
|
async-trait = { version = "0.1.66", optional = true }
|
||||||
futures = { version = "0.3.26", optional = true }
|
futures = { version = "0.3.26", optional = true }
|
||||||
|
|
||||||
# use these dependencies if you need to enable their /no-std features
|
# use these dependencies if you need to enable their /no-std features
|
||||||
bitcoin = { version = "0.30.0", optional = true, default-features = false }
|
bitcoin = { version = "0.31.0", optional = true, default-features = false }
|
||||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
miniscript = { version = "11.0.0", optional = true, default-features = false }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
[dev-dependencies]
|
||||||
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
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"] }
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bdk_chain::collections::btree_map;
|
use bdk_chain::collections::btree_map;
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
local_chain::{self, CheckPoint},
|
local_chain::{self, CheckPoint},
|
||||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||||
@@ -52,6 +52,19 @@ pub trait EsploraAsyncExt {
|
|||||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
/// 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
|
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||||
/// parallel.
|
/// parallel.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
||||||
|
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
||||||
|
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
||||||
|
///
|
||||||
|
/// This follows the same approach as other Bitcoin-related software,
|
||||||
|
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
||||||
|
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
||||||
|
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
||||||
|
///
|
||||||
|
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
||||||
async fn full_scan<K: Ord + Clone + Send>(
|
async fn full_scan<K: Ord + Clone + Send>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<
|
keychain_spks: BTreeMap<
|
||||||
@@ -162,6 +175,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||||
|
let stop_gap = Ord::max(stop_gap, 1);
|
||||||
|
|
||||||
for (keychain, spks) in keychain_spks {
|
for (keychain, spks) in keychain_spks {
|
||||||
let mut spks = spks.into_iter();
|
let mut spks = spks.into_iter();
|
||||||
@@ -214,7 +228,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
script_pubkey: prevout.scriptpubkey.clone(),
|
script_pubkey: prevout.scriptpubkey.clone(),
|
||||||
value: prevout.value,
|
value: Amount::from_sat(prevout.value),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
@@ -226,12 +240,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||||
let past_gap_limit = if let Some(i) = last_active_index {
|
let gap_limit_reached = if let Some(i) = last_active_index {
|
||||||
last_index > i.saturating_add(stop_gap as u32)
|
last_index >= i.saturating_add(stop_gap as u32)
|
||||||
} else {
|
} else {
|
||||||
last_index >= stop_gap as u32
|
last_index + 1 >= stop_gap as u32
|
||||||
};
|
};
|
||||||
if past_gap_limit {
|
if gap_limit_reached {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::thread::JoinHandle;
|
|||||||
use bdk_chain::collections::btree_map;
|
use bdk_chain::collections::btree_map;
|
||||||
use bdk_chain::collections::BTreeMap;
|
use bdk_chain::collections::BTreeMap;
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||||
local_chain::{self, CheckPoint},
|
local_chain::{self, CheckPoint},
|
||||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||||
};
|
};
|
||||||
@@ -50,6 +50,19 @@ pub trait EsploraExt {
|
|||||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
/// 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
|
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||||
/// parallel.
|
/// parallel.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
||||||
|
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
||||||
|
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
||||||
|
///
|
||||||
|
/// This follows the same approach as other Bitcoin-related software,
|
||||||
|
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
||||||
|
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
||||||
|
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
||||||
|
///
|
||||||
|
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
||||||
fn full_scan<K: Ord + Clone>(
|
fn full_scan<K: Ord + Clone>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||||
@@ -149,6 +162,7 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||||
|
let stop_gap = Ord::max(stop_gap, 1);
|
||||||
|
|
||||||
for (keychain, spks) in keychain_spks {
|
for (keychain, spks) in keychain_spks {
|
||||||
let mut spks = spks.into_iter();
|
let mut spks = spks.into_iter();
|
||||||
@@ -204,7 +218,7 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
},
|
},
|
||||||
TxOut {
|
TxOut {
|
||||||
script_pubkey: prevout.scriptpubkey.clone(),
|
script_pubkey: prevout.scriptpubkey.clone(),
|
||||||
value: prevout.value,
|
value: Amount::from_sat(prevout.value),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
@@ -216,12 +230,12 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||||
let past_gap_limit = if let Some(i) = last_active_index {
|
let gap_limit_reached = if let Some(i) = last_active_index {
|
||||||
last_index > i.saturating_add(stop_gap as u32)
|
last_index >= i.saturating_add(stop_gap as u32)
|
||||||
} else {
|
} else {
|
||||||
last_index >= stop_gap as u32
|
last_index + 1 >= stop_gap as u32
|
||||||
};
|
};
|
||||||
if past_gap_limit {
|
if gap_limit_reached {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,21 @@
|
|||||||
use bdk_esplora::EsploraAsyncExt;
|
use bdk_esplora::EsploraAsyncExt;
|
||||||
|
use electrsd::bitcoind::anyhow;
|
||||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
use esplora_client::{self, Builder};
|
||||||
use electrsd::{Conf, ElectrsD};
|
|
||||||
use esplora_client::{self, AsyncClient, Builder};
|
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||||
|
use bdk_testenv::TestEnv;
|
||||||
struct TestEnv {
|
|
||||||
bitcoind: BitcoinD,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
electrsd: ElectrsD,
|
|
||||||
client: AsyncClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestEnv {
|
|
||||||
fn new() -> Result<Self, anyhow::Error> {
|
|
||||||
let bitcoind_exe =
|
|
||||||
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
|
||||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
|
||||||
|
|
||||||
let mut electrs_conf = Conf::default();
|
|
||||||
electrs_conf.http_enabled = true;
|
|
||||||
let electrs_exe =
|
|
||||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
|
||||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
|
||||||
|
|
||||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
|
||||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
bitcoind,
|
|
||||||
electrsd,
|
|
||||||
client,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mine_blocks(
|
|
||||||
&self,
|
|
||||||
count: usize,
|
|
||||||
address: Option<Address>,
|
|
||||||
) -> anyhow::Result<Vec<BlockHash>> {
|
|
||||||
let coinbase_address = match address {
|
|
||||||
Some(address) => address,
|
|
||||||
None => self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.get_new_address(None, None)?
|
|
||||||
.assume_checked(),
|
|
||||||
};
|
|
||||||
let block_hashes = self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.generate_to_address(count as _, &coinbase_address)?;
|
|
||||||
Ok(block_hashes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
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()?;
|
||||||
|
|
||||||
let receive_address0 =
|
let receive_address0 =
|
||||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||||
let receive_address1 =
|
let receive_address1 =
|
||||||
@@ -95,12 +48,11 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while env.client.get_height().await.unwrap() < 102 {
|
while client.get_height().await.unwrap() < 102 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph_update = env
|
let graph_update = client
|
||||||
.client
|
|
||||||
.sync(
|
.sync(
|
||||||
misc_spks.into_iter(),
|
misc_spks.into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
@@ -114,7 +66,7 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
for tx in graph_update.full_txs() {
|
for tx in graph_update.full_txs() {
|
||||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||||
// floating txouts available from the transactions' previous outputs.
|
// floating txouts available from the transactions' previous outputs.
|
||||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
|
||||||
|
|
||||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||||
let tx_fee = env
|
let tx_fee = env
|
||||||
@@ -139,10 +91,12 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test the bounds of the address scan depending on the gap limit.
|
/// Test the bounds of the address scan depending on the `stop_gap`.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
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()?;
|
||||||
let _block_hashes = env.mine_blocks(101, None)?;
|
let _block_hashes = env.mine_blocks(101, None)?;
|
||||||
|
|
||||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||||
@@ -182,16 +136,16 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while env.client.get_height().await.unwrap() < 103 {
|
while client.get_height().await.unwrap() < 103 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
// A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||||
// will.
|
// will.
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
|
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
|
||||||
assert!(graph_update.full_txs().next().is_none());
|
assert!(graph_update.full_txs().next().is_none());
|
||||||
assert!(active_indices.is_empty());
|
assert!(active_indices.is_empty());
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
|
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!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
|
|
||||||
@@ -207,18 +161,18 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while env.client.get_height().await.unwrap() < 104 {
|
while client.get_height().await.unwrap() < 104 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
// 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.
|
// The last active indice won't be updated in the first case but will in the second one.
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
|
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 txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 1);
|
assert_eq!(txs.len(), 1);
|
||||||
assert!(txs.contains(&txid_4th_addr));
|
assert!(txs.contains(&txid_4th_addr));
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
|
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1).await?;
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 2);
|
assert_eq!(txs.len(), 2);
|
||||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use bdk_chain::local_chain::LocalChain;
|
use bdk_chain::local_chain::LocalChain;
|
||||||
use bdk_chain::BlockId;
|
use bdk_chain::BlockId;
|
||||||
use bdk_esplora::EsploraExt;
|
use bdk_esplora::EsploraExt;
|
||||||
|
use electrsd::bitcoind::anyhow;
|
||||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
use esplora_client::{self, Builder};
|
||||||
use electrsd::{Conf, ElectrsD};
|
|
||||||
use esplora_client::{self, BlockingClient, Builder};
|
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||||
|
use bdk_testenv::TestEnv;
|
||||||
|
|
||||||
macro_rules! h {
|
macro_rules! h {
|
||||||
($index:literal) => {{
|
($index:literal) => {{
|
||||||
@@ -26,73 +26,12 @@ macro_rules! local_chain {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestEnv {
|
|
||||||
bitcoind: BitcoinD,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
electrsd: ElectrsD,
|
|
||||||
client: BlockingClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestEnv {
|
|
||||||
fn new() -> Result<Self, anyhow::Error> {
|
|
||||||
let bitcoind_exe =
|
|
||||||
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
|
||||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
|
||||||
|
|
||||||
let mut electrs_conf = Conf::default();
|
|
||||||
electrs_conf.http_enabled = true;
|
|
||||||
let electrs_exe =
|
|
||||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
|
||||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
|
||||||
|
|
||||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
|
||||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
bitcoind,
|
|
||||||
electrsd,
|
|
||||||
client,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
|
||||||
let mut electrs_conf = Conf::default();
|
|
||||||
electrs_conf.http_enabled = true;
|
|
||||||
let electrs_exe =
|
|
||||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
|
||||||
let electrsd = ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrs_conf)?;
|
|
||||||
|
|
||||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
|
||||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
|
||||||
self.electrsd = electrsd;
|
|
||||||
self.client = client;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mine_blocks(
|
|
||||||
&self,
|
|
||||||
count: usize,
|
|
||||||
address: Option<Address>,
|
|
||||||
) -> anyhow::Result<Vec<BlockHash>> {
|
|
||||||
let coinbase_address = match address {
|
|
||||||
Some(address) => address,
|
|
||||||
None => self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.get_new_address(None, None)?
|
|
||||||
.assume_checked(),
|
|
||||||
};
|
|
||||||
let block_hashes = self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.generate_to_address(count as _, &coinbase_address)?;
|
|
||||||
Ok(block_hashes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
|
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||||
|
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||||
|
|
||||||
let receive_address0 =
|
let receive_address0 =
|
||||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||||
let receive_address1 =
|
let receive_address1 =
|
||||||
@@ -125,11 +64,11 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while env.client.get_height().unwrap() < 102 {
|
while client.get_height().unwrap() < 102 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph_update = env.client.sync(
|
let graph_update = client.sync(
|
||||||
misc_spks.into_iter(),
|
misc_spks.into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
@@ -141,7 +80,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
for tx in graph_update.full_txs() {
|
for tx in graph_update.full_txs() {
|
||||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||||
// floating txouts available from the transactions' previous outputs.
|
// floating txouts available from the transactions' previous outputs.
|
||||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
|
||||||
|
|
||||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||||
let tx_fee = env
|
let tx_fee = env
|
||||||
@@ -167,10 +106,12 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test the bounds of the address scan depending on the gap limit.
|
/// Test the bounds of the address scan depending on the `stop_gap`.
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
|
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||||
|
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||||
let _block_hashes = env.mine_blocks(101, None)?;
|
let _block_hashes = env.mine_blocks(101, None)?;
|
||||||
|
|
||||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||||
@@ -210,16 +151,16 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while env.client.get_height().unwrap() < 103 {
|
while client.get_height().unwrap() < 103 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||||
// will.
|
// will.
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
|
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
|
||||||
assert!(graph_update.full_txs().next().is_none());
|
assert!(graph_update.full_txs().next().is_none());
|
||||||
assert!(active_indices.is_empty());
|
assert!(active_indices.is_empty());
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
|
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!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
|
|
||||||
@@ -235,18 +176,18 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while env.client.get_height().unwrap() < 104 {
|
while client.get_height().unwrap() < 104 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
// 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.
|
// The last active indice won't be updated in the first case but will in the second one.
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
|
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 txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 1);
|
assert_eq!(txs.len(), 1);
|
||||||
assert!(txs.contains(&txid_4th_addr));
|
assert!(txs.contains(&txid_4th_addr));
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
|
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1)?;
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 2);
|
assert_eq!(txs.len(), 2);
|
||||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||||
@@ -273,6 +214,8 @@ fn update_local_chain() -> anyhow::Result<()> {
|
|||||||
};
|
};
|
||||||
// so new blocks can be seen by Electrs
|
// so new blocks can be seen by Electrs
|
||||||
let env = env.reset_electrsd()?;
|
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 {
|
struct TestCase {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
@@ -375,8 +318,7 @@ fn update_local_chain() -> anyhow::Result<()> {
|
|||||||
println!("Case {}: {}", i, t.name);
|
println!("Case {}: {}", i, t.name);
|
||||||
let mut chain = t.chain;
|
let mut chain = t.chain;
|
||||||
|
|
||||||
let update = env
|
let update = client
|
||||||
.client
|
|
||||||
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
|
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
|
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
|
||||||
@@ -418,8 +360,8 @@ fn update_local_chain() -> anyhow::Result<()> {
|
|||||||
for height in t.request_heights {
|
for height in t.request_heights {
|
||||||
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
chain.blocks().get(height),
|
chain.get(*height).map(|cp| cp.hash()),
|
||||||
Some(exp_blockhash),
|
Some(*exp_blockhash),
|
||||||
"[{}:{}] block {}:{} must exist in final chain",
|
"[{}:{}] block {}:{} must exist in final chain",
|
||||||
i,
|
i,
|
||||||
t.name,
|
t.name,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_file_store"
|
name = "bdk_file_store"
|
||||||
version = "0.6.0"
|
version = "0.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -11,7 +11,7 @@ authors = ["Bitcoin Dev Kit Developers"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk_chain = { path = "../chain", version = "0.10.0", features = [ "serde", "miniscript" ] }
|
bdk_chain = { path = "../chain", version = "0.12.0", features = [ "serde", "miniscript" ] }
|
||||||
bincode = { version = "1" }
|
bincode = { version = "1" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ where
|
|||||||
.create(true)
|
.create(true)
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
.open(file_path)?;
|
.open(file_path)?;
|
||||||
f.write_all(magic)?;
|
f.write_all(magic)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_hwi"
|
name = "bdk_hwi"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -10,4 +10,4 @@ readme = "README.md"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk = { path = "../bdk" }
|
bdk = { path = "../bdk" }
|
||||||
hwi = { version = "0.7.0", features = [ "miniscript"] }
|
hwi = { version = "0.8.0", features = [ "miniscript"] }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
//! # use bdk::signer::SignerOrdering;
|
//! # use bdk::signer::SignerOrdering;
|
||||||
//! # use bdk_hwi::HWISigner;
|
//! # use bdk_hwi::HWISigner;
|
||||||
//! # use bdk::wallet::AddressIndex::New;
|
//! # use bdk::wallet::AddressIndex::New;
|
||||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||||
//! # use hwi::HWIClient;
|
//! # use hwi::HWIClient;
|
||||||
//! # use std::sync::Arc;
|
//! # use std::sync::Arc;
|
||||||
//! #
|
//! #
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bdk::bitcoin::bip32::Fingerprint;
|
use bdk::bitcoin::bip32::Fingerprint;
|
||||||
use bdk::bitcoin::psbt::PartiallySignedTransaction;
|
|
||||||
use bdk::bitcoin::secp256k1::{All, Secp256k1};
|
use bdk::bitcoin::secp256k1::{All, Secp256k1};
|
||||||
|
use bdk::bitcoin::Psbt;
|
||||||
|
|
||||||
use hwi::error::Error;
|
use hwi::error::Error;
|
||||||
use hwi::types::{HWIChain, HWIDevice};
|
use hwi::types::{HWIChain, HWIDevice};
|
||||||
@@ -37,7 +37,7 @@ impl SignerCommon for HWISigner {
|
|||||||
impl TransactionSigner for HWISigner {
|
impl TransactionSigner for HWISigner {
|
||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut PartiallySignedTransaction,
|
psbt: &mut Psbt,
|
||||||
_sign_options: &bdk::SignOptions,
|
_sign_options: &bdk::SignOptions,
|
||||||
_secp: &Secp256k1<All>,
|
_secp: &Secp256k1<All>,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
|
|||||||
24
crates/testenv/Cargo.toml
Normal file
24
crates/testenv/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "bdk_testenv"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.63"
|
||||||
|
homepage = "https://bitcoindevkit.org"
|
||||||
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
|
documentation = "https://docs.rs/bdk_testenv"
|
||||||
|
description = "Testing framework for BDK chain sources."
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
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 }
|
||||||
|
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||||
|
anyhow = { version = "1" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["std"]
|
||||||
|
std = ["bdk_chain/std"]
|
||||||
|
serde = ["bdk_chain/serde"]
|
||||||
6
crates/testenv/README.md
Normal file
6
crates/testenv/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# BDK TestEnv
|
||||||
|
|
||||||
|
This crate sets up a regtest environment with a single [`bitcoind`] node
|
||||||
|
connected to an [`electrs`] instance. This framework provides the infrastructure
|
||||||
|
for testing chain source crates, e.g., [`bdk_chain`], [`bdk_electrum`],
|
||||||
|
[`bdk_esplora`], etc.
|
||||||
278
crates/testenv/src/lib.rs
Normal file
278
crates/testenv/src/lib.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
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 bitcoincore_rpc::{
|
||||||
|
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||||
|
RpcApi,
|
||||||
|
};
|
||||||
|
use electrsd::electrum_client::ElectrumApi;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
|
||||||
|
/// instance connected to it.
|
||||||
|
pub struct TestEnv {
|
||||||
|
pub bitcoind: electrsd::bitcoind::BitcoinD,
|
||||||
|
pub electrsd: electrsd::ElectrsD,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestEnv {
|
||||||
|
/// Construct a new [`TestEnv`] instance with default configurations.
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let bitcoind = match std::env::var_os("BITCOIND_EXE") {
|
||||||
|
Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
|
||||||
|
None => {
|
||||||
|
let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
|
||||||
|
.expect(
|
||||||
|
"you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
|
||||||
|
);
|
||||||
|
electrsd::bitcoind::BitcoinD::with_conf(
|
||||||
|
bitcoind_exe,
|
||||||
|
&electrsd::bitcoind::Conf::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let mut electrsd_conf = electrsd::Conf::default();
|
||||||
|
electrsd_conf.http_enabled = true;
|
||||||
|
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
||||||
|
Some(env_electrs_exe) => {
|
||||||
|
electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let electrs_exe = electrsd::downloaded_exe_path()
|
||||||
|
.expect("electrs version feature must be enabled");
|
||||||
|
electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(Self { bitcoind, electrsd })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exposes the [`ElectrumApi`] calls from the Electrum client.
|
||||||
|
pub fn electrum_client(&self) -> &impl ElectrumApi {
|
||||||
|
&self.electrsd.client
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
|
||||||
|
pub fn rpc_client(&self) -> &impl RpcApi {
|
||||||
|
&self.bitcoind.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset `electrsd` so that new blocks can be seen.
|
||||||
|
pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
||||||
|
let mut electrsd_conf = electrsd::Conf::default();
|
||||||
|
electrsd_conf.http_enabled = true;
|
||||||
|
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
||||||
|
Some(env_electrs_exe) => {
|
||||||
|
electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let electrs_exe = electrsd::downloaded_exe_path()
|
||||||
|
.expect("electrs version feature must be enabled");
|
||||||
|
electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
self.electrsd = electrsd;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
|
||||||
|
/// `address`.
|
||||||
|
pub fn mine_blocks(
|
||||||
|
&self,
|
||||||
|
count: usize,
|
||||||
|
address: Option<Address>,
|
||||||
|
) -> anyhow::Result<Vec<BlockHash>> {
|
||||||
|
let coinbase_address = match address {
|
||||||
|
Some(address) => address,
|
||||||
|
None => self
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked(),
|
||||||
|
};
|
||||||
|
let block_hashes = self
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.generate_to_address(count as _, &coinbase_address)?;
|
||||||
|
Ok(block_hashes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
|
||||||
|
pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
||||||
|
let bt = self.bitcoind.client.get_block_template(
|
||||||
|
GetBlockTemplateModes::Template,
|
||||||
|
&[GetBlockTemplateRules::SegWit],
|
||||||
|
&[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let txdata = vec![Transaction {
|
||||||
|
version: transaction::Version::ONE,
|
||||||
|
lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
|
||||||
|
input: vec![TxIn {
|
||||||
|
previous_output: bdk_chain::bitcoin::OutPoint::default(),
|
||||||
|
script_sig: ScriptBuf::builder()
|
||||||
|
.push_int(bt.height as _)
|
||||||
|
// randomn number so that re-mining creates unique block
|
||||||
|
.push_int(random())
|
||||||
|
.into_script(),
|
||||||
|
sequence: bdk_chain::bitcoin::Sequence::default(),
|
||||||
|
witness: bdk_chain::bitcoin::Witness::new(),
|
||||||
|
}],
|
||||||
|
output: vec![TxOut {
|
||||||
|
value: Amount::ZERO,
|
||||||
|
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
|
||||||
|
let bits: [u8; 4] = bt
|
||||||
|
.bits
|
||||||
|
.clone()
|
||||||
|
.try_into()
|
||||||
|
.expect("rpc provided us with invalid bits");
|
||||||
|
|
||||||
|
let mut block = Block {
|
||||||
|
header: Header {
|
||||||
|
version: bdk_chain::bitcoin::block::Version::default(),
|
||||||
|
prev_blockhash: bt.previous_block_hash,
|
||||||
|
merkle_root: TxMerkleNode::all_zeros(),
|
||||||
|
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
|
||||||
|
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
|
||||||
|
nonce: 0,
|
||||||
|
},
|
||||||
|
txdata,
|
||||||
|
};
|
||||||
|
|
||||||
|
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
|
||||||
|
|
||||||
|
for nonce in 0..=u32::MAX {
|
||||||
|
block.header.nonce = nonce;
|
||||||
|
if block.header.target().is_met_by(block.block_hash()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bitcoind.client.submit_block(&block)?;
|
||||||
|
Ok((bt.height as usize, block.block_hash()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method waits for the Electrum notification indicating that a new block has been mined.
|
||||||
|
pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
|
||||||
|
self.electrsd.client.block_headers_subscribe()?;
|
||||||
|
let mut delay = Duration::from_millis(64);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
self.electrsd.trigger()?;
|
||||||
|
self.electrsd.client.ping()?;
|
||||||
|
if self.electrsd.client.block_headers_pop()?.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if delay.as_millis() < 512 {
|
||||||
|
delay = delay.mul_f32(2.0);
|
||||||
|
}
|
||||||
|
std::thread::sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invalidate a number of blocks of a given size `count`.
|
||||||
|
pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
||||||
|
let mut hash = self.bitcoind.client.get_best_block_hash()?;
|
||||||
|
for _ in 0..count {
|
||||||
|
let prev_hash = self
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.get_block_info(&hash)?
|
||||||
|
.previousblockhash;
|
||||||
|
self.bitcoind.client.invalidate_block(&hash)?;
|
||||||
|
match prev_hash {
|
||||||
|
Some(prev_hash) => hash = prev_hash,
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reorg a number of blocks of a given size `count`.
|
||||||
|
/// Refer to [`TestEnv::mine_empty_block`] for more information.
|
||||||
|
pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
||||||
|
let start_height = self.bitcoind.client.get_block_count()?;
|
||||||
|
self.invalidate_blocks(count)?;
|
||||||
|
|
||||||
|
let res = self.mine_blocks(count, None);
|
||||||
|
assert_eq!(
|
||||||
|
self.bitcoind.client.get_block_count()?,
|
||||||
|
start_height,
|
||||||
|
"reorg should not result in height change"
|
||||||
|
);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reorg with a number of empty blocks of a given size `count`.
|
||||||
|
pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
||||||
|
let start_height = self.bitcoind.client.get_block_count()?;
|
||||||
|
self.invalidate_blocks(count)?;
|
||||||
|
|
||||||
|
let res = (0..count)
|
||||||
|
.map(|_| self.mine_empty_block())
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
assert_eq!(
|
||||||
|
self.bitcoind.client.get_block_count()?,
|
||||||
|
start_height,
|
||||||
|
"reorg should not result in height change"
|
||||||
|
);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a tx of a given `amount` to a given `address`.
|
||||||
|
pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
||||||
|
let txid = self
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
||||||
|
Ok(txid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::TestEnv;
|
||||||
|
use anyhow::Result;
|
||||||
|
use bitcoincore_rpc::RpcApi;
|
||||||
|
|
||||||
|
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
|
||||||
|
#[test]
|
||||||
|
fn test_reorg_is_detected_in_electrsd() -> Result<()> {
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
|
||||||
|
// Mine some blocks.
|
||||||
|
env.mine_blocks(101, None)?;
|
||||||
|
env.wait_until_electrum_sees_block()?;
|
||||||
|
let height = env.bitcoind.client.get_block_count()?;
|
||||||
|
let blocks = (0..=height)
|
||||||
|
.map(|i| env.bitcoind.client.get_block_hash(i))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
// Perform reorg on six blocks.
|
||||||
|
env.reorg(6)?;
|
||||||
|
env.wait_until_electrum_sees_block()?;
|
||||||
|
let reorged_height = env.bitcoind.client.get_block_count()?;
|
||||||
|
let reorged_blocks = (0..=height)
|
||||||
|
.map(|i| env.bitcoind.client.get_block_hash(i))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
assert_eq!(height, reorged_height);
|
||||||
|
|
||||||
|
// Block hashes should not be equal on the six reorged blocks.
|
||||||
|
for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
|
||||||
|
match i <= height as usize - 6 {
|
||||||
|
true => assert_eq!(block, reorged_block),
|
||||||
|
false => assert_ne!(block, reorged_block),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ use anyhow::Context;
|
|||||||
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
|
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
|
||||||
use bdk_file_store::Store;
|
use bdk_file_store::Store;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
|
use std::{cmp::Reverse, collections::BTreeMap, path::PathBuf, sync::Mutex, time::Duration};
|
||||||
|
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{
|
bitcoin::{
|
||||||
absolute, address, psbt::Prevouts, secp256k1::Secp256k1, sighash::SighashCache, Address,
|
absolute, address,
|
||||||
Network, Sequence, Transaction, TxIn, TxOut,
|
secp256k1::Secp256k1,
|
||||||
|
sighash::{Prevouts, SighashCache},
|
||||||
|
transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut,
|
||||||
},
|
},
|
||||||
indexed_tx_graph::{self, IndexedTxGraph},
|
indexed_tx_graph::{self, IndexedTxGraph},
|
||||||
keychain::{self, KeychainTxOutIndex},
|
keychain::{self, KeychainTxOutIndex},
|
||||||
@@ -197,7 +199,7 @@ pub struct CreateTxChange {
|
|||||||
pub fn create_tx<A: Anchor, O: ChainOracle>(
|
pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||||
graph: &mut KeychainTxGraph<A>,
|
graph: &mut KeychainTxGraph<A>,
|
||||||
chain: &O,
|
chain: &O,
|
||||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||||
cs_algorithm: CoinSelectionAlgo,
|
cs_algorithm: CoinSelectionAlgo,
|
||||||
address: Address,
|
address: Address,
|
||||||
value: u64,
|
value: u64,
|
||||||
@@ -235,7 +237,7 @@ where
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(plan, utxo)| {
|
.map(|(plan, utxo)| {
|
||||||
WeightedValue::new(
|
WeightedValue::new(
|
||||||
utxo.txout.value,
|
utxo.txout.value.to_sat(),
|
||||||
plan.expected_weight() as _,
|
plan.expected_weight() as _,
|
||||||
plan.witness_version().is_some(),
|
plan.witness_version().is_some(),
|
||||||
)
|
)
|
||||||
@@ -243,7 +245,7 @@ where
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut outputs = vec![TxOut {
|
let mut outputs = vec![TxOut {
|
||||||
value,
|
value: Amount::from_sat(value),
|
||||||
script_pubkey: address.script_pubkey(),
|
script_pubkey: address.script_pubkey(),
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -273,7 +275,7 @@ where
|
|||||||
.expect("failed to obtain change plan");
|
.expect("failed to obtain change plan");
|
||||||
|
|
||||||
let mut change_output = TxOut {
|
let mut change_output = TxOut {
|
||||||
value: 0,
|
value: Amount::ZERO,
|
||||||
script_pubkey: change_script,
|
script_pubkey: change_script,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,13 +313,13 @@ where
|
|||||||
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
|
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
|
||||||
|
|
||||||
if let Some(drain_value) = selection_meta.drain_value {
|
if let Some(drain_value) = selection_meta.drain_value {
|
||||||
change_output.value = drain_value;
|
change_output.value = Amount::from_sat(drain_value);
|
||||||
// if the selection tells us to use change and the change value is sufficient, we add it as an output
|
// if the selection tells us to use change and the change value is sufficient, we add it as an output
|
||||||
outputs.push(change_output)
|
outputs.push(change_output)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut transaction = Transaction {
|
let mut transaction = Transaction {
|
||||||
version: 0x02,
|
version: transaction::Version::TWO,
|
||||||
// because the temporary planning module does not support timelocks, we can use the chain
|
// because the temporary planning module does not support timelocks, we can use the chain
|
||||||
// tip as the `lock_time` for anti-fee-sniping purposes
|
// tip as the `lock_time` for anti-fee-sniping purposes
|
||||||
lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
|
lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
|
||||||
@@ -440,7 +442,7 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainO
|
|||||||
graph: &Mutex<KeychainTxGraph<A>>,
|
graph: &Mutex<KeychainTxGraph<A>>,
|
||||||
db: &Mutex<Database<C>>,
|
db: &Mutex<Database<C>>,
|
||||||
chain: &Mutex<O>,
|
chain: &Mutex<O>,
|
||||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||||
network: Network,
|
network: Network,
|
||||||
broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
|
broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
|
||||||
cmd: Commands<CS, S>,
|
cmd: Commands<CS, S>,
|
||||||
|
|||||||
@@ -299,12 +299,12 @@ fn main() -> anyhow::Result<()> {
|
|||||||
relevant_txids.missing_full_txs(graph.graph())
|
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
|
let now = std::time::UNIX_EPOCH
|
||||||
.elapsed()
|
.elapsed()
|
||||||
.expect("must get time")
|
.expect("must get time")
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||||
let graph_update = relevant_txids.into_tx_graph(&client, Some(now), missing_txids)?;
|
|
||||||
|
|
||||||
let db_changeset = {
|
let db_changeset = {
|
||||||
let mut chain = chain.lock().unwrap();
|
let mut chain = chain.lock().unwrap();
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ impl EsploraArgs {
|
|||||||
_ => panic!("unsupported network"),
|
_ => panic!("unsupported network"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
|
let client = esplora_client::Builder::new(esplora_url).build_blocking();
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,10 +189,14 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
|
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
|
||||||
// represents the last active spk derivation indices of keychains
|
// represents the last active spk derivation indices of keychains
|
||||||
// (`keychain_indices_update`).
|
// (`keychain_indices_update`).
|
||||||
let (graph_update, last_active_indices) = client
|
let (mut graph_update, last_active_indices) = client
|
||||||
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
|
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
|
||||||
.context("scanning for transactions")?;
|
.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 mut graph = graph.lock().expect("mutex must not be poisoned");
|
let mut graph = graph.lock().expect("mutex must not be poisoned");
|
||||||
// Because we did a stop gap based scan we are likely to have some updates to our
|
// 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
|
// deriviation indices. Usually before a scan you are on a fresh wallet with no
|
||||||
@@ -307,9 +311,13 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph_update =
|
let mut graph_update =
|
||||||
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
|
client.sync(spks, txids, outpoints, 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);
|
||||||
|
|
||||||
graph.lock().unwrap().apply_update(graph_update)
|
graph.lock().unwrap().apply_update(graph_update)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
println!();
|
println!();
|
||||||
|
|
||||||
let missing = relevant_txids.missing_full_txs(wallet.as_ref());
|
let missing = relevant_txids.missing_full_txs(wallet.as_ref());
|
||||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
|
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 {
|
let wallet_update = Update {
|
||||||
last_active_indices: keychain_update,
|
last_active_indices: keychain_update,
|
||||||
@@ -99,7 +101,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||||
assert!(finalized);
|
assert!(finalized);
|
||||||
|
|
||||||
let tx = psbt.extract_tx();
|
let tx = psbt.extract_tx()?;
|
||||||
client.transaction_broadcast(&tx)?;
|
client.transaction_broadcast(&tx)?;
|
||||||
println!("Tx broadcasted! Txid: {}", tx.txid());
|
println!("Tx broadcasted! Txid: {}", tx.txid());
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,12 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
(k, k_spks)
|
(k, k_spks)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let (update_graph, last_active_indices) = client
|
let (mut update_graph, last_active_indices) = client
|
||||||
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
|
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
|
||||||
.await?;
|
.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 missing_heights = update_graph.missing_heights(wallet.local_chain());
|
||||||
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
|
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
|
||||||
let update = Update {
|
let update = Update {
|
||||||
@@ -90,7 +93,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||||
assert!(finalized);
|
assert!(finalized);
|
||||||
|
|
||||||
let tx = psbt.extract_tx();
|
let tx = psbt.extract_tx()?;
|
||||||
client.broadcast(&tx).await?;
|
client.broadcast(&tx).await?;
|
||||||
println!("Tx broadcasted! Txid: {}", tx.txid());
|
println!("Tx broadcasted! Txid: {}", tx.txid());
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
print!("Syncing...");
|
print!("Syncing...");
|
||||||
let client =
|
let client =
|
||||||
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking()?;
|
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking();
|
||||||
|
|
||||||
let prev_tip = wallet.latest_checkpoint();
|
let prev_tip = wallet.latest_checkpoint();
|
||||||
let keychain_spks = wallet
|
let keychain_spks = wallet
|
||||||
@@ -53,8 +53,11 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let (update_graph, last_active_indices) =
|
let (mut update_graph, last_active_indices) =
|
||||||
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?;
|
client.full_scan(keychain_spks, 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 missing_heights = update_graph.missing_heights(wallet.local_chain());
|
||||||
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
|
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
|
||||||
let update = Update {
|
let update = Update {
|
||||||
@@ -90,7 +93,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||||
assert!(finalized);
|
assert!(finalized);
|
||||||
|
|
||||||
let tx = psbt.extract_tx();
|
let tx = psbt.extract_tx()?;
|
||||||
client.broadcast(&tx)?;
|
client.broadcast(&tx)?;
|
||||||
println!("Tx broadcasted! Txid: {}", tx.txid());
|
println!("Tx broadcasted! Txid: {}", tx.txid());
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ impl CoinSelectorOpt {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let mut tx = Transaction {
|
let mut tx = Transaction {
|
||||||
input: vec![],
|
input: vec![],
|
||||||
version: 1,
|
version: transaction::Version::ONE,
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
output: txouts.to_vec(),
|
output: txouts.to_vec(),
|
||||||
};
|
};
|
||||||
@@ -112,7 +112,7 @@ impl CoinSelectorOpt {
|
|||||||
target_value: if txouts.is_empty() {
|
target_value: if txouts.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(txouts.iter().map(|txout| txout.value).sum())
|
Some(txouts.iter().map(|txout| txout.value.to_sat()).sum())
|
||||||
},
|
},
|
||||||
..Self::from_weights(
|
..Self::from_weights(
|
||||||
base_weight.to_wu() as u32,
|
base_weight.to_wu() as u32,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use bdk_chain::{
|
|||||||
bitcoin,
|
bitcoin,
|
||||||
collections::{BTreeSet, HashMap},
|
collections::{BTreeSet, HashMap},
|
||||||
};
|
};
|
||||||
use bitcoin::{absolute, Transaction, TxOut};
|
use bitcoin::{absolute, transaction, Transaction, TxOut};
|
||||||
use core::fmt::{Debug, Display};
|
use core::fmt::{Debug, Display};
|
||||||
|
|
||||||
mod coin_selector;
|
mod coin_selector;
|
||||||
@@ -29,5 +29,5 @@ pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4;
|
|||||||
// Shamelessly copied from
|
// Shamelessly copied from
|
||||||
// https://github.com/rust-bitcoin/rust-miniscript/blob/d5615acda1a7fdc4041a11c1736af139b8c7ebe8/src/util.rs#L8
|
// https://github.com/rust-bitcoin/rust-miniscript/blob/d5615acda1a7fdc4041a11c1736af139b8c7ebe8/src/util.rs#L8
|
||||||
pub(crate) fn varint_size(v: usize) -> u32 {
|
pub(crate) fn varint_size(v: usize) -> u32 {
|
||||||
bitcoin::VarInt(v as u64).len() as u32
|
bitcoin::VarInt(v as u64).size() as u32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,13 @@
|
|||||||
use bdk_chain::{bitcoin, collections::*, miniscript};
|
use bdk_chain::{bitcoin, collections::*, miniscript};
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
absolute,
|
absolute,
|
||||||
address::WitnessVersion,
|
|
||||||
bip32::{DerivationPath, Fingerprint, KeySource},
|
bip32::{DerivationPath, Fingerprint, KeySource},
|
||||||
blockdata::transaction::Sequence,
|
blockdata::transaction::Sequence,
|
||||||
ecdsa,
|
ecdsa,
|
||||||
hashes::{hash160, ripemd160, sha256},
|
hashes::{hash160, ripemd160, sha256},
|
||||||
secp256k1::Secp256k1,
|
secp256k1::Secp256k1,
|
||||||
taproot::{self, LeafVersion, TapLeafHash},
|
taproot::{self, LeafVersion, TapLeafHash},
|
||||||
ScriptBuf, TxIn, Witness,
|
ScriptBuf, TxIn, Witness, WitnessVersion,
|
||||||
};
|
};
|
||||||
use miniscript::{
|
use miniscript::{
|
||||||
descriptor::{InnerXKey, Tr},
|
descriptor::{InnerXKey, Tr},
|
||||||
@@ -32,7 +31,7 @@ use miniscript::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn varint_len(v: usize) -> usize {
|
pub(crate) fn varint_len(v: usize) -> usize {
|
||||||
bitcoin::VarInt(v as u64).len() as usize
|
bitcoin::VarInt(v as u64).size() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
mod plan_impls;
|
mod plan_impls;
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ use core::ops::Deref;
|
|||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
bip32,
|
bip32,
|
||||||
hashes::{hash160, ripemd160, sha256},
|
hashes::{hash160, ripemd160, sha256, Hash},
|
||||||
key::XOnlyPublicKey,
|
key::XOnlyPublicKey,
|
||||||
psbt::Prevouts,
|
secp256k1::{Keypair, Message, PublicKey, Signing, Verification},
|
||||||
secp256k1::{KeyPair, Message, PublicKey, Signing, Verification},
|
|
||||||
sighash,
|
sighash,
|
||||||
sighash::{EcdsaSighashType, SighashCache, TapSighashType},
|
sighash::{EcdsaSighashType, Prevouts, SighashCache, TapSighashType},
|
||||||
taproot, Transaction, TxOut,
|
taproot, Transaction, TxOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,11 +162,11 @@ impl RequiredSignatures<DescriptorPublicKey> {
|
|||||||
|
|
||||||
let tweak =
|
let tweak =
|
||||||
taproot::TapTweakHash::from_key_and_tweak(x_only_pubkey, merkle_root.clone());
|
taproot::TapTweakHash::from_key_and_tweak(x_only_pubkey, merkle_root.clone());
|
||||||
let keypair = KeyPair::from_secret_key(&secp, &secret_key.clone())
|
let keypair = Keypair::from_secret_key(&secp, &secret_key.clone())
|
||||||
.add_xonly_tweak(&secp, &tweak.to_scalar())
|
.add_xonly_tweak(&secp, &tweak.to_scalar())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let msg = Message::from_slice(sighash.as_ref()).expect("Sighashes are 32 bytes");
|
let msg = Message::from_digest(sighash.to_byte_array());
|
||||||
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
||||||
|
|
||||||
let bitcoin_sig = taproot::Signature {
|
let bitcoin_sig = taproot::Signature {
|
||||||
@@ -209,9 +208,8 @@ impl RequiredSignatures<DescriptorPublicKey> {
|
|||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let keypair = KeyPair::from_secret_key(&secp, &secret_key.clone());
|
let keypair = Keypair::from_secret_key(&secp, &secret_key.clone());
|
||||||
let msg =
|
let msg = Message::from_digest(sighash.to_byte_array());
|
||||||
Message::from_slice(sighash.as_ref()).expect("Sighashes are 32 bytes");
|
|
||||||
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
||||||
let bitcoin_sig = taproot::Signature {
|
let bitcoin_sig = taproot::Signature {
|
||||||
sig,
|
sig,
|
||||||
|
|||||||
Reference in New Issue
Block a user