--- /dev/null
+use anyhow::Result;
+use bdk_chain::{
+ bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
+ keychain::Balance,
+ local_chain::LocalChain,
+ ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
+};
+use bdk_electrum::{ElectrumExt, ElectrumUpdate};
+use bdk_testenv::TestEnv;
+use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
+
+fn get_balance(
+ recv_chain: &LocalChain,
+ recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
+) -> Result<Balance> {
+ let chain_tip = recv_chain.tip().block_id();
+ let outpoints = recv_graph.index.outpoints().clone();
+ let balance = recv_graph
+ .graph()
+ .balance(recv_chain, chain_tip, outpoints, |_, _| true);
+ Ok(balance)
+}
+
+/// Ensure that [`ElectrumExt`] can sync properly.
+///
+/// 1. Mine 101 blocks.
+/// 2. Send a tx.
+/// 3. Mine extra block to confirm sent tx.
+/// 4. Check [`Balance`] to ensure tx is confirmed.
+#[test]
+fn scan_detects_confirmed_tx() -> Result<()> {
+ const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
+
+ let env = TestEnv::new()?;
+ let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
+
+ // Setup addresses.
+ let addr_to_mine = env
+ .bitcoind
+ .client
+ .get_new_address(None, None)?
+ .assume_checked();
+ let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
+ let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
+
+ // Setup receiver.
+ let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+ let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
+ let mut recv_index = SpkTxOutIndex::default();
+ recv_index.insert_spk((), spk_to_track.clone());
+ recv_index
+ });
+
+ // Mine some blocks.
+ env.mine_blocks(101, Some(addr_to_mine))?;
+
+ // Create transaction that is tracked by our receiver.
+ env.send(&addr_to_track, SEND_AMOUNT)?;
+
+ // Mine a block to confirm sent tx.
+ env.mine_blocks(1, None)?;
+
+ // Sync up to tip.
+ env.wait_until_electrum_sees_block()?;
+ let ElectrumUpdate {
+ chain_update,
+ relevant_txids,
+ } = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?;
+
+ let missing = relevant_txids.missing_full_txs(recv_graph.graph());
+ let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
+ let _ = recv_chain
+ .apply_update(chain_update)
+ .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
+ let _ = recv_graph.apply_update(graph_update);
+
+ // Check to see if tx is confirmed.
+ assert_eq!(
+ get_balance(&recv_chain, &recv_graph)?,
+ Balance {
+ confirmed: SEND_AMOUNT.to_sat(),
+ ..Balance::default()
+ },
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_reorg_is_detected_in_electrsd() -> Result<()> {
+ let env = TestEnv::new()?;
+
+ // Mine some blocks.
+ env.mine_blocks(101, None)?;
+ env.wait_until_electrum_sees_block()?;
+ let height = env.bitcoind.client.get_block_count()?;
+ let blocks = (0..=height)
+ .map(|i| env.bitcoind.client.get_block_hash(i))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ // Perform reorg on six blocks.
+ env.reorg(6)?;
+ env.wait_until_electrum_sees_block()?;
+ let reorged_height = env.bitcoind.client.get_block_count()?;
+ let reorged_blocks = (0..=height)
+ .map(|i| env.bitcoind.client.get_block_hash(i))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ assert_eq!(height, reorged_height);
+
+ // Block hashes should not be equal on the six reorged blocks.
+ for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
+ match i <= height as usize - 6 {
+ true => assert_eq!(block, reorged_block),
+ false => assert_ne!(block, reorged_block),
+ }
+ }
+
+ Ok(())
+}
+
+/// Ensure that confirmed txs that are reorged become unconfirmed.
+///
+/// 1. Mine 101 blocks.
+/// 2. Mine 8 blocks with a confirmed tx in each.
+/// 3. Perform 8 separate reorgs on each block with a confirmed tx.
+/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct.
+#[test]
+fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
+ const REORG_COUNT: usize = 8;
+ const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
+
+ let env = TestEnv::new()?;
+ let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
+
+ // Setup addresses.
+ let addr_to_mine = env
+ .bitcoind
+ .client
+ .get_new_address(None, None)?
+ .assume_checked();
+ let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
+ let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
+
+ // Setup receiver.
+ let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+ let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
+ let mut recv_index = SpkTxOutIndex::default();
+ recv_index.insert_spk((), spk_to_track.clone());
+ recv_index
+ });
+
+ // Mine some blocks.
+ env.mine_blocks(101, Some(addr_to_mine))?;
+
+ // Create transactions that are tracked by our receiver.
+ for _ in 0..REORG_COUNT {
+ env.send(&addr_to_track, SEND_AMOUNT)?;
+ env.mine_blocks(1, None)?;
+ }
+
+ // Sync up to tip.
+ env.wait_until_electrum_sees_block()?;
+ let ElectrumUpdate {
+ chain_update,
+ relevant_txids,
+ } = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
+
+ let missing = relevant_txids.missing_full_txs(recv_graph.graph());
+ let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
+ let _ = recv_chain
+ .apply_update(chain_update)
+ .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
+ let _ = recv_graph.apply_update(graph_update.clone());
+
+ // Retain a snapshot of all anchors before reorg process.
+ let initial_anchors = graph_update.all_anchors();
+
+ // Check if initial balance is correct.
+ assert_eq!(
+ get_balance(&recv_chain, &recv_graph)?,
+ Balance {
+ confirmed: SEND_AMOUNT.to_sat() * REORG_COUNT as u64,
+ ..Balance::default()
+ },
+ "initial balance must be correct",
+ );
+
+ // Perform reorgs with different depths.
+ for depth in 1..=REORG_COUNT {
+ env.reorg_empty_blocks(depth)?;
+
+ env.wait_until_electrum_sees_block()?;
+ let ElectrumUpdate {
+ chain_update,
+ relevant_txids,
+ } = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
+
+ let missing = relevant_txids.missing_full_txs(recv_graph.graph());
+ let graph_update =
+ relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
+ let _ = recv_chain
+ .apply_update(chain_update)
+ .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
+
+ // Check to see if a new anchor is added during current reorg.
+ if !initial_anchors.is_superset(graph_update.all_anchors()) {
+ println!("New anchor added at reorg depth {}", depth);
+ }
+ let _ = recv_graph.apply_update(graph_update);
+
+ assert_eq!(
+ get_balance(&recv_chain, &recv_graph)?,
+ Balance {
+ confirmed: SEND_AMOUNT.to_sat() * (REORG_COUNT - depth) as u64,
+ trusted_pending: SEND_AMOUNT.to_sat() * depth as u64,
+ ..Balance::default()
+ },
+ "reorg_count: {}",
+ depth,
+ );
+ }
+
+ Ok(())
+}