]> Untitled Git - bdk/commitdiff
Merge testutils crate into the main crate
authorLLFourn <lloyd.fourn@gmail.com>
Wed, 19 May 2021 03:04:32 +0000 (13:04 +1000)
committerLLFourn <lloyd.fourn@gmail.com>
Wed, 19 May 2021 06:45:48 +0000 (16:45 +1000)
This avoids having to keep the apis in sync between the macros and the
main project.

13 files changed:
.github/workflows/cont_integration.yml
Cargo.toml
src/blockchain/electrum.rs
src/blockchain/esplora.rs
src/lib.rs
src/testutils/blockchain_tests.rs [new file with mode: 0644]
src/testutils/mod.rs [new file with mode: 0644]
src/wallet/address_validator.rs
src/wallet/mod.rs
testutils/.gitignore [deleted file]
testutils/Cargo.toml [deleted file]
testutils/src/blockchain_tests.rs [deleted file]
testutils/src/lib.rs [deleted file]

index dd899cd4087e3b63b13a14c2dc1150a69a8269a5..9d6fe42035a32e5f0a2c7aba5852377c59b869c4 100644 (file)
@@ -120,7 +120,7 @@ jobs:
       - name: start ${{ matrix.blockchain.name }}
         run: nohup ${{ matrix.blockchain.start }} & sleep 5
       - name: Test
-        run: $HOME/.cargo/bin/cargo test --features test-${{ matrix.blockchain.name }} --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
+        run: $HOME/.cargo/bin/cargo test --features ${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
 
   check-wasm:
     name: Check WASM
index 44a5623caacad65e461ab7708e6d111f01f6b125..dda3121605770509d4af2a6b360abf2868c74132 100644 (file)
@@ -32,6 +32,10 @@ socks = { version = "0.3", optional = true }
 lazy_static = { version = "1.4", optional = true }
 tiny-bip39 = { version = "^0.8", optional = true }
 
+# Needed by bdk_blockchain_tests macro
+bitcoincore-rpc = {  version = "0.13", optional = true }
+serial_test = { version = "0.4", optional = true }
+
 # Platform-specific dependencies
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tokio = { version = "1", features = ["rt"] }
@@ -54,17 +58,16 @@ all-keys = ["keys-bip39"]
 keys-bip39 = ["tiny-bip39"]
 
 # Debug/Test features
-test-electrum = ["electrum"]
-test-esplora = ["esplora"]
+testutils = []
+test-blockchains = ["testutils", "bitcoincore-rpc", "electrum-client"]
 test-md-docs = ["electrum"]
 
 [dev-dependencies]
-bdk-testutils = { path = "./testutils" }
-serial_test = "0.4"
 lazy_static = "1.4"
 env_logger = "0.7"
 base64 = "^0.11"
 clap = "2.33"
+serial_test = "0.4"
 
 [[example]]
 name = "address_validator"
@@ -78,10 +81,7 @@ path = "examples/compiler.rs"
 required-features = ["compiler"]
 
 [workspace]
-members = ["macros", "testutils"]
-
-# Generate docs with nightly to add the "features required" badge
-# https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do
+members = ["macros"]
 [package.metadata.docs.rs]
 features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"]
 # defines the configuration attribute `docsrs`
index 2e1033077c0bf314f39a01fb941993be34693f82..a37f055625874f409a9e8a7ecac01e3ce51092df 100644 (file)
@@ -169,9 +169,8 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
     }
 }
 
