"crates/esplora",
"crates/bitcoind_rpc",
"crates/hwi",
+ "crates/testenv",
"example-crates/example_cli",
"example-crates/example_electrum",
"example-crates/example_esplora",
bdk_chain = { path = "../chain", version = "0.11", default-features = false }
[dev-dependencies]
-bitcoind = { version = "0.33", features = ["25_0"] }
+bdk_testenv = { path = "../testenv", default_features = false }
anyhow = { version = "1" }
[features]
use bdk_bitcoind_rpc::Emitter;
use bdk_chain::{
- bitcoin::{Address, Amount, BlockHash, Txid},
+ bitcoin::{Address, Amount, Txid},
keychain::Balance,
local_chain::{self, CheckPoint, LocalChain},
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
};
-use bitcoin::{
- address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
- secp256k1::rand::random, Block, CompactTarget, OutPoint, ScriptBuf, ScriptHash, Transaction,
- TxIn, TxOut, WScriptHash,
-};
-use bitcoincore_rpc::{
- bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
- RpcApi,
-};
-
-struct TestEnv {
- #[allow(dead_code)]
- daemon: bitcoind::BitcoinD,
- client: bitcoincore_rpc::Client,
-}
-
-impl TestEnv {
- fn new() -> anyhow::Result<Self> {
- let daemon = match std::env::var_os("TEST_BITCOIND") {
- Some(bitcoind_path) => bitcoind::BitcoinD::new(bitcoind_path),
- None => bitcoind::BitcoinD::from_downloaded(),
- }?;
- let client = bitcoincore_rpc::Client::new(
- &daemon.rpc_url(),
- bitcoincore_rpc::Auth::CookieFile(daemon.params.cookie_file.clone()),
- )?;
- Ok(Self { daemon, client })
- }
-
- fn mine_blocks(
- &self,
- count: usize,
- address: Option<Address>,
- ) -> anyhow::Result<Vec<BlockHash>> {
- let coinbase_address = match address {
- Some(address) => address,
- None => self.client.get_new_address(None, None)?.assume_checked(),
- };
- let block_hashes = self
- .client
- .generate_to_address(count as _, &coinbase_address)?;
- Ok(block_hashes)
- }
-
- fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
- let bt = self.client.get_block_template(
- GetBlockTemplateModes::Template,
- &[GetBlockTemplateRules::SegWit],
- &[],
- )?;
-
- let txdata = vec![Transaction {
- version: 1,
- lock_time: bitcoin::absolute::LockTime::from_height(0)?,
- input: vec![TxIn {
- previous_output: bitcoin::OutPoint::default(),
- script_sig: ScriptBuf::builder()
- .push_int(bt.height as _)
- // randomn number so that re-mining creates unique block
- .push_int(random())
- .into_script(),
- sequence: bitcoin::Sequence::default(),
- witness: bitcoin::Witness::new(),
- }],
- output: vec![TxOut {
- value: 0,
- script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
- }],
- }];
-
- let bits: [u8; 4] = bt
- .bits
- .clone()
- .try_into()
- .expect("rpc provided us with invalid bits");
-
- let mut block = Block {
- header: Header {
- version: bitcoin::block::Version::default(),
- prev_blockhash: bt.previous_block_hash,
- merkle_root: TxMerkleNode::all_zeros(),
- time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
- bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
- nonce: 0,
- },
- txdata,
- };
-
- block.header.merkle_root = block.compute_merkle_root().expect("must compute");
-
- for nonce in 0..=u32::MAX {
- block.header.nonce = nonce;
- if block.header.target().is_met_by(block.block_hash()) {
- break;
- }
- }
-
- self.client.submit_block(&block)?;
- Ok((bt.height as usize, block.block_hash()))
- }
-
- fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
- let mut hash = self.client.get_best_block_hash()?;
- for _ in 0..count {
- let prev_hash = self.client.get_block_info(&hash)?.previousblockhash;
- self.client.invalidate_block(&hash)?;
- match prev_hash {
- Some(prev_hash) => hash = prev_hash,
- None => break,
- }
- }
- Ok(())
- }
-
- fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
- let start_height = self.client.get_block_count()?;
- self.invalidate_blocks(count)?;
-
- let res = self.mine_blocks(count, None);
- assert_eq!(
- self.client.get_block_count()?,
- start_height,
- "reorg should not result in height change"
- );
- res
- }
-
- fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
- let start_height = self.client.get_block_count()?;
- self.invalidate_blocks(count)?;
-
- let res = (0..count)
- .map(|_| self.mine_empty_block())
- .collect::<Result<Vec<_>, _>>()?;
- assert_eq!(
- self.client.get_block_count()?,
- start_height,
- "reorg should not result in height change"
- );
- Ok(res)
- }
-
- fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
- let txid = self
- .client
- .send_to_address(address, amount, None, None, None, None, None, None)?;
- Ok(txid)
- }
-}
+use bdk_testenv::TestEnv;
+use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
+use bitcoincore_rpc::RpcApi;
/// Ensure that blocks are emitted in order even after reorg.
///
#[test]
pub fn test_sync_local_chain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
- let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
- let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
+ let network_tip = env.rpc_client().get_block_count()?;
+ let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
+ let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
- // mine some blocks and returned the actual block hashes
+ // Mine some blocks and return the actual block hashes.
+ // Because initializing `ElectrsD` already mines some blocks, we must include those too when
+ // returning block hashes.
let exp_hashes = {
- let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
- hashes.extend(env.mine_blocks(101, None)?);
+ let mut hashes = (0..=network_tip)
+ .map(|height| env.rpc_client().get_block_hash(height))
+ .collect::<Result<Vec<_>, _>>()?;
+ hashes.extend(env.mine_blocks(101 - network_tip as usize, None)?);
hashes
};
- // see if the emitter outputs the right blocks
+ // See if the emitter outputs the right blocks.
println!("first sync:");
while let Some(emission) = emitter.next_block()? {
let height = emission.block_height();
"final local_chain state is unexpected",
);
- // perform reorg
+ // Perform reorg.
let reorged_blocks = env.reorg(6)?;
let exp_hashes = exp_hashes
.iter()
.cloned()
.collect::<Vec<_>>();
- // see if the emitter outputs the right blocks
+ // See if the emitter outputs the right blocks.
println!("after reorg:");
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
while let Some(emission) = emitter.next_block()? {
let env = TestEnv::new()?;
println!("getting new addresses!");
- let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
- let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
- let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
+ let addr_0 = env
+ .rpc_client()
+ .get_new_address(None, None)?
+ .assume_checked();
+ let addr_1 = env
+ .rpc_client()
+ .get_new_address(None, None)?
+ .assume_checked();
+ let addr_2 = env
+ .rpc_client()
+ .get_new_address(None, None)?
+ .assume_checked();
println!("got new addresses!");
println!("mining block!");
env.mine_blocks(101, None)?;
println!("mined blocks!");
- let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
+ let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
let mut index = SpkTxOutIndex::<usize>::default();
index.insert_spk(0, addr_0.script_pubkey());
index
});
- let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
+ let emitter = &mut Emitter::new(env.rpc_client(), chain.tip(), 0);
while let Some(emission) = emitter.next_block()? {
let height = emission.block_height();
let exp_txids = {
let mut txids = BTreeSet::new();
for _ in 0..3 {
- txids.insert(env.client.send_to_address(
+ txids.insert(env.rpc_client().send_to_address(
&addr_0,
Amount::from_sat(10_000),
None,
// mine a block that confirms the 3 txs
let exp_block_hash = env.mine_blocks(1, None)?[0];
- let exp_block_height = env.client.get_block_info(&exp_block_hash)?.height as u32;
+ let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32;
let exp_anchors = exp_txids
.iter()
.map({
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
- &env.client,
+ env.rpc_client(),
CheckPoint::new(BlockId {
height: 0,
- hash: env.client.get_block_hash(0)?,
+ hash: env.rpc_client().get_block_hash(0)?,
}),
EMITTER_START_HEIGHT as _,
);
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
- &env.client,
+ env.rpc_client(),
CheckPoint::new(BlockId {
height: 0,
- hash: env.client.get_block_hash(0)?,
+ hash: env.rpc_client().get_block_hash(0)?,
}),
0,
);
// setup addresses
- let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
+ let addr_to_mine = env
+ .rpc_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, bitcoin::Network::Regtest)?;
// setup receiver
- let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
+ let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone());
// lock outputs that send to `addr_to_track`
let outpoints_to_lock = env
- .client
+ .rpc_client()
.get_transaction(&txid, None)?
.transaction()?
.output
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
.map(|(vout, _)| OutPoint::new(txid, vout as _))
.collect::<Vec<_>>();
- env.client.lock_unspent(&outpoints_to_lock)?;
+ env.rpc_client().lock_unspent(&outpoints_to_lock)?;
let _ = env.mine_blocks(1, None)?;
}
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
- &env.client,
+ env.rpc_client(),
CheckPoint::new(BlockId {
height: 0,
- hash: env.client.get_block_hash(0)?,
+ hash: env.rpc_client().get_block_hash(0)?,
}),
0,
);
// mine blocks and sync up emitter
- let addr = env.client.get_new_address(None, None)?.assume_checked();
+ let addr = env
+ .rpc_client()
+ .get_new_address(None, None)?
+ .assume_checked();
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
while emitter.next_header()?.is_some() {}
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
- &env.client,
+ env.rpc_client(),
CheckPoint::new(BlockId {
height: 0,
- hash: env.client.get_block_hash(0)?,
+ hash: env.rpc_client().get_block_hash(0)?,
}),
0,
);
// mine blocks to get initial balance, sync emitter up to tip
- let addr = env.client.get_new_address(None, None)?.assume_checked();
+ let addr = env
+ .rpc_client()
+ .get_new_address(None, None)?
+ .assume_checked();
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
while emitter.next_header()?.is_some() {}
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
- &env.client,
+ env.rpc_client(),
CheckPoint::new(BlockId {
height: 0,
- hash: env.client.get_block_hash(0)?,
+ hash: env.rpc_client().get_block_hash(0)?,
}),
0,
);
// mine blocks to get initial balance
- let addr = env.client.get_new_address(None, None)?.assume_checked();
+ let addr = env
+ .rpc_client()
+ .get_new_address(None, None)?
+ .assume_checked();
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
// introduce mempool tx at each block extension
.into_iter()
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<_>>(),
- env.client
+ env.rpc_client()
.get_raw_mempool()?
.into_iter()
.collect::<BTreeSet<_>>(),
// emission.
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
let tx_introductions = dbg!(env
- .client
+ .rpc_client()
.get_raw_mempool_verbose()?
.into_iter()
.map(|(txid, entry)| (txid, entry.height as usize))
// start height is 99
let mut emitter = Emitter::new(
- &env.client,
+ env.rpc_client(),
CheckPoint::new(BlockId {
height: 0,
- hash: env.client.get_block_hash(0)?,
+ hash: env.rpc_client().get_block_hash(0)?,
}),
(PREMINE_COUNT - 2) as u32,
);
let block_hash_100a = block_header_100a.block_hash();
// get hash for block 101a
- let block_hash_101a = env.client.get_block_hash(101)?;
+ let block_hash_101a = env.rpc_client().get_block_hash(101)?;
// invalidate blocks 99a, 100a, 101a
- env.client.invalidate_block(&block_hash_99a)?;
- env.client.invalidate_block(&block_hash_100a)?;
- env.client.invalidate_block(&block_hash_101a)?;
+ env.rpc_client().invalidate_block(&block_hash_99a)?;
+ env.rpc_client().invalidate_block(&block_hash_100a)?;
+ env.rpc_client().invalidate_block(&block_hash_101a)?;
// mine new blocks 99b, 100b, 101b
env.mine_blocks(3, None)?;
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
electrum-client = { version = "0.18" }
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
+
+[dev-dependencies]
+bdk_testenv = { path = "../testenv", default-features = false }
+electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
+anyhow = "1"
\ No newline at end of file
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
let mut request_spks = keychain_spks
.into_iter()
- .map(|(k, s)| (k.clone(), s.into_iter()))
+ .map(|(k, s)| (k, s.into_iter()))
.collect::<BTreeMap<K, _>>();
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
bitcoin = { version = "0.30.0", optional = true, default-features = false }
miniscript = { version = "10.0.0", optional = true, default-features = false }
+[dev-dependencies]
+bdk_testenv = { path = "../testenv", default_features = false }
+
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
use bdk_esplora::EsploraAsyncExt;
+use electrsd::bitcoind::anyhow;
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
-use electrsd::bitcoind::{self, anyhow, BitcoinD};
-use electrsd::{Conf, ElectrsD};
-use esplora_client::{self, AsyncClient, Builder};
+use esplora_client::{self, Builder};
use std::collections::{BTreeMap, HashSet};
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
-use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
-
-struct TestEnv {
- bitcoind: BitcoinD,
- #[allow(dead_code)]
- electrsd: ElectrsD,
- client: AsyncClient,
-}
-
-impl TestEnv {
- fn new() -> Result<Self, anyhow::Error> {
- let bitcoind_exe =
- bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
- let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
-
- let mut electrs_conf = Conf::default();
- electrs_conf.http_enabled = true;
- let electrs_exe =
- electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
- let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
-
- let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
- let client = Builder::new(base_url.as_str()).build_async()?;
-
- Ok(Self {
- bitcoind,
- electrsd,
- client,
- })
- }
-
- fn mine_blocks(
- &self,
- count: usize,
- address: Option<Address>,
- ) -> anyhow::Result<Vec<BlockHash>> {
- let coinbase_address = match address {
- Some(address) => address,
- None => self
- .bitcoind
- .client
- .get_new_address(None, None)?
- .assume_checked(),
- };
- let block_hashes = self
- .bitcoind
- .client
- .generate_to_address(count as _, &coinbase_address)?;
- Ok(block_hashes)
- }
-}
+use bdk_chain::bitcoin::{Address, Amount, Txid};
+use bdk_testenv::TestEnv;
#[tokio::test]
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
+ let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+ let client = Builder::new(base_url.as_str()).build_async()?;
+
let receive_address0 =
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
let receive_address1 =
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
- while env.client.get_height().await.unwrap() < 102 {
+ while client.get_height().await.unwrap() < 102 {
sleep(Duration::from_millis(10))
}
- let graph_update = env
- .client
+ let graph_update = client
.sync(
misc_spks.into_iter(),
vec![].into_iter(),
#[tokio::test]
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
let env = TestEnv::new()?;
+ let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+ let client = Builder::new(base_url.as_str()).build_async()?;
let _block_hashes = env.mine_blocks(101, None)?;
// Now let's test the gap limit. First of all get a chain of 10 addresses.
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
- while env.client.get_height().await.unwrap() < 103 {
+ while client.get_height().await.unwrap() < 103 {
sleep(Duration::from_millis(10))
}
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
- let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
+ let (graph_update, active_indices) = client.full_scan(keychains.clone(), 2, 1).await?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
- let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
+ let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
- while env.client.get_height().await.unwrap() < 104 {
+ while client.get_height().await.unwrap() < 104 {
sleep(Duration::from_millis(10))
}
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
- let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
+ let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(active_indices[&0], 3);
- let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
+ let (graph_update, active_indices) = client.full_scan(keychains, 5, 1).await?;
let txs: HashSet<_> = 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));
use bdk_chain::local_chain::LocalChain;
use bdk_chain::BlockId;
use bdk_esplora::EsploraExt;
+use electrsd::bitcoind::anyhow;
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
-use electrsd::bitcoind::{self, anyhow, BitcoinD};
-use electrsd::{Conf, ElectrsD};
-use esplora_client::{self, BlockingClient, Builder};
+use esplora_client::{self, Builder};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
-use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
+use bdk_chain::bitcoin::{Address, Amount, Txid};
+use bdk_testenv::TestEnv;
macro_rules! h {
($index:literal) => {{
}};
}
-struct TestEnv {
- bitcoind: BitcoinD,
- #[allow(dead_code)]
- electrsd: ElectrsD,
- client: BlockingClient,
-}
-
-impl TestEnv {
- fn new() -> Result<Self, anyhow::Error> {
- let bitcoind_exe =
- bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
- let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
-
- let mut electrs_conf = Conf::default();
- electrs_conf.http_enabled = true;
- let electrs_exe =
- electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
- let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
-
- let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
- let client = Builder::new(base_url.as_str()).build_blocking()?;
-
- Ok(Self {
- bitcoind,
- electrsd,
- client,
- })
- }
-
- fn reset_electrsd(mut self) -> anyhow::Result<Self> {
- let mut electrs_conf = Conf::default();
- electrs_conf.http_enabled = true;
- let electrs_exe =
- electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
- let electrsd = ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrs_conf)?;
-
- let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
- let client = Builder::new(base_url.as_str()).build_blocking()?;
- self.electrsd = electrsd;
- self.client = client;
- Ok(self)
- }
-
- fn mine_blocks(
- &self,
- count: usize,
- address: Option<Address>,
- ) -> anyhow::Result<Vec<BlockHash>> {
- let coinbase_address = match address {
- Some(address) => address,
- None => self
- .bitcoind
- .client
- .get_new_address(None, None)?
- .assume_checked(),
- };
- let block_hashes = self
- .bitcoind
- .client
- .generate_to_address(count as _, &coinbase_address)?;
- Ok(block_hashes)
- }
-}
-
#[test]
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
+ let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+ let client = Builder::new(base_url.as_str()).build_blocking()?;
+
let receive_address0 =
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
let receive_address1 =
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
- while env.client.get_height().unwrap() < 102 {
+ while client.get_height().unwrap() < 102 {
sleep(Duration::from_millis(10))
}
- let graph_update = env.client.sync(
+ let graph_update = client.sync(
misc_spks.into_iter(),
vec![].into_iter(),
vec![].into_iter(),
#[test]
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
let env = TestEnv::new()?;
+ let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+ let client = Builder::new(base_url.as_str()).build_blocking()?;
let _block_hashes = env.mine_blocks(101, None)?;
// Now let's test the gap limit. First of all get a chain of 10 addresses.
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
- while env.client.get_height().unwrap() < 103 {
+ while client.get_height().unwrap() < 103 {
sleep(Duration::from_millis(10))
}
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
- let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
+ let (graph_update, active_indices) = client.full_scan(keychains.clone(), 2, 1)?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
- let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
+ let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
- while env.client.get_height().unwrap() < 104 {
+ while client.get_height().unwrap() < 104 {
sleep(Duration::from_millis(10))
}
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
- let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
+ let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(active_indices[&0], 3);
- let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
+ let (graph_update, active_indices) = client.full_scan(keychains, 5, 1)?;
let txs: HashSet<_> = 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));
};
// so new blocks can be seen by Electrs
let env = env.reset_electrsd()?;
+ let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
+ let client = Builder::new(base_url.as_str()).build_blocking()?;
struct TestCase {
name: &'static str,
println!("Case {}: {}", i, t.name);
let mut chain = t.chain;
- let update = env
- .client
+ let update = client
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
.map_err(|err| {
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
--- /dev/null
+[package]
+name = "bdk_testenv"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+bitcoincore-rpc = { version = "0.17" }
+bdk_chain = { path = "../chain", version = "0.11", default-features = false }
+electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
+anyhow = { version = "1" }
+
+[features]
+default = ["std"]
+std = ["bdk_chain/std"]
+serde = ["bdk_chain/serde"]
\ No newline at end of file
--- /dev/null
+# BDK TestEnv
+
+This crate sets up a regtest environment with a single [`bitcoind`] node
+connected to an [`electrs`] instance. This framework provides the infrastructure
+for testing chain source crates, e.g., [`bdk_chain`], [`bdk_electrum`],
+[`bdk_esplora`], etc.
\ No newline at end of file
--- /dev/null
+use bdk_chain::bitcoin::{
+ address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
+ secp256k1::rand::random, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf,
+ ScriptHash, Transaction, TxIn, TxOut, Txid,
+};
+use bitcoincore_rpc::{
+ bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
+ RpcApi,
+};
+use electrsd::electrum_client::ElectrumApi;
+use std::time::Duration;
+
+/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
+/// instance connected to it.
+pub struct TestEnv {
+ pub bitcoind: electrsd::bitcoind::BitcoinD,
+ pub electrsd: electrsd::ElectrsD,
+}
+
+impl TestEnv {
+ /// Construct a new [`TestEnv`] instance with default configurations.
+ pub fn new() -> anyhow::Result<Self> {
+ let bitcoind = match std::env::var_os("BITCOIND_EXE") {
+ Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
+ None => {
+ let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
+ .expect(
+ "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
+ );
+ electrsd::bitcoind::BitcoinD::with_conf(
+ bitcoind_exe,
+ &electrsd::bitcoind::Conf::default(),
+ )
+ }
+ }?;
+
+ let mut electrsd_conf = electrsd::Conf::default();
+ electrsd_conf.http_enabled = true;
+ let electrsd = match std::env::var_os("ELECTRS_EXE") {
+ Some(env_electrs_exe) => {
+ electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
+ }
+ None => {
+ let electrs_exe = electrsd::downloaded_exe_path()
+ .expect("electrs version feature must be enabled");
+ electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
+ }
+ }?;
+
+ Ok(Self { bitcoind, electrsd })
+ }
+
+ /// Exposes the [`ElectrumApi`] calls from the Electrum client.
+ pub fn electrum_client(&self) -> &impl ElectrumApi {
+ &self.electrsd.client
+ }
+
+ /// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
+ pub fn rpc_client(&self) -> &impl RpcApi {
+ &self.bitcoind.client
+ }
+
+ // Reset `electrsd` so that new blocks can be seen.
+ pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
+ let mut electrsd_conf = electrsd::Conf::default();
+ electrsd_conf.http_enabled = true;
+ let electrsd = match std::env::var_os("ELECTRS_EXE") {
+ Some(env_electrs_exe) => {
+ electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
+ }
+ None => {
+ let electrs_exe = electrsd::downloaded_exe_path()
+ .expect("electrs version feature must be enabled");
+ electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
+ }
+ }?;
+ self.electrsd = electrsd;
+ Ok(self)
+ }
+
+ /// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
+ /// `address`.
+ pub fn mine_blocks(
+ &self,
+ count: usize,
+ address: Option<Address>,
+ ) -> anyhow::Result<Vec<BlockHash>> {
+ let coinbase_address = match address {
+ Some(address) => address,
+ None => self
+ .bitcoind
+ .client
+ .get_new_address(None, None)?
+ .assume_checked(),
+ };
+ let block_hashes = self
+ .bitcoind
+ .client
+ .generate_to_address(count as _, &coinbase_address)?;
+ Ok(block_hashes)
+ }
+
+ /// Mine a block that is guaranteed to be empty even with transactions in the mempool.
+ pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
+ let bt = self.bitcoind.client.get_block_template(
+ GetBlockTemplateModes::Template,
+ &[GetBlockTemplateRules::SegWit],
+ &[],
+ )?;
+
+ let txdata = vec![Transaction {
+ version: 1,
+ lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
+ input: vec![TxIn {
+ previous_output: bdk_chain::bitcoin::OutPoint::default(),
+ script_sig: ScriptBuf::builder()
+ .push_int(bt.height as _)
+ // randomn number so that re-mining creates unique block
+ .push_int(random())
+ .into_script(),
+ sequence: bdk_chain::bitcoin::Sequence::default(),
+ witness: bdk_chain::bitcoin::Witness::new(),
+ }],
+ output: vec![TxOut {
+ value: 0,
+ script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
+ }],
+ }];
+
+ let bits: [u8; 4] = bt
+ .bits
+ .clone()
+ .try_into()
+ .expect("rpc provided us with invalid bits");
+
+ let mut block = Block {
+ header: Header {
+ version: bdk_chain::bitcoin::block::Version::default(),
+ prev_blockhash: bt.previous_block_hash,
+ merkle_root: TxMerkleNode::all_zeros(),
+ time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
+ bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
+ nonce: 0,
+ },
+ txdata,
+ };
+
+ block.header.merkle_root = block.compute_merkle_root().expect("must compute");
+
+ for nonce in 0..=u32::MAX {
+ block.header.nonce = nonce;
+ if block.header.target().is_met_by(block.block_hash()) {
+ break;
+ }
+ }
+
+ self.bitcoind.client.submit_block(&block)?;
+ Ok((bt.height as usize, block.block_hash()))
+ }
+
+ /// This method waits for the Electrum notification indicating that a new block has been mined.
+ pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
+ self.electrsd.client.block_headers_subscribe()?;
+ let mut delay = Duration::from_millis(64);
+
+ loop {
+ self.electrsd.trigger()?;
+ self.electrsd.client.ping()?;
+ if self.electrsd.client.block_headers_pop()?.is_some() {
+ return Ok(());
+ }
+
+ if delay.as_millis() < 512 {
+ delay = delay.mul_f32(2.0);
+ }
+ std::thread::sleep(delay);
+ }
+ }
+
+ /// Invalidate a number of blocks of a given size `count`.
+ pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
+ let mut hash = self.bitcoind.client.get_best_block_hash()?;
+ for _ in 0..count {
+ let prev_hash = self
+ .bitcoind
+ .client
+ .get_block_info(&hash)?
+ .previousblockhash;
+ self.bitcoind.client.invalidate_block(&hash)?;
+ match prev_hash {
+ Some(prev_hash) => hash = prev_hash,
+ None => break,
+ }
+ }
+ Ok(())
+ }
+
+ /// Reorg a number of blocks of a given size `count`.
+ /// Refer to [`TestEnv::mine_empty_block`] for more information.
+ pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
+ let start_height = self.bitcoind.client.get_block_count()?;
+ self.invalidate_blocks(count)?;
+
+ let res = self.mine_blocks(count, None);
+ assert_eq!(
+ self.bitcoind.client.get_block_count()?,
+ start_height,
+ "reorg should not result in height change"
+ );
+ res
+ }
+
+ /// Reorg with a number of empty blocks of a given size `count`.
+ pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
+ let start_height = self.bitcoind.client.get_block_count()?;
+ self.invalidate_blocks(count)?;
+
+ let res = (0..count)
+ .map(|_| self.mine_empty_block())
+ .collect::<Result<Vec<_>, _>>()?;
+ assert_eq!(
+ self.bitcoind.client.get_block_count()?,
+ start_height,
+ "reorg should not result in height change"
+ );
+ Ok(res)
+ }
+
+ /// Send a tx of a given `amount` to a given `address`.
+ pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
+ let txid = self
+ .bitcoind
+ .client
+ .send_to_address(address, amount, None, None, None, None, None, None)?;
+ Ok(txid)
+ }
+}