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,
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::<BTreeSet<_>>();
+ let superset_cps = cp_tip
+ .iter()
+ .map(|cp| cp.block_id())
+ .collect::<BTreeSet<_>>();
+ 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<Txid> = 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.