-#[cfg(all(feature = "test-electrum", test))]
-testutils::bdk_blockchain_tests! {
-    bdk => crate,
+#[cfg(feature = "test-blockchains")]
+crate::bdk_blockchain_tests! {
     fn test_instance() -> ElectrumBlockchain {
         ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap())
     }
index 5214fcec049efaf3d6bbb133f0ab0ed8f7a369df..ff85f22f67986fefb8768bb0b4c5d5f18609dce5 100644 (file)
@@ -415,9 +415,8 @@ impl_error!(std::num::ParseIntError, Parsing, EsploraError);
 impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
 impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
 
-#[cfg(all(feature = "test-esplora", test))]
-testutils::bdk_blockchain_tests! {
-    bdk => crate,
+#[cfg(feature = "test-blockchains")]
+crate::bdk_blockchain_tests! {
     fn test_instance() -> EsploraBlockchain {
         EsploraBlockchain::new(std::env::var("BDK_ESPLORA_URL").unwrap_or("127.0.0.1:3002".into()).as_str(), None)
     }
index a4670b5c52ad20d367569da1c0ffe716a42817eb..722f680a85d63fd9e91cac8547f1c61786369e4f 100644 (file)
@@ -228,16 +228,12 @@ pub extern crate reqwest;
 #[cfg(feature = "key-value-db")]
 pub extern crate sled;
 
-#[allow(unused_imports)]
-#[cfg(test)]
-#[macro_use]
-extern crate testutils;
 #[allow(unused_imports)]
 #[cfg(test)]
 #[allow(unused_imports)]
 #[cfg(test)]
 #[macro_use]
-extern crate serial_test;
+pub extern crate serial_test;
 
 #[macro_use]
 pub(crate) mod error;
@@ -265,3 +261,6 @@ pub use wallet::Wallet;
 pub fn version() -> &'static str {
     env!("CARGO_PKG_VERSION", "unknown")
 }
+
+#[cfg(any(feature = "testutils", test))]
+pub mod testutils;
diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs
new file mode 100644 (file)
index 0000000..1c470f0
--- /dev/null
@@ -0,0 +1,495 @@
+/// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a
+/// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at
+/// the setup required to run the tests yourself.
+#[macro_export]
+macro_rules! bdk_blockchain_tests {
+    (
+     fn test_instance() -> $blockchain:ty $block:block) => {
+        #[cfg(test)]
+        mod bdk_blockchain_tests {
+            use $crate::bitcoin::Network;
+            use $crate::testutils::{TestClient};
+            use $crate::blockchain::noop_progress;
+            use $crate::database::MemoryDatabase;
+            use $crate::types::KeychainKind;
+            use $crate::{Wallet, FeeRate};
+            use $crate::wallet::AddressIndex::New;
+            use $crate::testutils;
+            use $crate::serial_test::serial;
+
+            use super::*;
+
+            fn get_blockchain() -> $blockchain {
+                $block
+            }
+
+            fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<$blockchain, MemoryDatabase> {
+                Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
+            }
+
+            fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
+                let descriptors = testutils! {
+                    @descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
+                };
+
+                let test_client = TestClient::default();
+                let wallet = get_wallet_from_descriptors(&descriptors);
+
+                (wallet, descriptors, test_client)
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_simple() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                let tx = testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 )
+                };
+                println!("{:?}", tx);
+                let txid = test_client.receive(tx);
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+                assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
+
+                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
+                assert_eq!(list_tx_item.txid, txid);
+                assert_eq!(list_tx_item.received, 50_000);
+                assert_eq!(list_tx_item.sent, 0);
+                assert_eq!(list_tx_item.height, None);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_stop_gap_20() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 5) => 50_000 )
+                });
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 25) => 50_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 100_000);
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_before_and_after_receive() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 0);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_multiple_outputs_same_tx() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                let txid = test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 105_000);
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
+                assert_eq!(wallet.list_unspent().unwrap().len(), 3);
+
+                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
+                assert_eq!(list_tx_item.txid, txid);
+                assert_eq!(list_tx_item.received, 105_000);
+                assert_eq!(list_tx_item.sent, 0);
+                assert_eq!(list_tx_item.height, None);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_receive_multi() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 )
+                });
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 5) => 25_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 75_000);
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
+                assert_eq!(wallet.list_unspent().unwrap().len(), 2);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_address_reuse() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 25_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 75_000);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_receive_rbf_replaced() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                let txid = test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
+                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
+
+                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
+                assert_eq!(list_tx_item.txid, txid);
+                assert_eq!(list_tx_item.received, 50_000);
+                assert_eq!(list_tx_item.sent, 0);
+                assert_eq!(list_tx_item.height, None);
+
+                let new_txid = test_client.bump_fee(&txid);
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
+                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
+
+                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
+                assert_eq!(list_tx_item.txid, new_txid);
+                assert_eq!(list_tx_item.received, 50_000);
+                assert_eq!(list_tx_item.sent, 0);
+                assert_eq!(list_tx_item.height, None);
+            }
+
+            // FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it
+            // doesn't work for some reason.
+            #[cfg(not(feature = "esplora"))]
+            #[test]
+            #[serial]
+            fn test_sync_reorg_block() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+
+                let txid = test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
+                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
+
+                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
+                assert_eq!(list_tx_item.txid, txid);
+                assert!(list_tx_item.height.is_some());
+
+                // Invalidate 1 block
+                test_client.invalidate(1);
+
+                wallet.sync(noop_progress(), None).unwrap();
+
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
+                assert_eq!(list_tx_item.txid, txid);
+                assert_eq!(list_tx_item.height, None);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_after_send() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+                println!("{}", descriptors.0);
+                let node_addr = test_client.get_node_address(None);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                let mut builder = wallet.build_tx();
+                builder.add_recipient(node_addr.script_pubkey(), 25_000);
+                let (mut psbt, details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                let tx = psbt.extract_tx();
+                println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
+                wallet.broadcast(tx).unwrap();
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), details.received);
+
+                assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
+                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_outgoing_from_scratch() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+                let node_addr = test_client.get_node_address(None);
+
+                let received_txid = test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                let mut builder = wallet.build_tx();
+                builder.add_recipient(node_addr.script_pubkey(), 25_000);
+                let (mut psbt, details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), details.received);
+
+                // empty wallet
+                let wallet = get_wallet_from_descriptors(&descriptors);
+                wallet.sync(noop_progress(), None).unwrap();
+
+                let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
+
+                let received = tx_map.get(&received_txid).unwrap();
+                assert_eq!(received.received, 50_000);
+                assert_eq!(received.sent, 0);
+
+                let sent = tx_map.get(&sent_txid).unwrap();
+                assert_eq!(sent.received, details.received);
+                assert_eq!(sent.sent, details.sent);
+                assert_eq!(sent.fees, details.fees);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_long_change_chain() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+                let node_addr = test_client.get_node_address(None);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 )
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                let mut total_sent = 0;
+                for _ in 0..5 {
+                    let mut builder = wallet.build_tx();
+                    builder.add_recipient(node_addr.script_pubkey(), 5_000);
+                    let (mut psbt, details) = builder.finish().unwrap();
+                    let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(psbt.extract_tx()).unwrap();
+
+                    wallet.sync(noop_progress(), None).unwrap();
+
+                    total_sent += 5_000 + details.fees;
+                }
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
+
+                // empty wallet
+                let wallet = get_wallet_from_descriptors(&descriptors);
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_bump_fee() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+                let node_addr = test_client.get_node_address(None);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                let mut builder = wallet.build_tx();
+                builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
+                let (mut psbt, details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
+                assert_eq!(wallet.get_balance().unwrap(), details.received);
+
+                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
+                builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
+                let (mut new_psbt, new_details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
+                assert_eq!(wallet.get_balance().unwrap(), new_details.received);
+
+                assert!(new_details.fees > details.fees);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_bump_fee_remove_change() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+                let node_addr = test_client.get_node_address(None);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                let mut builder = wallet.build_tx();
+                builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
+                let (mut psbt, details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
+                assert_eq!(wallet.get_balance().unwrap(), details.received);
+
+                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
+                builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
+                let (mut new_psbt, new_details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 0);
+                assert_eq!(new_details.received, 0);
+
+                assert!(new_details.fees > details.fees);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_bump_fee_add_input() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+                let node_addr = test_client.get_node_address(None);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 75_000);
+
+                let mut builder = wallet.build_tx();
+                builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
+                let (mut psbt, details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
+                assert_eq!(details.received, 1_000 - details.fees);
+
+                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
+                builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
+                let (mut new_psbt, new_details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(new_details.sent, 75_000);
+                assert_eq!(wallet.get_balance().unwrap(), new_details.received);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_bump_fee_add_input_no_change() {
+                let (wallet, descriptors, mut test_client) = init_single_sig();
+                let node_addr = test_client.get_node_address(None);
+
+                test_client.receive(testutils! {
+                    @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
+                });
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 75_000);
+
+                let mut builder = wallet.build_tx();
+                builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
+                let (mut psbt, details) = builder.finish().unwrap();
+                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
+                assert_eq!(details.received, 1_000 - details.fees);
+
+                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
+                builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
+                let (mut new_psbt, new_details) = builder.finish().unwrap();
+                println!("{:#?}", new_details);
+
+                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
+                assert!(finalized, "Cannot finalize transaction");
+                wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(new_details.sent, 75_000);
+                assert_eq!(wallet.get_balance().unwrap(), 0);
+                assert_eq!(new_details.received, 0);
+            }
+
+            #[test]
+            #[serial]
+            fn test_sync_receive_coinbase() {
+                let (wallet, _, mut test_client) = init_single_sig();
+                let wallet_addr = wallet.get_address(New).unwrap();
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert_eq!(wallet.get_balance().unwrap(), 0);
+
+                test_client.generate(1, Some(wallet_addr));
+
+                wallet.sync(noop_progress(), None).unwrap();
+                assert!(wallet.get_balance().unwrap() > 0);
+            }
+        }
+    }
+}
diff --git a/src/testutils/mod.rs b/src/testutils/mod.rs
new file mode 100644 (file)
index 0000000..4771c94
--- /dev/null
@@ -0,0 +1,579 @@
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+#![allow(missing_docs)]
+
+mod blockchain_tests;
+
+// pub use serial_test::serial;
+
+use std::collections::HashMap;
+use std::env;
+use std::ops::Deref;
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::time::Duration;
+
+#[allow(unused_imports)]
+use log::{debug, error, info, trace};
+
+use bitcoin::consensus::encode::{deserialize, serialize};
+use bitcoin::hashes::hex::{FromHex, ToHex};
+use bitcoin::hashes::sha256d;
+use bitcoin::secp256k1::{Secp256k1, Verification};
+use bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid};
+
+use miniscript::descriptor::DescriptorPublicKey;
+use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
+
+pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
+pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
+
+pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
+
+// TODO: we currently only support env vars, we could also parse a toml file
+fn get_auth() -> Auth {
+    match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
+        Ok("USER_PASS") => Auth::UserPass(
+            env::var("BDK_RPC_USER").unwrap(),
+            env::var("BDK_RPC_PASS").unwrap(),
+        ),
+        _ => Auth::CookieFile(PathBuf::from(
+            env::var("BDK_RPC_COOKIEFILE")
+                .unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
+        )),
+    }
+}
+
+pub fn get_electrum_url() -> String {
+    env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
+}
+
+pub struct TestClient {
+    client: RpcClient,
+    electrum: ElectrumClient,
+}
+
+#[derive(Clone, Debug)]
+pub struct TestIncomingOutput {
+    pub value: u64,
+    pub to_address: String,
+}
+
+impl TestIncomingOutput {
+    pub fn new(value: u64, to_address: Address) -> Self {
+        Self {
+            value,
+            to_address: to_address.to_string(),
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct TestIncomingTx {
+    pub output: Vec<TestIncomingOutput>,
+    pub min_confirmations: Option<u64>,
+    pub locktime: Option<i64>,
+    pub replaceable: Option<bool>,
+}
+
+impl TestIncomingTx {
+    pub fn new(
+        output: Vec<TestIncomingOutput>,
+        min_confirmations: Option<u64>,
+        locktime: Option<i64>,
+        replaceable: Option<bool>,
+    ) -> Self {
+        Self {
+            output,
+            min_confirmations,
+            locktime,
+            replaceable,
+        }
+    }
+
+    pub fn add_output(&mut self, output: TestIncomingOutput) {
+        self.output.push(output);
+    }
+}
+
+#[doc(hidden)]
+pub trait TranslateDescriptor {
+    // derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
+    fn derive_translated<C: Verification>(
+        &self,
+        secp: &Secp256k1<C>,
+        index: u32,
+    ) -> Descriptor<PublicKey>;
+}
+
+impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
+    fn derive_translated<C: Verification>(
+        &self,
+        secp: &Secp256k1<C>,
+        index: u32,
+    ) -> Descriptor<PublicKey> {
+        let translate = |key: &DescriptorPublicKey| -> PublicKey {
+            match key {
+                DescriptorPublicKey::XPub(xpub) => {
+                    xpub.xkey
+                        .derive_pub(secp, &xpub.derivation_path)
+                        .expect("hardened derivation steps")
+                        .public_key
+                }
+                DescriptorPublicKey::SinglePub(key) => key.key,
+            }
+        };
+
+        self.derive(index)
+            .translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
+    }
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! testutils {
+    ( @external $descriptors:expr, $child:expr ) => ({
+        use bitcoin::secp256k1::Secp256k1;
+        use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
+
+        use $crate::testutils::TranslateDescriptor;
+
+        let secp = Secp256k1::new();
+
+        let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
+        parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
+    });
+    ( @internal $descriptors:expr, $child:expr ) => ({
+        use bitcoin::secp256k1::Secp256k1;
+        use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
+
+        use $crate::testutils::TranslateDescriptor;
+
+        let secp = Secp256k1::new();
+
+        let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
+        parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
+    });
+    ( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
+    ( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
+
+    ( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({
+        let mut outs = Vec::new();
+        $( outs.push($crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+
+        #[allow(unused_mut)]
+        let mut locktime = None::<i64>;
+        $( locktime = Some($locktime); )*
+
+        #[allow(unused_assignments, unused_mut)]
+        let mut min_confirmations = None::<u64>;
+        $( min_confirmations = Some($confirmations); )*
+
+        #[allow(unused_assignments, unused_mut)]
+        let mut replaceable = None::<bool>;
+        $( replaceable = Some($replaceable); )*
+
+        $crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
+    });
+
+    ( @literal $key:expr ) => ({
+        let key = $key.to_string();
+        (key, None::<String>, None::<String>)
+    });
+    ( @generate_xprv $( $external_path:expr )? $( ,$internal_path:expr )? ) => ({
+        use rand::Rng;
+
+        let mut seed = [0u8; 32];
+        rand::thread_rng().fill(&mut seed[..]);
+
+        let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
+            bitcoin::Network::Testnet,
+            &seed,
+        );
+
+        #[allow(unused_assignments)]
+        let mut external_path = None::<String>;
+        $( external_path = Some($external_path.to_string()); )?
+
+        #[allow(unused_assignments)]
+        let mut internal_path = None::<String>;
+        $( internal_path = Some($internal_path.to_string()); )?
+
+        (key.unwrap().to_string(), external_path, internal_path)
+    });
+    ( @generate_wif ) => ({
+        use rand::Rng;
+
+        let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
+        rand::thread_rng().fill(&mut key[..]);
+
+        (bitcoin::PrivateKey {
+            compressed: true,
+            network: bitcoin::Network::Testnet,
+            key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
+        }.to_string(), None::<String>, None::<String>)
+    });
+
+    ( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
+        let mut map = std::collections::HashMap::new();
+        $(
+            let alias: &str = $alias;
+            map.insert(alias, testutils!( $($key_type)* ));
+        )+
+
+        map
+    });
+
+    ( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({
+        use std::str::FromStr;
+        use std::collections::HashMap;
+        use miniscript::descriptor::Descriptor;
+        use miniscript::TranslatePk;
+
+        #[allow(unused_assignments, unused_mut)]
+        let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
+        $(
+            keys = testutils!{ @keys $( $keys )* };
+        )*
+
+        let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
+        let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
+            if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
+                format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
+            } else {
+                k.clone()
+            }
+        }, |kh| {
+            if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
+                format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
+            } else {
+                kh.clone()
+            }
+
+        });
+        let external = external.to_string();
+
+        #[allow(unused_assignments, unused_mut)]
+        let mut internal = None::<String>;
+        $(
+            let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
+
+            let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
+                if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
+                    format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
+                } else {
+                    k.clone()
+                }
+            }, |kh| {
+                if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
+                    format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
+                } else {
+                    kh.clone()
+                }
+            });
+            internal = Some(string_internal.to_string());
+        )*
+
+        (external, internal)
+    })
+}
+
+fn exponential_backoff_poll<T, F>(mut poll: F) -> T
+where
+    F: FnMut() -> Option<T>,
+{
+    let mut delay = Duration::from_millis(64);
+    loop {
+        match poll() {
+            Some(data) => break data,
+            None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
+            None => {}
+        }
+
+        std::thread::sleep(delay);
+    }
+}
+
+impl TestClient {
+    pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self {
+        let client = RpcClient::new(
+            format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name),
+            get_auth(),
+        )
+        .unwrap();
+        let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
+
+        TestClient { client, electrum }
+    }
+
+    fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
+        // wait for electrs to index the tx
+        exponential_backoff_poll(|| {
+            trace!("wait_for_tx {}", txid);
+
+            self.electrum
+                .script_get_history(monitor_script)
+                .unwrap()
+                .iter()
+                .position(|entry| entry.tx_hash == txid)
+        });
+    }
+
+    fn wait_for_block(&mut self, min_height: usize) {
+        self.electrum.block_headers_subscribe().unwrap();
+
+        loop {
+            let header = exponential_backoff_poll(|| {
+                self.electrum.ping().unwrap();
+                self.electrum.block_headers_pop().unwrap()
+            });
+            if header.height >= min_height {
+                break;
+            }
+        }
+    }
+
+    pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
+        assert!(
+            !meta_tx.output.is_empty(),
+            "can't create a transaction with no outputs"
+        );
+
+        let mut map = HashMap::new();
+
+        let mut required_balance = 0;
+        for out in &meta_tx.output {
+            required_balance += out.value;
+            map.insert(out.to_address.clone(), Amount::from_sat(out.value));
+        }
+
+        if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
+            panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
+        }
+
+        // FIXME: core can't create a tx with two outputs to the same address
+        let tx = self
+            .create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
+            .unwrap();
+        let tx = self.fund_raw_transaction(tx, None, None).unwrap();
+        let mut tx: Transaction = deserialize(&tx.hex).unwrap();
+
+        if let Some(true) = meta_tx.replaceable {
+            // for some reason core doesn't set this field right
+            for input in &mut tx.input {
+                input.sequence = 0xFFFFFFFD;
+            }
+        }
+
+        let tx = self
+            .sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
+            .unwrap();
+
+        // broadcast through electrum so that it caches the tx immediately
+        let txid = self
+            .electrum
+            .transaction_broadcast(&deserialize(&tx.hex).unwrap())
+            .unwrap();
+
+        if let Some(num) = meta_tx.min_confirmations {
+            self.generate(num, None);
+        }
+
+        let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
+            .unwrap()
+            .script_pubkey();
+        self.wait_for_tx(txid, &monitor_script);
+
+        debug!("Sent tx: {}", txid);
+
+        txid
+    }
+
+    pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
+        let tx = self.get_raw_transaction_info(txid, None).unwrap();
+        assert!(
+            tx.confirmations.is_none(),
+            "Can't bump tx {} because it's already confirmed",
+            txid
+        );
+
+        let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
+        let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
+
+        let monitor_script =
+            tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
+        self.wait_for_tx(new_txid, &monitor_script);
+
+        debug!("Bumped {}, new txid {}", txid, new_txid);
+
+        new_txid
+    }
+
+    pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
+        use bitcoin::blockdata::block::{Block, BlockHeader};
+        use bitcoin::blockdata::script::Builder;
+        use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
+        use bitcoin::hash_types::{BlockHash, TxMerkleNode};
+
+        let block_template: serde_json::Value = self
+            .call("getblocktemplate", &[json!({"rules": ["segwit"]})])
+            .unwrap();
+        trace!("getblocktemplate: {:#?}", block_template);
+
+        let header = BlockHeader {
+            version: block_template["version"].as_i64().unwrap() as i32,
+            prev_blockhash: BlockHash::from_hex(
+                block_template["previousblockhash"].as_str().unwrap(),
+            )
+            .unwrap(),
+            merkle_root: TxMerkleNode::default(),
+            time: block_template["curtime"].as_u64().unwrap() as u32,
+            bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
+            nonce: 0,
+        };
+        debug!("header: {:#?}", header);
+
+        let height = block_template["height"].as_u64().unwrap() as i64;
+        let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
+        // burn block subsidy and fees, not a big deal
+        let mut coinbase_tx = Transaction {
+            version: 1,
+            lock_time: 0,
+            input: vec![TxIn {
+                previous_output: OutPoint::null(),
+                script_sig: Builder::new().push_int(height).into_script(),
+                sequence: 0xFFFFFFFF,
+                witness: vec![witness_reserved_value],
+            }],
+            output: vec![],
+        };
+
+        let mut txdata = vec![coinbase_tx.clone()];
+        txdata.extend_from_slice(&txs);
+
+        let mut block = Block { header, txdata };
+
+        let witness_root = block.witness_root();
+        let witness_commitment =
+            Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
+
+        // now update and replace the coinbase tx
+        let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
+        coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
+
+        coinbase_tx.output.push(TxOut {
+            value: 0,
+            script_pubkey: coinbase_witness_commitment_script.into(),
+        });
+        block.txdata[0] = coinbase_tx;
+
+        // set merkle root
+        let merkle_root = block.merkle_root();
+        block.header.merkle_root = merkle_root;
+
+        assert!(block.check_merkle_root());
+        assert!(block.check_witness_commitment());
+
+        // now do PoW :)
+        let target = block.header.target();
+        while block.header.validate_pow(&target).is_err() {
+            block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
+        }
+
+        let block_hex: String = serialize(&block).to_hex();
+        debug!("generated block hex: {}", block_hex);
+
+        self.electrum.block_headers_subscribe().unwrap();
+
+        let submit_result: serde_json::Value =
+            self.call("submitblock", &[block_hex.into()]).unwrap();
+        debug!("submitblock: {:?}", submit_result);
+        assert!(
+            submit_result.is_null(),
+            "submitblock error: {:?}",
+            submit_result.as_str()
+        );
+
+        self.wait_for_block(height as usize);
+
+        block.header.block_hash().to_hex()
+    }
+
+    pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
+        let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
+        let hashes = self.generate_to_address(num_blocks, &address).unwrap();
+        let best_hash = hashes.last().unwrap();
+        let height = self.get_block_info(best_hash).unwrap().height;
+
+        self.wait_for_block(height);
+
+        debug!("Generated blocks to new height {}", height);
+    }
+
+    pub fn invalidate(&mut self, num_blocks: u64) {
+        self.electrum.block_headers_subscribe().unwrap();
+
+        let best_hash = self.get_best_block_hash().unwrap();
+        let initial_height = self.get_block_info(&best_hash).unwrap().height;
+
+        let mut to_invalidate = best_hash;
+        for i in 1..=num_blocks {
+            trace!(
+                "Invalidating block {}/{} ({})",
+                i,
+                num_blocks,
+                to_invalidate
+            );
+
+            self.invalidate_block(&to_invalidate).unwrap();
+            to_invalidate = self.get_best_block_hash().unwrap();
+        }
+
+        self.wait_for_block(initial_height - num_blocks as usize);
+
+        debug!(
+            "Invalidated {} blocks to new height of {}",
+            num_blocks,
+            initial_height - num_blocks as usize
+        );
+    }
+
+    pub fn reorg(&mut self, num_blocks: u64) {
+        self.invalidate(num_blocks);
+        self.generate(num_blocks, None);
+    }
+
+    pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
+        Address::from_str(
+            &self
+                .get_new_address(None, address_type)
+                .unwrap()
+                .to_string(),
+        )
+        .unwrap()
+    }
+}
+
+impl Deref for TestClient {
+    type Target = RpcClient;
+
+    fn deref(&self) -> &Self::Target {
+        &self.client
+    }
+}
+
+impl Default for TestClient {
+    fn default() -> Self {
+        let rpc_host_and_port =
+            env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
+        let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
+        Self::new(rpc_host_and_port, wallet)
+    }
+}
index 4e20ef5e0e65e5d8353ed1e280c10ecdaa1cd38d..36e39be19a0298be1da94aaf486c010d67c3f4c6 100644 (file)
@@ -146,7 +146,7 @@ mod test {
         let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
         wallet.add_address_validator(Arc::new(TestValidator));
 
-        let addr = testutils!(@external descriptors, 10);
+        let addr = crate::testutils!(@external descriptors, 10);
         let mut builder = wallet.build_tx();
         builder.add_recipient(addr.script_pubkey(), 25_000);
         builder.finish().unwrap();
index 71fc5d7d2edef0b9e9fdc1494dbba201a9afd3fe..28d954e4384dd858905bac528d725ce6249649cf 100644 (file)
@@ -1515,6 +1515,7 @@ pub(crate) mod test {
     use crate::types::KeychainKind;
 
     use super::*;
+    use crate::testutils;
     use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset};
 
     #[test]
diff --git a/testutils/.gitignore b/testutils/.gitignore
deleted file mode 100644 (file)
index 2c96eb1..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-target/
-Cargo.lock
diff --git a/testutils/Cargo.toml b/testutils/Cargo.toml
deleted file mode 100644 (file)
index 8f7f261..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-[package]
-name = "bdk-testutils"
-version = "0.4.0"
-authors = ["Alekos Filini <alekos.filini@gmail.com>"]
-edition = "2018"
-homepage = "https://bitcoindevkit.org"
-repository = "https://github.com/bitcoindevkit/bdk"
-documentation = "https://docs.rs/bdk-testutils"
-description = "Supporting testing utilities for `bdk`"
-keywords = ["bdk"]
-license = "MIT OR Apache-2.0"
-
-[lib]
-name = "testutils"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-log = "0.4.8"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-serial_test = "0.4"
-bitcoin = "0.26"
-bitcoincore-rpc = "0.13"
-miniscript = "5.1"
-electrum-client = "0.6.0"
diff --git a/testutils/src/blockchain_tests.rs b/testutils/src/blockchain_tests.rs
deleted file mode 100644 (file)
index 07953a3..0000000
+++ /dev/null
@@ -1,494 +0,0 @@
-/// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a
-/// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at
-/// the setup required to run the tests yourself.
-#[macro_export]
-macro_rules! bdk_blockchain_tests {
-    (bdk => $bdk:ident,
-     fn test_instance() -> $blockchain:ty $block:block) => {
-        mod bdk_blockchain_tests {
-            use $bdk::bitcoin::Network;
-            use $bdk::miniscript::Descriptor;
-            use $crate::{TestClient, serial};
-            use $bdk::blockchain::{Blockchain, noop_progress};
-            use $bdk::descriptor::ExtendedDescriptor;
-            use $bdk::database::MemoryDatabase;
-            use $bdk::types::KeychainKind;
-            use $bdk::{Wallet, TxBuilder, FeeRate};
-            use $bdk::wallet::AddressIndex::New;
-
-            use super::*;
-
-            fn get_blockchain() -> $blockchain {
-                $block
-            }
-
-            fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<$blockchain, MemoryDatabase> {
-                Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
-            }
-
-            fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
-                let descriptors = testutils! {
-                    @descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
-                };
-
-                let test_client = TestClient::default();
-                let wallet = get_wallet_from_descriptors(&descriptors);
-
-                (wallet, descriptors, test_client)
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_simple() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                let tx = testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 )
-                };
-                println!("{:?}", tx);
-                let txid = test_client.receive(tx);
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-                assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
-
-                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
-                assert_eq!(list_tx_item.txid, txid);
-                assert_eq!(list_tx_item.received, 50_000);
-                assert_eq!(list_tx_item.sent, 0);
-                assert_eq!(list_tx_item.height, None);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_stop_gap_20() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 5) => 50_000 )
-                });
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 25) => 50_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 100_000);
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_before_and_after_receive() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 0);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_multiple_outputs_same_tx() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                let txid = test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 105_000);
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
-                assert_eq!(wallet.list_unspent().unwrap().len(), 3);
-
-                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
-                assert_eq!(list_tx_item.txid, txid);
-                assert_eq!(list_tx_item.received, 105_000);
-                assert_eq!(list_tx_item.sent, 0);
-                assert_eq!(list_tx_item.height, None);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_receive_multi() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 )
-                });
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 5) => 25_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 75_000);
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
-                assert_eq!(wallet.list_unspent().unwrap().len(), 2);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_address_reuse() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 25_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 75_000);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_receive_rbf_replaced() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                let txid = test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
-                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
-
-                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
-                assert_eq!(list_tx_item.txid, txid);
-                assert_eq!(list_tx_item.received, 50_000);
-                assert_eq!(list_tx_item.sent, 0);
-                assert_eq!(list_tx_item.height, None);
-
-                let new_txid = test_client.bump_fee(&txid);
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
-                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
-
-                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
-                assert_eq!(list_tx_item.txid, new_txid);
-                assert_eq!(list_tx_item.received, 50_000);
-                assert_eq!(list_tx_item.sent, 0);
-                assert_eq!(list_tx_item.height, None);
-            }
-
-            // FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it
-            // doesn't work for some reason.
-            #[cfg(not(feature = "test-esplora"))]
-            #[test]
-            #[serial]
-            fn test_sync_reorg_block() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-
-                let txid = test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
-                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
-
-                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
-                assert_eq!(list_tx_item.txid, txid);
-                assert!(list_tx_item.height.is_some());
-
-                // Invalidate 1 block
-                test_client.invalidate(1);
-
-                wallet.sync(noop_progress(), None).unwrap();
-
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-
-                let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
-                assert_eq!(list_tx_item.txid, txid);
-                assert_eq!(list_tx_item.height, None);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_after_send() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                println!("{}", descriptors.0);
-                let node_addr = test_client.get_node_address(None);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-
-                let mut builder = wallet.build_tx();
-                builder.add_recipient(node_addr.script_pubkey(), 25_000);
-                let (mut psbt, details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                let tx = psbt.extract_tx();
-                println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
-                wallet.broadcast(tx).unwrap();
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), details.received);
-
-                assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
-                assert_eq!(wallet.list_unspent().unwrap().len(), 1);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_outgoing_from_scratch() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                let node_addr = test_client.get_node_address(None);
-
-                let received_txid = test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-
-                let mut builder = wallet.build_tx();
-                builder.add_recipient(node_addr.script_pubkey(), 25_000);
-                let (mut psbt, details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), details.received);
-
-                // empty wallet
-                let wallet = get_wallet_from_descriptors(&descriptors);
-                wallet.sync(noop_progress(), None).unwrap();
-
-                let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
-
-                let received = tx_map.get(&received_txid).unwrap();
-                assert_eq!(received.received, 50_000);
-                assert_eq!(received.sent, 0);
-
-                let sent = tx_map.get(&sent_txid).unwrap();
-                assert_eq!(sent.received, details.received);
-                assert_eq!(sent.sent, details.sent);
-                assert_eq!(sent.fees, details.fees);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_long_change_chain() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                let node_addr = test_client.get_node_address(None);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 )
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-
-                let mut total_sent = 0;
-                for _ in 0..5 {
-                    let mut builder = wallet.build_tx();
-                    builder.add_recipient(node_addr.script_pubkey(), 5_000);
-                    let (mut psbt, details) = builder.finish().unwrap();
-                    let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
-                    assert!(finalized, "Cannot finalize transaction");
-                    wallet.broadcast(psbt.extract_tx()).unwrap();
-
-                    wallet.sync(noop_progress(), None).unwrap();
-
-                    total_sent += 5_000 + details.fees;
-                }
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
-
-                // empty wallet
-                let wallet = get_wallet_from_descriptors(&descriptors);
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_bump_fee() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                let node_addr = test_client.get_node_address(None);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-
-                let mut builder = wallet.build_tx();
-                builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
-                let (mut psbt, details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
-                assert_eq!(wallet.get_balance().unwrap(), details.received);
-
-                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
-                builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
-                let (mut new_psbt, new_details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(new_psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
-                assert_eq!(wallet.get_balance().unwrap(), new_details.received);
-
-                assert!(new_details.fees > details.fees);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_bump_fee_remove_change() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                let node_addr = test_client.get_node_address(None);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 50_000);
-
-                let mut builder = wallet.build_tx();
-                builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
-                let (mut psbt, details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
-                assert_eq!(wallet.get_balance().unwrap(), details.received);
-
-                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
-                builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
-                let (mut new_psbt, new_details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(new_psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 0);
-                assert_eq!(new_details.received, 0);
-
-                assert!(new_details.fees > details.fees);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_bump_fee_add_input() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                let node_addr = test_client.get_node_address(None);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 75_000);
-
-                let mut builder = wallet.build_tx();
-                builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
-                let (mut psbt, details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
-                assert_eq!(details.received, 1_000 - details.fees);
-
-                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
-                builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
-                let (mut new_psbt, new_details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(new_psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(new_details.sent, 75_000);
-                assert_eq!(wallet.get_balance().unwrap(), new_details.received);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_bump_fee_add_input_no_change() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                let node_addr = test_client.get_node_address(None);
-
-                test_client.receive(testutils! {
-                    @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
-                });
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 75_000);
-
-                let mut builder = wallet.build_tx();
-                builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
-                let (mut psbt, details) = builder.finish().unwrap();
-                let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
-                assert_eq!(details.received, 1_000 - details.fees);
-
-                let mut builder = wallet.build_fee_bump(details.txid).unwrap();
-                builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
-                let (mut new_psbt, new_details) = builder.finish().unwrap();
-                println!("{:#?}", new_details);
-
-                let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
-                assert!(finalized, "Cannot finalize transaction");
-                wallet.broadcast(new_psbt.extract_tx()).unwrap();
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(new_details.sent, 75_000);
-                assert_eq!(wallet.get_balance().unwrap(), 0);
-                assert_eq!(new_details.received, 0);
-            }
-
-            #[test]
-            #[serial]
-            fn test_sync_receive_coinbase() {
-                let (wallet, descriptors, mut test_client) = init_single_sig();
-                let wallet_addr = wallet.get_address(New).unwrap();
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert_eq!(wallet.get_balance().unwrap(), 0);
-
-                test_client.generate(1, Some(wallet_addr));
-
-                wallet.sync(noop_progress(), None).unwrap();
-                assert!(wallet.get_balance().unwrap() > 0);
-            }
-        }
-    }
-}
diff --git a/testutils/src/lib.rs b/testutils/src/lib.rs
deleted file mode 100644 (file)
index 333af54..0000000
+++ /dev/null
@@ -1,575 +0,0 @@
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-#[macro_use]
-extern crate serde_json;
-mod blockchain_tests;
-
-pub use serial_test::serial;
-
-use std::collections::HashMap;
-use std::env;
-use std::ops::Deref;
-use std::path::PathBuf;
-use std::str::FromStr;
-use std::time::Duration;
-
-#[allow(unused_imports)]
-use log::{debug, error, info, trace};
-
-use bitcoin::consensus::encode::{deserialize, serialize};
-use bitcoin::hashes::hex::{FromHex, ToHex};
-use bitcoin::hashes::sha256d;
-use bitcoin::secp256k1::{Secp256k1, Verification};
-use bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid};
-
-use miniscript::descriptor::DescriptorPublicKey;
-use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
-
-pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
-pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
-
-pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
-
-// TODO: we currently only support env vars, we could also parse a toml file
-fn get_auth() -> Auth {
-    match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
-        Ok("USER_PASS") => Auth::UserPass(
-            env::var("BDK_RPC_USER").unwrap(),
-            env::var("BDK_RPC_PASS").unwrap(),
-        ),
-        _ => Auth::CookieFile(PathBuf::from(
-            env::var("BDK_RPC_COOKIEFILE")
-                .unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
-        )),
-    }
-}
-
-pub fn get_electrum_url() -> String {
-    env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
-}
-
-pub struct TestClient {
-    client: RpcClient,
-    electrum: ElectrumClient,
-}
-
-#[derive(Clone, Debug)]
-pub struct TestIncomingOutput {
-    pub value: u64,
-    pub to_address: String,
-}
-
-impl TestIncomingOutput {
-    pub fn new(value: u64, to_address: Address) -> Self {
-        Self {
-            value,
-            to_address: to_address.to_string(),
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct TestIncomingTx {
-    pub output: Vec<TestIncomingOutput>,
-    pub min_confirmations: Option<u64>,
-    pub locktime: Option<i64>,
-    pub replaceable: Option<bool>,
-}
-
-impl TestIncomingTx {
-    pub fn new(
-        output: Vec<TestIncomingOutput>,
-        min_confirmations: Option<u64>,
-        locktime: Option<i64>,
-        replaceable: Option<bool>,
-    ) -> Self {
-        Self {
-            output,
-            min_confirmations,
-            locktime,
-            replaceable,
-        }
-    }
-
-    pub fn add_output(&mut self, output: TestIncomingOutput) {
-        self.output.push(output);
-    }
-}
-
-#[doc(hidden)]
-pub trait TranslateDescriptor {
-    // derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
-    fn derive_translated<C: Verification>(
-        &self,
-        secp: &Secp256k1<C>,
-        index: u32,
-    ) -> Descriptor<PublicKey>;
-}
-
-impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
-    fn derive_translated<C: Verification>(
-        &self,
-        secp: &Secp256k1<C>,
-        index: u32,
-    ) -> Descriptor<PublicKey> {
-        let translate = |key: &DescriptorPublicKey| -> PublicKey {
-            match key {
-                DescriptorPublicKey::XPub(xpub) => {
-                    xpub.xkey
-                        .derive_pub(secp, &xpub.derivation_path)
-                        .expect("hardened derivation steps")
-                        .public_key
-                }
-                DescriptorPublicKey::SinglePub(key) => key.key,
-            }
-        };
-
-        self.derive(index)
-            .translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
-    }
-}
-
-#[macro_export]
-macro_rules! testutils {
-    ( @external $descriptors:expr, $child:expr ) => ({
-        use bitcoin::secp256k1::Secp256k1;
-        use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
-
-        use $crate::TranslateDescriptor;
-
-        let secp = Secp256k1::new();
-
-        let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
-        parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
-    });
-    ( @internal $descriptors:expr, $child:expr ) => ({
-        use bitcoin::secp256k1::Secp256k1;
-        use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
-
-        use $crate::TranslateDescriptor;
-
-        let secp = Secp256k1::new();
-
-        let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
-        parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
-    });
-    ( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
-    ( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
-
-    ( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({
-        let mut outs = Vec::new();
-        $( outs.push(testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+
-
-        let mut locktime = None::<i64>;
-        $( locktime = Some($locktime); )*
-
-        let mut min_confirmations = None::<u64>;
-        $( min_confirmations = Some($confirmations); )*
-
-        let mut replaceable = None::<bool>;
-        $( replaceable = Some($replaceable); )*
-
-        testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
-    });
-
-    ( @literal $key:expr ) => ({
-        let key = $key.to_string();
-        (key, None::<String>, None::<String>)
-    });
-    ( @generate_xprv $( $external_path:expr )* $( ,$internal_path:expr )* ) => ({
-        use rand::Rng;
-
-        let mut seed = [0u8; 32];
-        rand::thread_rng().fill(&mut seed[..]);
-
-        let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
-            bitcoin::Network::Testnet,
-            &seed,
-        );
-
-        let mut external_path = None::<String>;
-        $( external_path = Some($external_path.to_string()); )*
-
-        let mut internal_path = None::<String>;
-        $( internal_path = Some($internal_path.to_string()); )*
-
-        (key.unwrap().to_string(), external_path, internal_path)
-    });
-    ( @generate_wif ) => ({
-        use rand::Rng;
-
-        let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
-        rand::thread_rng().fill(&mut key[..]);
-
-        (bitcoin::PrivateKey {
-            compressed: true,
-            network: bitcoin::Network::Testnet,
-            key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
-        }.to_string(), None::<String>, None::<String>)
-    });
-
-    ( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
-        let mut map = std::collections::HashMap::new();
-        $(
-            let alias: &str = $alias;
-            map.insert(alias, testutils!( $($key_type)* ));
-        )+
-
-        map
-    });
-
-    ( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({
-        use std::str::FromStr;
-        use std::collections::HashMap;
-        use std::convert::TryInto;
-
-        use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
-        use miniscript::TranslatePk;
-
-        let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
-        $(
-            keys = testutils!{ @keys $( $keys )* };
-        )*
-
-        let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
-        let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
-            if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
-                format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
-            } else {
-                k.clone()
-            }
-        }, |kh| {
-            if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
-                format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
-            } else {
-                kh.clone()
-            }
-
-        });
-        let external = external.to_string();
-
-        let mut internal = None::<String>;
-        $(
-            let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
-
-            let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
-                if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
-                    format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
-                } else {
-                    k.clone()
-                }
-            }, |kh| {
-                if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
-                    format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
-                } else {
-                    kh.clone()
-                }
-            });
-            internal = Some(string_internal.to_string());
-        )*
-
-        (external, internal)
-    })
-}
-
-fn exponential_backoff_poll<T, F>(mut poll: F) -> T
-where
-    F: FnMut() -> Option<T>,
-{
-    let mut delay = Duration::from_millis(64);
-    loop {
-        match poll() {
-            Some(data) => break data,
-            None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
-            None => {}
-        }
-
-        std::thread::sleep(delay);
-    }
-}
-
-impl TestClient {
-    pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self {
-        let client = RpcClient::new(
-            format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name),
-            get_auth(),
-        )
-        .unwrap();
-        let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
-
-        TestClient { client, electrum }
-    }
-
-    fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
-        // wait for electrs to index the tx
-        exponential_backoff_poll(|| {
-            trace!("wait_for_tx {}", txid);
-
-            self.electrum
-                .script_get_history(monitor_script)
-                .unwrap()
-                .iter()
-                .position(|entry| entry.tx_hash == txid)
-        });
-    }
-
-    fn wait_for_block(&mut self, min_height: usize) {
-        self.electrum.block_headers_subscribe().unwrap();
-
-        loop {
-            let header = exponential_backoff_poll(|| {
-                self.electrum.ping().unwrap();
-                self.electrum.block_headers_pop().unwrap()
-            });
-            if header.height >= min_height {
-                break;
-            }
-        }
-    }
-
-    pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
-        assert!(
-            !meta_tx.output.is_empty(),
-            "can't create a transaction with no outputs"
-        );
-
-        let mut map = HashMap::new();
-
-        let mut required_balance = 0;
-        for out in &meta_tx.output {
-            required_balance += out.value;
-            map.insert(out.to_address.clone(), Amount::from_sat(out.value));
-        }
-
-        if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
-            panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
-        }
-
-        // FIXME: core can't create a tx with two outputs to the same address
-        let tx = self
-            .create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
-            .unwrap();
-        let tx = self.fund_raw_transaction(tx, None, None).unwrap();
-        let mut tx: Transaction = deserialize(&tx.hex).unwrap();
-
-        if let Some(true) = meta_tx.replaceable {
-            // for some reason core doesn't set this field right
-            for input in &mut tx.input {
-                input.sequence = 0xFFFFFFFD;
-            }
-        }
-
-        let tx = self
-            .sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
-            .unwrap();
-
-        // broadcast through electrum so that it caches the tx immediately
-        let txid = self
-            .electrum
-            .transaction_broadcast(&deserialize(&tx.hex).unwrap())
-            .unwrap();
-
-        if let Some(num) = meta_tx.min_confirmations {
-            self.generate(num, None);
-        }
-
-        let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
-            .unwrap()
-            .script_pubkey();
-        self.wait_for_tx(txid, &monitor_script);
-
-        debug!("Sent tx: {}", txid);
-
-        txid
-    }
-
-    pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
-        let tx = self.get_raw_transaction_info(txid, None).unwrap();
-        assert!(
-            tx.confirmations.is_none(),
-            "Can't bump tx {} because it's already confirmed",
-            txid
-        );
-
-        let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
-        let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
-
-        let monitor_script =
-            tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
-        self.wait_for_tx(new_txid, &monitor_script);
-
-        debug!("Bumped {}, new txid {}", txid, new_txid);
-
-        new_txid
-    }
-
-    pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
-        use bitcoin::blockdata::block::{Block, BlockHeader};
-        use bitcoin::blockdata::script::Builder;
-        use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
-        use bitcoin::hash_types::{BlockHash, TxMerkleNode};
-
-        let block_template: serde_json::Value = self
-            .call("getblocktemplate", &[json!({"rules": ["segwit"]})])
-            .unwrap();
-        trace!("getblocktemplate: {:#?}", block_template);
-
-        let header = BlockHeader {
-            version: block_template["version"].as_i64().unwrap() as i32,
-            prev_blockhash: BlockHash::from_hex(
-                block_template["previousblockhash"].as_str().unwrap(),
-            )
-            .unwrap(),
-            merkle_root: TxMerkleNode::default(),
-            time: block_template["curtime"].as_u64().unwrap() as u32,
-            bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
-            nonce: 0,
-        };
-        debug!("header: {:#?}", header);
-
-        let height = block_template["height"].as_u64().unwrap() as i64;
-        let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
-        // burn block subsidy and fees, not a big deal
-        let mut coinbase_tx = Transaction {
-            version: 1,
-            lock_time: 0,
-            input: vec![TxIn {
-                previous_output: OutPoint::null(),
-                script_sig: Builder::new().push_int(height).into_script(),
-                sequence: 0xFFFFFFFF,
-                witness: vec![witness_reserved_value],
-            }],
-            output: vec![],
-        };
-
-        let mut txdata = vec![coinbase_tx.clone()];
-        txdata.extend_from_slice(&txs);
-
-        let mut block = Block { header, txdata };
-
-        let witness_root = block.witness_root();
-        let witness_commitment =
-            Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
-
-        // now update and replace the coinbase tx
-        let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
-        coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
-
-        coinbase_tx.output.push(TxOut {
-            value: 0,
-            script_pubkey: coinbase_witness_commitment_script.into(),
-        });
-        block.txdata[0] = coinbase_tx;
-
-        // set merkle root
-        let merkle_root = block.merkle_root();
-        block.header.merkle_root = merkle_root;
-
-        assert!(block.check_merkle_root());
-        assert!(block.check_witness_commitment());
-
-        // now do PoW :)
-        let target = block.header.target();
-        while block.header.validate_pow(&target).is_err() {
-            block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
-        }
-
-        let block_hex: String = serialize(&block).to_hex();
-        debug!("generated block hex: {}", block_hex);
-
-        self.electrum.block_headers_subscribe().unwrap();
-
-        let submit_result: serde_json::Value =
-            self.call("submitblock", &[block_hex.into()]).unwrap();
-        debug!("submitblock: {:?}", submit_result);
-        assert!(
-            submit_result.is_null(),
-            "submitblock error: {:?}",
-            submit_result.as_str()
-        );
-
-        self.wait_for_block(height as usize);
-
-        block.header.block_hash().to_hex()
-    }
-
-    pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
-        let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
-        let hashes = self.generate_to_address(num_blocks, &address).unwrap();
-        let best_hash = hashes.last().unwrap();
-        let height = self.get_block_info(best_hash).unwrap().height;
-
-        self.wait_for_block(height);
-
-        debug!("Generated blocks to new height {}", height);
-    }
-
-    pub fn invalidate(&mut self, num_blocks: u64) {
-        self.electrum.block_headers_subscribe().unwrap();
-
-        let best_hash = self.get_best_block_hash().unwrap();
-        let initial_height = self.get_block_info(&best_hash).unwrap().height;
-
-        let mut to_invalidate = best_hash;
-        for i in 1..=num_blocks {
-            trace!(
-                "Invalidating block {}/{} ({})",
-                i,
-                num_blocks,
-                to_invalidate
-            );
-
-            self.invalidate_block(&to_invalidate).unwrap();
-            to_invalidate = self.get_best_block_hash().unwrap();
-        }
-
-        self.wait_for_block(initial_height - num_blocks as usize);
-
-        debug!(
-            "Invalidated {} blocks to new height of {}",
-            num_blocks,
-            initial_height - num_blocks as usize
-        );
-    }
-
-    pub fn reorg(&mut self, num_blocks: u64) {
-        self.invalidate(num_blocks);
-        self.generate(num_blocks, None);
-    }
-
-    pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
-        Address::from_str(
-            &self
-                .get_new_address(None, address_type)
-                .unwrap()
-                .to_string(),
-        )
-        .unwrap()
-    }
-}
-
-impl Deref for TestClient {
-    type Target = RpcClient;
-
-    fn deref(&self) -> &Self::Target {
-        &self.client
-    }
-}
-
-impl Default for TestClient {
-    fn default() -> Self {
-        let rpc_host_and_port =
-            env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
-        let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
-        Self::new(rpc_host_and_port, wallet)
-    }
-}