diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 3a92f052..fe4a97e1 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -73,10 +73,19 @@ jobs: - name: Test run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests - test-electrum: - name: Test electrum + test-blockchains: + name: Test ${{ matrix.blockchain.name }} runs-on: ubuntu-16.04 - container: bitcoindevkit/electrs:0.2.0 + strategy: + matrix: + blockchain: + - name: electrum + container: bitcoindevkit/electrs + start: /root/electrs --network regtest --jsonrpc-import + - name: esplora + container: bitcoindevkit/esplora + start: /root/electrs --network regtest -vvv --cookie admin:passw --jsonrpc-import --electrum-rpc-addr=0.0.0.0:60401 --http-addr 0.0.0.0:3002 + container: ${{ matrix.blockchain.container }} env: BDK_RPC_AUTH: USER_PASS BDK_RPC_USER: admin @@ -84,6 +93,7 @@ jobs: BDK_RPC_URL: 127.0.0.1:18443 BDK_RPC_WALLET: bdk-test BDK_ELECTRUM_URL: tcp://127.0.0.1:60401 + BDK_ESPLORA_URL: http://127.0.0.1:3002 steps: - name: Checkout uses: actions/checkout@v2 @@ -95,6 +105,8 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + - name: get pkg-config # running eslpora tests seems to need this + run: apt update && apt install -y --fix-missing pkg-config libssl-dev - name: Install rustup run: curl https://sh.rustup.rs -sSf | sh -s -- -y - name: Set default toolchain @@ -105,8 +117,10 @@ jobs: run: $HOME/.cargo/bin/rustup update - name: Start core run: ./ci/start-core.sh + - name: start ${{ matrix.blockchain.name }} + run: nohup ${{ matrix.blockchain.start }} & sleep 5 - name: Test - run: $HOME/.cargo/bin/cargo test --features test-electrum --no-default-features + 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 diff --git a/Cargo.toml b/Cargo.toml index 687e016c..78ddb116 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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,18 +58,15 @@ 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-blockchains = ["bitcoincore-rpc", "electrum-client"] test-md-docs = ["electrum"] [dev-dependencies] -bdk-testutils = "0.4" -bdk-testutils-macros = "0.6" -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" @@ -79,10 +80,7 @@ path = "examples/compiler.rs" required-features = ["compiler"] [workspace] -members = ["macros", "testutils", "testutils-macros"] - -# 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` diff --git a/ci/start-core.sh b/ci/start-core.sh index 59c024e1..455442b4 100755 --- a/ci/start-core.sh +++ b/ci/start-core.sh @@ -11,7 +11,3 @@ done echo "Generating 150 bitcoin blocks." ADDR=$(/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcwallet=$BDK_RPC_WALLET getnewaddress) /root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS generatetoaddress 150 $ADDR - -echo "Starting electrs node." -nohup /root/electrs --network regtest --jsonrpc-import & -sleep 5 diff --git a/run_blockchain_tests.sh b/run_blockchain_tests.sh new file mode 100755 index 00000000..ce87f26f --- /dev/null +++ b/run_blockchain_tests.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +usage() { + cat <<'EOF' +Script for running the bdk blockchain tests for a specific blockchain by starting up the backend in docker. + +Usage: ./run_blockchain_tests.sh [esplora|electrum] [test name]. + +EOF +} + +eprintln(){ + echo "$@" >&2 +} + +cleanup() { + if test "$id"; then + eprintln "cleaning up $blockchain docker container $id"; + docker rm -fv "$id" > /dev/null; + fi + trap - EXIT INT +} + +# Makes sure we clean up the container at the end or if ^C +trap 'rc=$?; cleanup; exit $rc' EXIT INT + +blockchain="$1" +test_name="$2" + +case "$blockchain" in + electrum) + eprintln "starting electrs docker container" + id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs)" + ;; + esplora) + eprintln "starting esplora docker container" + id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp -p 127.0.0.1:3002:3002/tcp bitcoindevkit/esplora)" + export BDK_ESPLORA_URL=http://127.0.0.1:3002 + ;; + *) + usage; + exit 1; + ;; + esac + +# taken from https://github.com/bitcoindevkit/bitcoin-regtest-box +export BDK_RPC_AUTH=USER_PASS +export BDK_RPC_USER=admin +export BDK_RPC_PASS=passw +export BDK_RPC_URL=127.0.0.1:18443 +export BDK_RPC_WALLET=bdk-test +export BDK_ELECTRUM_URL=tcp://127.0.0.1:60401 + +cli(){ + docker exec -it "$id" /root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw $@ +} + +eprintln "running getwalletinfo until bitcoind seems to be alive" +while ! cli getwalletinfo >/dev/null; do sleep 1; done + +# sleep again for good measure! +sleep 1; + +cargo test --features "test-blockchains,$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests::$test_name" diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index 926155a3..4d8926af 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,10 @@ impl ConfigurableBlockchain for ElectrumBlockchain { )?)) } } + +#[cfg(feature = "test-blockchains")] +crate::bdk_blockchain_tests! { + fn test_instance() -> ElectrumBlockchain { + ElectrumBlockchain::from(Client::new(&testutils::blockchain_tests::get_electrum_url()).unwrap()) + } +} diff --git a/src/blockchain/esplora.rs b/src/blockchain/esplora.rs index 793da96b..ff85f22f 100644 --- a/src/blockchain/esplora.rs +++ b/src/blockchain/esplora.rs @@ -414,3 +414,10 @@ impl_error!(reqwest::Error, Reqwest, EsploraError); impl_error!(std::num::ParseIntError, Parsing, EsploraError); impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError); impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError); + +#[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) + } +} diff --git a/src/database/memory.rs b/src/database/memory.rs index 465698d5..adf4e20f 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -511,7 +511,7 @@ macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::Network; use $crate::database::MemoryDatabase; - use testutils::testutils; + use $crate::testutils; let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; let descriptors = testutils!(@descriptors (descriptor) (descriptor)); diff --git a/src/lib.rs b/src/lib.rs index 0e7f8287..77cbef61 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,16 +230,10 @@ pub extern crate sled; #[allow(unused_imports)] #[cfg(test)] -#[macro_use] -extern crate testutils; #[allow(unused_imports)] #[cfg(test)] #[macro_use] -extern crate testutils_macros; -#[allow(unused_imports)] -#[cfg(test)] -#[macro_use] -extern crate serial_test; +pub extern crate serial_test; #[macro_use] pub(crate) mod error; @@ -267,3 +261,10 @@ pub use wallet::Wallet; pub fn version() -> &'static str { env!("CARGO_PKG_VERSION", "unknown") } + +// We should consider putting this under a feature flag but we need the macro in doctets so we need +// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed. +// +// Stuff in here is too rough to document atm +#[doc(hidden)] +pub mod testutils; diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs new file mode 100644 index 00000000..30999990 --- /dev/null +++ b/src/testutils/blockchain_tests.rs @@ -0,0 +1,830 @@ +use crate::testutils::TestIncomingTx; +use bitcoin::consensus::encode::{deserialize, serialize}; +use bitcoin::hashes::hex::{FromHex, ToHex}; +use bitcoin::hashes::sha256d; +use bitcoin::{Address, Amount, Script, Transaction, Txid}; +pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; +pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi}; +use core::str::FromStr; +pub use electrum_client::{Client as ElectrumClient, ElectrumApi}; +#[allow(unused_imports)] +use log::{debug, error, info, trace}; +use std::collections::HashMap; +use std::env; +use std::ops::Deref; +use std::path::PathBuf; +use std::time::Duration; + +pub struct TestClient { + client: RpcClient, + electrum: ElectrumClient, +} + +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) -> 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 = 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
) { + 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) -> Address { + Address::from_str( + &self + .get_new_address(None, address_type) + .unwrap() + .to_string(), + ) + .unwrap() + } +} + +pub fn get_electrum_url() -> String { + env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string()) +} + +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) + } +} + +fn exponential_backoff_poll(mut poll: F) -> T +where + F: FnMut() -> Option, +{ + 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); + } +} + +// 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()), + )), + } +} + +/// 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::blockchain_tests::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)) -> 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 _ = env_logger::try_init(); + + 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::>(); + + 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 index 00000000..5d7146bd --- /dev/null +++ b/src/testutils/mod.rs @@ -0,0 +1,230 @@ +// 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. +#![allow(missing_docs)] + +#[cfg(feature = "test-blockchains")] +pub mod blockchain_tests; + +use bitcoin::secp256k1::{Secp256k1, Verification}; +use bitcoin::{Address, PublicKey}; + +use miniscript::descriptor::DescriptorPublicKey; +use miniscript::{Descriptor, MiniscriptKey, TranslatePk}; + +#[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, + pub min_confirmations: Option, + pub locktime: Option, + pub replaceable: Option, +} + +impl TestIncomingTx { + pub fn new( + output: Vec, + min_confirmations: Option, + locktime: Option, + replaceable: Option, + ) -> 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` into a `Descriptor` + fn derive_translated( + &self, + secp: &Secp256k1, + index: u32, + ) -> Descriptor; +} + +impl TranslateDescriptor for Descriptor { + fn derive_translated( + &self, + secp: &Secp256k1, + index: u32, + ) -> Descriptor { + 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::::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::::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 outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+]; + + let locktime = None::$(.or(Some($locktime)))?; + + let min_confirmations = None::$(.or(Some($confirmations)))?; + let replaceable = None::$(.or(Some($replaceable)))?; + + $crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable) + }); + + ( @literal $key:expr ) => ({ + let key = $key.to_string(); + (key, None::, None::) + }); + ( @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 external_path = None::$(.or(Some($external_path.to_string())))?; + let internal_path = None::$(.or(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::, None::) + }); + + ( @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, Option)> = HashMap::new(); + $( + keys = testutils!{ @keys $( $keys )* }; + )* + + let external: Descriptor = FromStr::from_str($external_descriptor).unwrap(); + let external: Descriptor = 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 internal = None::$(.or({ + let string_internal: Descriptor = FromStr::from_str($internal_descriptor).unwrap(); + + let string_internal: Descriptor = 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() + } + }); + Some(string_internal.to_string()) + }))?; + + (external, internal) + }) +} diff --git a/src/wallet/address_validator.rs b/src/wallet/address_validator.rs index 4e20ef5e..36e39be1 100644 --- a/src/wallet/address_validator.rs +++ b/src/wallet/address_validator.rs @@ -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(); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 11b58849..32d51081 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1514,6 +1514,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-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/.gitignore b/testutils/.gitignore deleted file mode 100644 index 2c96eb1b..00000000 --- a/testutils/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target/ -Cargo.lock diff --git a/testutils/Cargo.toml b/testutils/Cargo.toml deleted file mode 100644 index 8f7f2618..00000000 --- a/testutils/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "bdk-testutils" -version = "0.4.0" -authors = ["Alekos Filini "] -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/lib.rs b/testutils/src/lib.rs deleted file mode 100644 index 29e43a41..00000000 --- a/testutils/src/lib.rs +++ /dev/null @@ -1,564 +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 serde_json; - -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, - pub min_confirmations: Option, - pub locktime: Option, - pub replaceable: Option, -} - -impl TestIncomingTx { - pub fn new( - output: Vec, - min_confirmations: Option, - locktime: Option, - replaceable: Option, - ) -> 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` into a `Descriptor` - fn derive_translated( - &self, - secp: &Secp256k1, - index: u32, - ) -> Descriptor; -} - -impl TranslateDescriptor for Descriptor { - fn derive_translated( - &self, - secp: &Secp256k1, - index: u32, - ) -> Descriptor { - 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::::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::::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::; - $( locktime = Some($locktime); )* - - let mut min_confirmations = None::; - $( min_confirmations = Some($confirmations); )* - - let mut replaceable = None::; - $( replaceable = Some($replaceable); )* - - testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable) - }); - - ( @literal $key:expr ) => ({ - let key = $key.to_string(); - (key, None::, None::) - }); - ( @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::; - $( external_path = Some($external_path.to_string()); )* - - let mut internal_path = None::; - $( 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::, None::) - }); - - ( @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, Option)> = HashMap::new(); - $( - keys = testutils!{ @keys $( $keys )* }; - )* - - let external: Descriptor = FromStr::from_str($external_descriptor).unwrap(); - let external: Descriptor = 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::; - $( - let string_internal: Descriptor = FromStr::from_str($internal_descriptor).unwrap(); - - let string_internal: Descriptor = 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(mut poll: F) -> T -where - F: FnMut() -> Option, -{ - 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() -> 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(); - 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) -> 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 = 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
) { - 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) -> 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 - } -}