diff --git a/Cargo.toml b/Cargo.toml index 8438b5ce..02d491e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,10 @@ name = "esplora-client" version = "0.12.1" edition = "2021" -authors = ["Alekos Filini "] +authors = [ + "Alekos Filini ", + "Bitcoin Dev Kit Developers" +] license = "MIT" homepage = "https://github.com/bitcoindevkit/rust-esplora-client" repository = "https://github.com/bitcoindevkit/rust-esplora-client" diff --git a/src/api.rs b/src/api.rs index 14e3661f..0ce3369a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,15 +2,16 @@ //! //! See: +use bitcoin::hash_types; +use serde::Deserialize; + pub use bitcoin::consensus::{deserialize, serialize}; pub use bitcoin::hex::FromHex; -use bitcoin::Weight; pub use bitcoin::{ - transaction, Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness, + absolute, block, transaction, Amount, BlockHash, CompactTarget, OutPoint, ScriptBuf, + ScriptHash, Transaction, TxIn, TxOut, Txid, Weight, Witness, }; -use serde::Deserialize; - #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct PrevOut { pub value: u64, @@ -87,6 +88,59 @@ pub struct BlockTime { pub height: u32, } +/// Information about a [`Block`]. +#[derive(Debug, Clone, Deserialize)] +pub struct BlockInformation { + /// The block's [`BlockHash`]. + pub id: BlockHash, + /// The block's height. + pub height: u32, + /// The block's version. + pub version: block::Version, + /// The block's timestamp. + pub timestamp: u64, + /// The block's transaction count. + pub tx_count: u64, + /// The block's size, in bytes. + pub size: usize, + /// The block's weight. + pub weight: u64, + /// The merkle root of the transactions in the block. + pub merkle_root: hash_types::TxMerkleNode, + /// The [`BlockHash`] of the previous block (`None` for the genesis block). + pub previousblockhash: Option, + /// The block's MTP (Median Time Past). + pub mediantime: u64, + /// The block's nonce value. + pub nonce: u32, + /// The block's `bits` value as a [`CompactTarget`]. + pub bits: CompactTarget, + /// The block's difficulty target value. + pub difficulty: f64, +} + +impl PartialEq for BlockInformation { + fn eq(&self, other: &Self) -> bool { + let Self { difficulty: d1, .. } = self; + let Self { difficulty: d2, .. } = other; + + self.id == other.id + && self.height == other.height + && self.version == other.version + && self.timestamp == other.timestamp + && self.tx_count == other.tx_count + && self.size == other.size + && self.weight == other.weight + && self.merkle_root == other.merkle_root + && self.previousblockhash == other.previousblockhash + && self.mediantime == other.mediantime + && self.nonce == other.nonce + && self.bits == other.bits + && ((d1.is_nan() && d2.is_nan()) || (d1 == d2)) + } +} +impl Eq for BlockInformation {} + #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct BlockSummary { pub id: BlockHash, @@ -123,6 +177,18 @@ pub struct AddressTxsSummary { pub tx_count: u32, } +/// Statistics about a particular [`Script`] hash's confirmed and mempool transactions. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] +pub struct ScriptHashStats { + /// The summary of confirmed transactions for this [`Script`] hash. + pub chain_stats: ScriptHashTxsSummary, + /// The summary of mempool transactions for this [`Script`] hash. + pub mempool_stats: ScriptHashTxsSummary, +} + +/// Contains a summary of the transactions for a particular [`Script`] hash. +pub type ScriptHashTxsSummary = AddressTxsSummary; + /// Information about an UTXO's status: confirmation status, /// confirmation height, confirmation block hash and confirmation block time. #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] @@ -150,6 +216,36 @@ pub struct Utxo { pub value: Amount, } +/// Statistics about the mempool. +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct MempoolStats { + /// The number of transactions in the mempool. + pub count: usize, + /// The total size of mempool transactions in virtual bytes. + pub vsize: usize, + /// The total fee paid by mempool transactions, in sats. + pub total_fee: u64, + /// The mempool's fee rate distribution histogram. + /// + /// An array of `(feerate, vsize)` tuples, where each entry's `vsize` is the total vsize + /// of transactions paying more than `feerate` but less than the previous entry's `feerate` + /// (except for the first entry, which has no upper bound). + pub fee_histogram: Vec<(f64, usize)>, +} + +/// A [`Transaction`] that recently entered the mempool. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub struct MempoolRecentTx { + /// Transaction ID as a [`Txid`]. + pub txid: Txid, + /// [`Amount`] of fees paid by the transaction, in satoshis. + pub fee: u64, + /// The transaction size, in virtual bytes. + pub vsize: usize, + /// Combined [`Amount`] of the transaction, in satoshis. + pub value: u64, +} + impl Tx { pub fn to_tx(&self) -> Transaction { Transaction { diff --git a/src/async.rs b/src/async.rs index 390eb2f7..3df87bf3 100644 --- a/src/async.rs +++ b/src/async.rs @@ -15,22 +15,20 @@ use std::collections::HashMap; use std::marker::PhantomData; use std::str::FromStr; +use bitcoin::block::Header as BlockHeader; use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::hex::{DisplayHex, FromHex}; -use bitcoin::Address; -use bitcoin::{ - block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid, -}; +use bitcoin::{Address, Block, BlockHash, MerkleBlock, Script, Transaction, Txid}; #[allow(unused_imports)] use log::{debug, error, info, trace}; use reqwest::{header, Client, Response}; -use crate::api::AddressStats; use crate::{ - BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus, Utxo, + AddressStats, BlockInformation, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx, + MempoolStats, MerkleProof, OutputStatus, ScriptHashStats, Tx, TxStatus, Utxo, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES, }; @@ -315,6 +313,12 @@ impl AsyncClient { self.get_opt_response_json(&format!("/tx/{txid}")).await } + /// Get the spend status of a [`Transaction`]'s outputs, given it's [`Txid`]. + pub async fn get_tx_outspends(&self, txid: &Txid) -> Result, Error> { + self.get_response_json(&format!("/tx/{txid}/outspends")) + .await + } + /// Get a [`BlockHeader`] given a particular block hash. pub async fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result { self.get_response_hex(&format!("/block/{block_hash}/header")) @@ -391,7 +395,14 @@ impl AsyncClient { self.get_response_json(&path).await } - /// Get transaction history for the specified address/scripthash, sorted with newest first. + /// Get statistics about a particular [`Script`] hash's confirmed and mempool transactions. + pub async fn get_scripthash_stats(&self, script: &Script) -> Result { + let script_hash = sha256::Hash::hash(script.as_bytes()); + let path = format!("/scripthash/{script_hash}"); + self.get_response_json(&path).await + } + + /// Get transaction history for the specified address, sorted with newest first. /// /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions. /// More can be requested by specifying the last txid seen by the previous query. @@ -408,7 +419,14 @@ impl AsyncClient { self.get_response_json(&path).await } - /// Get confirmed transaction history for the specified address/scripthash, + /// Get mempool [`Transaction`]s for the specified [`Address`], sorted with newest first. + pub async fn get_mempool_address_txs(&self, address: &Address) -> Result, Error> { + let path = format!("/address/{address}/txs/mempool"); + + self.get_response_json(&path).await + } + + /// Get transaction history for the specified address/scripthash, /// sorted with newest first. Returns 25 transactions per page. /// More can be requested by specifying the last txid seen by the previous /// query. @@ -426,12 +444,70 @@ impl AsyncClient { self.get_response_json(&path).await } + /// Get mempool [`Transaction`] history for the + /// specified [`Script`] hash, sorted with newest first. + pub async fn get_mempool_scripthash_txs(&self, script: &Script) -> Result, Error> { + let script_hash = sha256::Hash::hash(script.as_bytes()); + let path = format!("/scripthash/{script_hash:x}/txs/mempool"); + + self.get_response_json(&path).await + } + + /// Get statistics about the mempool. + pub async fn get_mempool_stats(&self) -> Result { + self.get_response_json("/mempool").await + } + + // Get a list of the last 10 [`Transaction`]s to enter the mempool. + pub async fn get_mempool_recent_txs(&self) -> Result, Error> { + self.get_response_json("/mempool/recent").await + } + + /// Get the full list of [`Txid`]s in the mempool. + /// + /// The order of the [`Txid`]s is arbitrary. + pub async fn get_mempool_txids(&self) -> Result, Error> { + self.get_response_json("/mempool/txids").await + } + /// Get an map where the key is the confirmation target (in number of /// blocks) and the value is the estimated feerate (in sat/vB). pub async fn get_fee_estimates(&self) -> Result, Error> { self.get_response_json("/fee-estimates").await } + /// Get a summary about a [`Block`], given it's [`BlockHash`]. + pub async fn get_block(&self, blockhash: &BlockHash) -> Result { + let path = format!("/block/{blockhash}"); + + self.get_response_json(&path).await + } + + /// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`]. + pub async fn get_block_txids(&self, blockhash: &BlockHash) -> Result, Error> { + let path = format!("/block/{blockhash}/txids"); + + self.get_response_json(&path).await + } + + /// Get up to 25 [`Transaction`]s from a [`Block`], given it's [`BlockHash`], + /// beginning at `start_index` (starts from 0 if `start_index` is `None`). + /// + /// The `start_index` value MUST be a multiple of 25, + /// else an error will be returned by Esplora. + pub async fn get_block_txs( + &self, + blockhash: &BlockHash, + start_index: Option, + ) -> Result, Error> { + let path = match start_index { + None => format!("/block/{blockhash}/txs"), + Some(start_index) => format!("/block/{blockhash}/txs/{start_index}"), + }; + + self.get_response_json(&path).await + } + /// Gets some recent block summaries starting at the tip or at `height` if /// provided. /// @@ -451,8 +527,17 @@ impl AsyncClient { /// Get all UTXOs locked to an address. pub async fn get_address_utxos(&self, address: &Address) -> Result, Error> { - self.get_response_json(&format!("/address/{address}/utxo")) - .await + let path = format!("/address/{address}/utxo"); + + self.get_response_json(&path).await + } + + /// Get all [`TxOut`]s locked to a [`Script`] hash. + pub async fn get_scripthash_utxos(&self, script: &Script) -> Result, Error> { + let script_hash = sha256::Hash::hash(script.as_bytes()); + let path = format!("/scripthash/{script_hash}/utxo"); + + self.get_response_json(&path).await } /// Get the underlying base URL. diff --git a/src/blocking.rs b/src/blocking.rs index b959c858..e0643c36 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -21,17 +21,15 @@ use log::{debug, error, info, trace}; use minreq::{Proxy, Request, Response}; +use bitcoin::block::Header as BlockHeader; use bitcoin::consensus::{deserialize, serialize, Decodable}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::hex::{DisplayHex, FromHex}; -use bitcoin::Address; -use bitcoin::{ - block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid, -}; +use bitcoin::{Address, Block, BlockHash, MerkleBlock, Script, Transaction, Txid}; -use crate::api::AddressStats; use crate::{ - BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus, Utxo, + AddressStats, BlockInformation, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx, + MempoolStats, MerkleProof, OutputStatus, ScriptHashStats, Tx, TxStatus, Utxo, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES, }; @@ -229,6 +227,11 @@ impl BlockingClient { self.get_opt_response_json(&format!("/tx/{txid}")) } + /// Get the spend status of a [`Transaction`]'s outputs, given it's [`Txid`]. + pub fn get_tx_outspends(&self, txid: &Txid) -> Result, Error> { + self.get_response_json(&format!("/tx/{txid}/outspends")) + } + /// Get a [`BlockHeader`] given a particular block hash. pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result { self.get_response_hex(&format!("/block/{block_hash}/header")) @@ -313,6 +316,23 @@ impl BlockingClient { .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))? } + /// Get statistics about the mempool. + pub fn get_mempool_stats(&self) -> Result { + self.get_response_json("/mempool") + } + + // Get a list of the last 10 [`Transaction`]s to enter the mempool. + pub fn get_mempool_recent_txs(&self) -> Result, Error> { + self.get_response_json("/mempool/recent") + } + + /// Get the full list of [`Txid`]s in the mempool. + /// + /// The order of the txids is arbitrary and does not match bitcoind's. + pub fn get_mempool_txids(&self) -> Result, Error> { + self.get_response_json("/mempool/txids") + } + /// Get an map where the key is the confirmation target (in number of /// blocks) and the value is the estimated feerate (in sat/vB). pub fn get_fee_estimates(&self) -> Result, Error> { @@ -326,7 +346,15 @@ impl BlockingClient { self.get_response_json(&path) } - /// Get transaction history for the specified address/scripthash, sorted with newest first. + /// Get statistics about a particular [`Script`] hash's confirmed and mempool transactions. + pub fn get_scripthash_stats(&self, script: &Script) -> Result { + let script_hash = sha256::Hash::hash(script.as_bytes()); + let path = format!("/scripthash/{script_hash}"); + self.get_response_json(&path) + } + + /// Get transaction history for the specified address, sorted with newest + /// first. /// /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions. /// More can be requested by specifying the last txid seen by the previous query. @@ -343,7 +371,14 @@ impl BlockingClient { self.get_response_json(&path) } - /// Get confirmed transaction history for the specified address/scripthash, + /// Get mempool [`Transaction`]s for the specified [`Address`], sorted with newest first. + pub fn get_mempool_address_txs(&self, address: &Address) -> Result, Error> { + let path = format!("/address/{address}/txs/mempool"); + + self.get_response_json(&path) + } + + /// Get transaction history for the specified scripthash, /// sorted with newest first. Returns 25 transactions per page. /// More can be requested by specifying the last txid seen by the previous /// query. @@ -360,6 +395,47 @@ impl BlockingClient { self.get_response_json(&path) } + /// Get mempool [`Transaction`] history for the + /// specified [`Script`] hash, sorted with newest first. + pub fn get_mempool_scripthash_txs(&self, script: &Script) -> Result, Error> { + let script_hash = sha256::Hash::hash(script.as_bytes()); + let path = format!("/scripthash/{script_hash:x}/txs/mempool"); + + self.get_response_json(&path) + } + + /// Get a summary about a [`Block`], given it's [`BlockHash`]. + pub fn get_block(&self, blockhash: &BlockHash) -> Result { + let path = format!("/block/{blockhash}"); + + self.get_response_json(&path) + } + + /// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`]. + pub fn get_block_txids(&self, blockhash: &BlockHash) -> Result, Error> { + let path = format!("/block/{blockhash}/txids"); + + self.get_response_json(&path) + } + + /// Get up to 25 [`Transaction`]s from a [`Block`], given it's [`BlockHash`], + /// beginning at `start_index` (starts from 0 if `start_index` is `None`). + /// + /// The `start_index` value MUST be a multiple of 25, + /// else an error will be returned by Esplora. + pub fn get_block_txs( + &self, + blockhash: &BlockHash, + start_index: Option, + ) -> Result, Error> { + let path = match start_index { + None => format!("/block/{blockhash}/txs"), + Some(start_index) => format!("/block/{blockhash}/txs/{start_index}"), + }; + + self.get_response_json(&path) + } + /// Gets some recent block summaries starting at the tip or at `height` if /// provided. /// @@ -379,7 +455,17 @@ impl BlockingClient { /// Get all UTXOs locked to an address. pub fn get_address_utxos(&self, address: &Address) -> Result, Error> { - self.get_response_json(&format!("/address/{address}/utxo")) + let path = format!("/address/{address}/utxo"); + + self.get_response_json(&path) + } + + /// Get all [`TxOut`]s locked to a [`Script`] hash. + pub fn get_scripthash_utxos(&self, script: &Script) -> Result, Error> { + let script_hash = sha256::Hash::hash(script.as_bytes()); + let path = format!("/scripthash/{script_hash}/utxo"); + + self.get_response_json(&path) } /// Sends a GET request to the given `url`, retrying failed attempts diff --git a/src/lib.rs b/src/lib.rs index 51bc6869..1cd8fc79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -267,6 +267,7 @@ mod test { use electrsd::{corepc_node, ElectrsD}; use lazy_static::lazy_static; use std::env; + use std::str::FromStr; use tokio::sync::Mutex; #[cfg(all(feature = "blocking", feature = "async"))] use { @@ -922,6 +923,79 @@ mod test { assert_eq!(scripthash_txs_txids, scripthash_txs_txids_async); } + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_block() { + let (blocking_client, async_client) = setup_clients().await; + + // Genesis block `BlockHash` on regtest. + let blockhash_genesis = + BlockHash::from_str("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206") + .unwrap(); + + let block_info_blocking = blocking_client.get_block(&blockhash_genesis).unwrap(); + let block_info_async = async_client.get_block(&blockhash_genesis).await.unwrap(); + + assert_eq!(block_info_async, block_info_blocking); + assert_eq!(block_info_async.id, blockhash_genesis); + assert_eq!(block_info_async.height, 0); + assert_eq!(block_info_async.previousblockhash, None); + } + + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_block_txids() { + let (blocking_client, async_client) = setup_clients().await; + + let address = BITCOIND + .client + .new_address_with_type(AddressType::Legacy) + .unwrap(); + + // Create 5 transactions and mine a block. + let txids: Vec<_> = (0..5) + .map(|_| { + BITCOIND + .client + .send_to_address(&address, Amount::from_sat(1000)) + .unwrap() + .txid() + .unwrap() + }) + .collect(); + + let _miner = MINER.lock().await; + generate_blocks_and_wait(1); + + // Get the block hash at the chain's tip. + let blockhash = blocking_client.get_tip_hash().unwrap(); + + let txids_async = async_client.get_block_txids(&blockhash).await.unwrap(); + let txids_blocking = blocking_client.get_block_txids(&blockhash).unwrap(); + + assert_eq!(txids_async, txids_blocking); + + // Compare expected and received (skipping the coinbase TXID). + for expected_txid in txids.iter() { + assert!(txids_async.contains(expected_txid)); + } + } + + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_block_txs() { + let (blocking_client, async_client) = setup_clients().await; + + let _miner = MINER.lock().await; + let blockhash = blocking_client.get_tip_hash().unwrap(); + + let txs_blocking = blocking_client.get_block_txs(&blockhash, None).unwrap(); + let txs_async = async_client.get_block_txs(&blockhash, None).await.unwrap(); + + assert_ne!(txs_blocking.len(), 0); + assert_eq!(txs_blocking.len(), txs_async.len()); + } + #[cfg(all(feature = "blocking", feature = "async"))] #[tokio::test] async fn test_get_blocks() { @@ -1011,6 +1085,142 @@ mod test { assert_eq!(address_stats_async.chain_stats.funded_txo_sum, 1000); } + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_scripthash_stats() { + let (blocking_client, async_client) = setup_clients().await; + + // Create an address of each type. + let address_legacy = BITCOIND + .client + .new_address_with_type(AddressType::Legacy) + .unwrap(); + let address_p2sh_segwit = BITCOIND + .client + .new_address_with_type(AddressType::P2shSegwit) + .unwrap(); + let address_bech32 = BITCOIND + .client + .new_address_with_type(AddressType::Bech32) + .unwrap(); + let address_bech32m = BITCOIND + .client + .new_address_with_type(AddressType::Bech32m) + .unwrap(); + + // Send a transaction to each address. + let _txid = BITCOIND + .client + .send_to_address(&address_legacy, Amount::from_sat(1000)) + .unwrap() + .txid() + .unwrap(); + let _txid = BITCOIND + .client + .send_to_address(&address_p2sh_segwit, Amount::from_sat(1000)) + .unwrap() + .txid() + .unwrap(); + let _txid = BITCOIND + .client + .send_to_address(&address_bech32, Amount::from_sat(1000)) + .unwrap() + .txid() + .unwrap(); + let _txid = BITCOIND + .client + .send_to_address(&address_bech32m, Amount::from_sat(1000)) + .unwrap() + .txid() + .unwrap(); + + let _miner = MINER.lock().await; + generate_blocks_and_wait(1); + + // Derive each addresses script. + let script_legacy = address_legacy.script_pubkey(); + let script_p2sh_segwit = address_p2sh_segwit.script_pubkey(); + let script_bech32 = address_bech32.script_pubkey(); + let script_bech32m = address_bech32m.script_pubkey(); + + // P2PKH + let scripthash_stats_blocking_legacy = blocking_client + .get_scripthash_stats(&script_legacy) + .unwrap(); + let scripthash_stats_async_legacy = async_client + .get_scripthash_stats(&script_legacy) + .await + .unwrap(); + assert_eq!( + scripthash_stats_blocking_legacy, + scripthash_stats_async_legacy + ); + assert_eq!( + scripthash_stats_blocking_legacy.chain_stats.funded_txo_sum, + 1000 + ); + assert_eq!(scripthash_stats_blocking_legacy.chain_stats.tx_count, 1); + + // P2SH-P2WSH + let scripthash_stats_blocking_p2sh_segwit = blocking_client + .get_scripthash_stats(&script_p2sh_segwit) + .unwrap(); + let scripthash_stats_async_p2sh_segwit = async_client + .get_scripthash_stats(&script_p2sh_segwit) + .await + .unwrap(); + assert_eq!( + scripthash_stats_blocking_p2sh_segwit, + scripthash_stats_async_p2sh_segwit + ); + assert_eq!( + scripthash_stats_blocking_p2sh_segwit + .chain_stats + .funded_txo_sum, + 1000 + ); + assert_eq!( + scripthash_stats_blocking_p2sh_segwit.chain_stats.tx_count, + 1 + ); + + // P2WPKH / P2WSH + let scripthash_stats_blocking_bech32 = blocking_client + .get_scripthash_stats(&script_bech32) + .unwrap(); + let scripthash_stats_async_bech32 = async_client + .get_scripthash_stats(&script_bech32) + .await + .unwrap(); + assert_eq!( + scripthash_stats_blocking_bech32, + scripthash_stats_async_bech32 + ); + assert_eq!( + scripthash_stats_blocking_bech32.chain_stats.funded_txo_sum, + 1000 + ); + assert_eq!(scripthash_stats_blocking_bech32.chain_stats.tx_count, 1); + + // P2TR + let scripthash_stats_blocking_bech32m = blocking_client + .get_scripthash_stats(&script_bech32m) + .unwrap(); + let scripthash_stats_async_bech32m = async_client + .get_scripthash_stats(&script_bech32m) + .await + .unwrap(); + assert_eq!( + scripthash_stats_blocking_bech32m, + scripthash_stats_async_bech32m + ); + assert_eq!( + scripthash_stats_blocking_bech32m.chain_stats.funded_txo_sum, + 1000 + ); + assert_eq!(scripthash_stats_blocking_bech32m.chain_stats.tx_count, 1); + } + #[cfg(all(feature = "blocking", feature = "async"))] #[tokio::test] async fn test_get_address_txs() { @@ -1065,4 +1275,127 @@ mod test { assert_ne!(address_utxos_async.len(), 0); assert_eq!(address_utxos_blocking, address_utxos_async); } + + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_scripthash_utxos() { + let (blocking_client, async_client) = setup_clients().await; + + let address = BITCOIND + .client + .new_address_with_type(AddressType::Legacy) + .unwrap(); + let script = address.script_pubkey(); + + let _txid = BITCOIND + .client + .send_to_address(&address, Amount::from_sat(21000)) + .unwrap() + .txid() + .unwrap(); + + let _miner = MINER.lock().await; + generate_blocks_and_wait(1); + + let scripthash_utxos_blocking = blocking_client.get_scripthash_utxos(&script).unwrap(); + let scripthash_utxos_async = async_client.get_scripthash_utxos(&script).await.unwrap(); + + assert_ne!(scripthash_utxos_blocking.len(), 0); + assert_ne!(scripthash_utxos_async.len(), 0); + assert_eq!(scripthash_utxos_blocking, scripthash_utxos_async); + } + + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_tx_outspends() { + let (blocking_client, async_client) = setup_clients().await; + + let address = BITCOIND + .client + .new_address_with_type(AddressType::Legacy) + .unwrap(); + + let txid = BITCOIND + .client + .send_to_address(&address, Amount::from_sat(21000)) + .unwrap() + .txid() + .unwrap(); + + let _miner = MINER.lock().await; + generate_blocks_and_wait(1); + + let outspends_blocking = blocking_client.get_tx_outspends(&txid).unwrap(); + let outspends_async = async_client.get_tx_outspends(&txid).await.unwrap(); + + // Assert that there are 2 outputs: 21K sat and (coinbase - 21K sat). + assert_eq!(outspends_blocking.len(), 2); + assert_eq!(outspends_async.len(), 2); + assert_eq!(outspends_blocking, outspends_async); + + // Assert that both outputs are returned as unspent (spent == false). + assert!(outspends_blocking.iter().all(|output| !output.spent)); + } + + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_mempool_methods() { + let (blocking_client, async_client) = setup_clients().await; + + let address = BITCOIND + .client + .new_address_with_type(AddressType::Legacy) + .unwrap(); + + for _ in 0..5 { + let _txid = BITCOIND + .client + .send_to_address(&address, Amount::from_sat(1000)) + .unwrap() + .txid() + .unwrap(); + } + + // Wait for transactions to propagate to electrs' mempool. + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + + // Test `get_mempool_stats` + let stats_blocking = blocking_client.get_mempool_stats().unwrap(); + let stats_async = async_client.get_mempool_stats().await.unwrap(); + assert_eq!(stats_blocking, stats_async); + assert!(stats_blocking.count >= 5); + + // Test `get_mempool_recent_txs` + let recent_blocking = blocking_client.get_mempool_recent_txs().unwrap(); + let recent_async = async_client.get_mempool_recent_txs().await.unwrap(); + assert_eq!(recent_blocking, recent_async); + assert!(recent_blocking.len() <= 10); + assert!(!recent_blocking.is_empty()); + + // Test `get_mempool_txids` + let txids_blocking = blocking_client.get_mempool_txids().unwrap(); + let txids_async = async_client.get_mempool_txids().await.unwrap(); + assert_eq!(txids_blocking, txids_async); + assert!(txids_blocking.len() >= 5); + + // Test `get_mempool_scripthash_txs` + let script = address.script_pubkey(); + let scripthash_txs_blocking = blocking_client.get_mempool_scripthash_txs(&script).unwrap(); + let scripthash_txs_async = async_client + .get_mempool_scripthash_txs(&script) + .await + .unwrap(); + assert_eq!(scripthash_txs_blocking, scripthash_txs_async); + assert_eq!(scripthash_txs_blocking.len(), 5); + + // Test `get_mempool_address_txs` + let mempool_address_txs_blocking = + blocking_client.get_mempool_address_txs(&address).unwrap(); + let mempool_address_txs_async = async_client + .get_mempool_address_txs(&address) + .await + .unwrap(); + assert_eq!(mempool_address_txs_blocking, mempool_address_txs_async); + assert_eq!(mempool_address_txs_blocking.len(), 5); + } }