diff --git a/Cargo.toml b/Cargo.toml index 687e016c..52176e5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,13 +54,11 @@ all-keys = ["keys-bip39"] keys-bip39 = ["tiny-bip39"] # Debug/Test features -debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"] test-electrum = ["electrum"] test-md-docs = ["electrum"] [dev-dependencies] -bdk-testutils = "0.4" -bdk-testutils-macros = "0.6" +bdk-testutils = { path = "./testutils" } serial_test = "0.4" lazy_static = "1.4" env_logger = "0.7" @@ -79,7 +77,7 @@ path = "examples/compiler.rs" required-features = ["compiler"] [workspace] -members = ["macros", "testutils", "testutils-macros"] +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 diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index 926155a3..2e103307 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -45,13 +45,6 @@ use crate::FeeRate; /// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example. pub struct ElectrumBlockchain(Client); -#[cfg(test)] -#[cfg(feature = "test-electrum")] -#[bdk_blockchain_tests(crate)] -fn local_electrs() -> ElectrumBlockchain { - ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap()) -} - impl std::convert::From for ElectrumBlockchain { fn from(client: Client) -> Self { ElectrumBlockchain(client) @@ -175,3 +168,11 @@ impl ConfigurableBlockchain for ElectrumBlockchain { )?)) } } + +#[cfg(all(feature = "test-electrum", test))] +testutils::bdk_blockchain_tests! { + bdk => crate, + fn test_instance() -> ElectrumBlockchain { + ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0e7f8287..a4670b5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -234,8 +234,6 @@ pub extern crate sled; extern crate testutils; #[allow(unused_imports)] #[cfg(test)] -#[macro_use] -extern crate testutils_macros; #[allow(unused_imports)] #[cfg(test)] #[macro_use] diff --git a/testutils-macros/Cargo.toml b/testutils-macros/Cargo.toml deleted file mode 100644 index f78b7f9d..00000000 --- a/testutils-macros/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "bdk-testutils-macros" -version = "0.6.0" -authors = ["Alekos Filini "] -edition = "2018" -homepage = "https://bitcoindevkit.org" -repository = "https://github.com/bitcoindevkit/bdk" -documentation = "https://docs.rs/bdk-testutils-macros" -description = "Supporting testing macros for `bdk`" -keywords = ["bdk"] -license = "MIT OR Apache-2.0" - -[lib] -proc-macro = true -name = "testutils_macros" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -syn = { version = "1.0", features = ["parsing", "full"] } -proc-macro2 = "1.0" -quote = "1.0" - -[features] -debug = ["syn/extra-traits"] diff --git a/testutils-macros/src/lib.rs b/testutils-macros/src/lib.rs deleted file mode 100644 index db01e1ed..00000000 --- a/testutils-macros/src/lib.rs +++ /dev/null @@ -1,553 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -#[macro_use] -extern crate quote; - -use proc_macro::TokenStream; - -use syn::spanned::Spanned; -use syn::{parse, parse2, Ident, ReturnType}; - -#[proc_macro_attribute] -pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream { - let root_ident = if !attr.is_empty() { - match parse::(attr) { - Ok(parsed) => parsed, - Err(e) => { - let error_string = e.to_string(); - return (quote! { - compile_error!("Invalid crate path: {:?}", #error_string) - }) - .into(); - } - } - } else { - parse2::(quote! { bdk }).unwrap() - }; - - match parse::(item) { - Err(_) => (quote! { - compile_error!("#[bdk_blockchain_tests] can only be used on `fn`s") - }) - .into(), - Ok(parsed) => { - let parsed_sig_ident = parsed.sig.ident.clone(); - let mod_name = Ident::new( - &format!("generated_tests_{}", parsed_sig_ident.to_string()), - parsed.span(), - ); - - let return_type = match parsed.sig.output { - ReturnType::Type(_, ref t) => t.clone(), - ReturnType::Default => { - return (quote! { - compile_error!("The tagged function must return a type that impl `Blockchain`") - }).into(); - } - }; - - let output = quote! { - - #parsed - - mod #mod_name { - use bitcoin::Network; - - use miniscript::Descriptor; - - use testutils::{TestClient, serial}; - - use #root_ident::blockchain::{Blockchain, noop_progress}; - use #root_ident::descriptor::ExtendedDescriptor; - use #root_ident::database::MemoryDatabase; - use #root_ident::types::KeychainKind; - use #root_ident::{Wallet, TxBuilder, FeeRate}; - use #root_ident::wallet::AddressIndex::New; - - use super::*; - - fn get_blockchain() -> #return_type { - #parsed_sig_ident() - } - - fn get_wallet_from_descriptors(descriptors: &(String, Option)) -> Wallet<#return_type, MemoryDatabase> { - Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap() - } - - fn init_single_sig() -> (Wallet<#return_type, MemoryDatabase>, (String, Option), 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::new(); - 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); - } - - #[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::>(); - - 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); - } - } - - }; - - output.into() - } - } -} diff --git a/testutils/src/blockchain_tests.rs b/testutils/src/blockchain_tests.rs new file mode 100644 index 00000000..95ebcc4b --- /dev/null +++ b/testutils/src/blockchain_tests.rs @@ -0,0 +1,491 @@ +/// 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)) -> 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), 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); + } + + #[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::>(); + + 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 index 29e43a41..333af54f 100644 --- a/testutils/src/lib.rs +++ b/testutils/src/lib.rs @@ -11,6 +11,7 @@ #[macro_use] extern crate serde_json; +mod blockchain_tests; pub use serial_test::serial; @@ -297,11 +298,12 @@ where } impl TestClient { - pub fn new() -> Self { - let url = 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()); - let client = - RpcClient::new(format!("http://{}/wallet/{}", url, wallet), get_auth()).unwrap(); + 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 } @@ -562,3 +564,12 @@ impl Deref for TestClient { &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) + } +}