From e761adf48178e2688a817a2b7466b0ebf7902eeb Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Thu, 4 Jul 2024 18:28:01 +0800 Subject: [PATCH] test(electrum): Imported `bdk_esplora` tests into `bdk_electrum` --- crates/electrum/tests/test_electrum.rs | 222 ++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 2 deletions(-) diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 90d9f30bb..4d98b5150 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -1,11 +1,13 @@ use bdk_chain::{ - bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, + bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, local_chain::LocalChain, - spk_client::SyncRequest, + spk_client::{FullScanRequest, SyncRequest}, Balance, ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; +use std::collections::{BTreeSet, HashSet}; +use std::str::FromStr; fn get_balance( recv_chain: &LocalChain, @@ -19,6 +21,222 @@ fn get_balance( Ok(balance) } +#[test] +pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { + let env = TestEnv::new()?; + let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + let client = BdkElectrumClient::new(electrum_client); + + let receive_address0 = + Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked(); + let receive_address1 = + Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked(); + + let misc_spks = [ + receive_address0.script_pubkey(), + receive_address1.script_pubkey(), + ]; + + let _block_hashes = env.mine_blocks(101, None)?; + let txid1 = env.bitcoind.client.send_to_address( + &receive_address1, + Amount::from_sat(10000), + None, + None, + None, + None, + Some(1), + None, + )?; + let txid2 = env.bitcoind.client.send_to_address( + &receive_address0, + Amount::from_sat(20000), + None, + None, + None, + None, + Some(1), + None, + )?; + env.mine_blocks(1, None)?; + env.wait_until_electrum_sees_block()?; + + // use a full checkpoint linked list (since this is not what we are testing) + let cp_tip = env.make_checkpoint_tip(); + + let sync_update = { + let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks); + client.sync(request, 1, true)? + }; + + assert!( + { + let update_cps = sync_update + .chain_update + .iter() + .map(|cp| cp.block_id()) + .collect::>(); + let superset_cps = cp_tip + .iter() + .map(|cp| cp.block_id()) + .collect::>(); + superset_cps.is_superset(&update_cps) + }, + "update should not alter original checkpoint tip since we already started with all checkpoints", + ); + + let graph_update = sync_update.graph_update; + // Check to see if we have the floating txouts available from our two created transactions' + // previous outputs in order to calculate transaction fees. + for tx in graph_update.full_txs() { + // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the + // floating txouts available from the transactions' previous outputs. + let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + + // Retrieve the fee in the transaction data from `bitcoind`. + let tx_fee = env + .bitcoind + .client + .get_transaction(&tx.txid, None) + .expect("Tx must exist") + .fee + .expect("Fee must exist") + .abs() + .to_unsigned() + .expect("valid `Amount`"); + + // Check that the calculated fee matches the fee from the transaction data. + assert_eq!(fee, tx_fee); + } + + let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); + graph_update_txids.sort(); + let mut expected_txids = vec![txid1, txid2]; + expected_txids.sort(); + assert_eq!(graph_update_txids, expected_txids); + + Ok(()) +} + +/// Test the bounds of the address scan depending on the `stop_gap`. +#[test] +pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { + let env = TestEnv::new()?; + let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + let client = BdkElectrumClient::new(electrum_client); + let _block_hashes = env.mine_blocks(101, None)?; + + // Now let's test the gap limit. First of all get a chain of 10 addresses. + let addresses = [ + "bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4", + "bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30", + "bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g", + "bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww", + "bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu", + "bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh", + "bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2", + "bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8", + "bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef", + "bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk", + ]; + let addresses: Vec<_> = addresses + .into_iter() + .map(|s| Address::from_str(s).unwrap().assume_checked()) + .collect(); + let spks: Vec<_> = addresses + .iter() + .enumerate() + .map(|(i, addr)| (i as u32, addr.script_pubkey())) + .collect(); + + // Then receive coins on the 4th address. + let txid_4th_addr = env.bitcoind.client.send_to_address( + &addresses[3], + Amount::from_sat(10000), + None, + None, + None, + None, + Some(1), + None, + )?; + env.mine_blocks(1, None)?; + env.wait_until_electrum_sees_block()?; + + // use a full checkpoint linked list (since this is not what we are testing) + let cp_tip = env.make_checkpoint_tip(); + + // A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4 + // will. + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 3, 1, false)? + }; + assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.last_active_indices.is_empty()); + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 4, 1, false)? + }; + assert_eq!( + full_scan_update + .graph_update + .full_txs() + .next() + .unwrap() + .txid, + txid_4th_addr + ); + assert_eq!(full_scan_update.last_active_indices[&0], 3); + + // Now receive a coin on the last address. + let txid_last_addr = env.bitcoind.client.send_to_address( + &addresses[addresses.len() - 1], + Amount::from_sat(10000), + None, + None, + None, + None, + Some(1), + None, + )?; + env.mine_blocks(1, None)?; + env.wait_until_electrum_sees_block()?; + + // A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will. + // The last active indice won't be updated in the first case but will in the second one. + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 5, 1, false)? + }; + let txs: HashSet<_> = full_scan_update + .graph_update + .full_txs() + .map(|tx| tx.txid) + .collect(); + assert_eq!(txs.len(), 1); + assert!(txs.contains(&txid_4th_addr)); + assert_eq!(full_scan_update.last_active_indices[&0], 3); + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 6, 1, false)? + }; + let txs: HashSet<_> = full_scan_update + .graph_update + .full_txs() + .map(|tx| tx.txid) + .collect(); + assert_eq!(txs.len(), 2); + assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); + assert_eq!(full_scan_update.last_active_indices[&0], 9); + + Ok(()) +} + /// Ensure that [`ElectrumExt`] can sync properly. /// /// 1. Mine 101 blocks.