Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 3 additions & 27 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,14 @@ use serde::Deserialize;
use std::collections::HashMap;

pub use bitcoin::consensus::{deserialize, serialize};
use bitcoin::hash_types::TxMerkleNode;
pub use bitcoin::hex::FromHex;
pub use bitcoin::{
absolute, block, transaction, Address, Amount, Block, BlockHash, CompactTarget, FeeRate,
OutPoint, Script, ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid, Weight, Witness,
Wtxid,
};

/// Information about a previous output.
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct PrevOut {
/// The value of the previous output, in satoshis.
pub value: u64,
/// The ScriptPubKey that the previous output is locked to, as a [`ScriptBuf`].
pub scriptpubkey: ScriptBuf,
}

/// Information about an input from a [`Transaction`].
/// An input to a [`Transaction`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Vin {
/// The [`Txid`] of the previous [`Transaction`] this input spends from.
Expand All @@ -44,7 +34,7 @@ pub struct Vin {
pub vout: u32,
/// The previous output amount and ScriptPubKey.
/// `None` if this is a coinbase input.
pub prevout: Option<PrevOut>,
pub prevout: Option<Vout>,
/// The ScriptSig authorizes spending this input.
pub scriptsig: ScriptBuf,
/// The Witness that authorizes spending this input, if this is a SegWit spend.
Expand All @@ -56,7 +46,7 @@ pub struct Vin {
pub is_coinbase: bool,
}

/// Information about a [`Transaction`]s output.
/// An output from a [`Transaction`].
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Vout {
/// The value of the output, in satoshis.
Expand Down Expand Up @@ -204,20 +194,6 @@ pub struct BlockTime {
pub height: u32,
}

/// Summary about a [`Block`].
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BlockSummary {
/// The [`Block`]'s hash.
pub id: BlockHash,
/// The [`Block`]'s timestamp and height.
#[serde(flatten)]
pub time: BlockTime,
/// The [`BlockHash`] of the previous [`Block`] (`None` for the genesis [`Block`]).
pub previousblockhash: Option<BlockHash>,
/// The Merkle root of the [`Block`]'s [`Transaction`]s.
pub merkle_root: TxMerkleNode,
}

/// Statistics about an [`Address`].
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct AddressStats {
Expand Down
95 changes: 69 additions & 26 deletions src/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ use log::{debug, error, info, trace};
use reqwest::{header, Body, Client, Response};

use crate::{
AddressStats, BlockInfo, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx,
MempoolStats, MerkleProof, OutputStatus, ScriptHashStats, SubmitPackageResult, Tx, TxStatus,
Utxo, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
AddressStats, BlockInfo, BlockStatus, Builder, Error, MempoolRecentTx, MempoolStats,
MerkleProof, OutputStatus, ScriptHashStats, SubmitPackageResult, Tx, TxStatus, Utxo,
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
};

/// An async client for interacting with an Esplora API server.
Expand Down Expand Up @@ -476,11 +476,11 @@ impl<S: Sleeper> AsyncClient<S> {
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.
pub async fn scripthash_txs(
/// Get transaction history for the specified [`Script`] hash, sorted by newest first.
///
/// Returns 25 transactions per page. More can be requested by
/// specifying the last [`Txid`] seen in the previous query.
pub async fn get_script_hash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
Expand Down Expand Up @@ -533,6 +533,25 @@ impl<S: Sleeper> AsyncClient<S> {
self.get_response_json(&path).await
}

/// Get [block summaries](BlockInfo) for recent blocks:
/// - If `height` is `None`: from the tip
/// - If `height is `Some(height)`: from `height`
///
/// The maximum number of [block summaries](BlockInfo) returned depends on the backend:
/// - Esplora returns 10
/// - [Mempool.space](https://mempool.space/docs/api/rest#get-blocks) returns 10
Comment thread
luisschwab marked this conversation as resolved.
Outdated
pub async fn get_block_infos(&self, height: Option<u32>) -> Result<Vec<BlockInfo>, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
None => "/blocks".to_string(),
};
let block_infos: Vec<BlockInfo> = self.get_response_json(&path).await?;
if block_infos.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(block_infos)
}

/// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`].
pub async fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
let path = format!("/block/{blockhash}/txids");
Expand All @@ -558,31 +577,55 @@ impl<S: Sleeper> AsyncClient<S> {
self.get_response_json(&path).await
}

/// Gets some recent block summaries starting at the tip or at `height` if
/// provided.
///
/// The maximum number of summaries returned depends on the backend itself:
/// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
pub async fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
None => "/blocks".to_string(),
};
let blocks: Vec<BlockSummary> = self.get_response_json(&path).await?;
if blocks.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(blocks)
}

/// Get all UTXOs locked to an address.
pub async fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
let path = format!("/address/{address}/utxo");

self.get_response_json(&path).await
}

/// Get all [`Utxo`]s locked to a [`Script`].
/// Get unconfirmed mempool [`EsploraTx`]s for an [`Address`], sorted newest first.
pub async fn get_mempool_address_txs(
&self,
address: &Address,
) -> Result<Vec<EsploraTx>, Error> {
let path = format!("/address/{address}/txs/mempool");

self.get_response_json(&path).await
}

// ----> SCRIPT HASH

/// Get statistics about a [`Script`] hash's confirmed and mempool transactions.
///
/// Returns a [`ScriptHashStats`] containing
/// [transaction summaries](crate::api::AddressTxsSummary)
/// for the SHA256 hash of the given [`Script`].
pub async fn get_scripthash_stats(&self, script: &Script) -> Result<ScriptHashStats, Error> {
Comment thread
luisschwab marked this conversation as resolved.
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}");
self.get_response_json(&path).await
}

/// Get confirmed transaction history for a [`Script`] hash, sorted newest first.
///
/// Returns 25 transactions per page. To paginate, pass the [`Txid`] of the
/// last transaction seen in the previous response as `last_seen`.
pub async fn get_scripthash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
) -> Result<Vec<EsploraTx>, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = match last_seen {
Some(last_seen) => format!("/scripthash/{script_hash:x}/txs/chain/{last_seen}"),
None => format!("/scripthash/{script_hash:x}/txs"),
};

self.get_response_json(&path).await
}
Comment thread
luisschwab marked this conversation as resolved.

/// Get all confirmed [`Utxo`]s locked to the given [`Script`].
pub async fn get_scripthash_utxos(&self, script: &Script) -> Result<Vec<Utxo>, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes());
let path = format!("/scripthash/{script_hash}/utxo");
Expand Down
53 changes: 28 additions & 25 deletions src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ use bitcoin::hex::{DisplayHex, FromHex};
use bitcoin::{Address, Block, BlockHash, MerkleBlock, Script, Transaction, Txid};

use crate::{
AddressStats, BlockInfo, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx,
MempoolStats, MerkleProof, OutputStatus, ScriptHashStats, SubmitPackageResult, Tx, TxStatus,
Utxo, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
AddressStats, BlockInfo, BlockStatus, Builder, Error, MempoolRecentTx, MempoolStats,
MerkleProof, OutputStatus, ScriptHashStats, SubmitPackageResult, Tx, TxStatus, Utxo,
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
};

/// A blocking client for interacting with an Esplora API server.
Expand Down Expand Up @@ -438,11 +438,11 @@ impl BlockingClient {
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.
pub fn scripthash_txs(
/// Get transaction history for the specified [`Script`] hash, sorted by newest first.
///
/// Returns 25 transactions per page. More can be requested by
/// specifying the last [`Txid`] seen in the previous query.
pub fn get_script_hash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
Expand All @@ -452,6 +452,7 @@ impl BlockingClient {
Some(last_seen) => format!("/scripthash/{script_hash:x}/txs/chain/{last_seen}"),
None => format!("/scripthash/{script_hash:x}/txs"),
};

self.get_response_json(&path)
}

Expand All @@ -471,6 +472,25 @@ impl BlockingClient {
self.get_response_json(&path)
}

/// Get [block summaries](BlockInfo) for recent blocks:
/// - If `height` is `None`: from the tip
/// - If `height is `Some(height)`: from `height`
///
/// The maximum number of [block summaries](BlockInfo) returned depends on the backend:
/// - Esplora returns 10
/// - [Mempool.space](https://mempool.space/docs/api/rest#get-blocks) returns 10
pub fn get_block_infos(&self, height: Option<u32>) -> Result<Vec<BlockInfo>, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
None => "/blocks".to_string(),
};
let block_infos: Vec<BlockInfo> = self.get_response_json(&path)?;
if block_infos.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(block_infos)
}

/// Get all [`Txid`]s that belong to a [`Block`] identified by it's [`BlockHash`].
pub fn get_block_txids(&self, blockhash: &BlockHash) -> Result<Vec<Txid>, Error> {
let path = format!("/block/{blockhash}/txids");
Expand All @@ -496,23 +516,6 @@ impl BlockingClient {
self.get_response_json(&path)
}

/// Gets some recent block summaries starting at the tip or at `height` if
/// provided.
///
/// The maximum number of summaries returned depends on the backend itself:
/// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`.
pub fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
let path = match height {
Some(height) => format!("/blocks/{height}"),
None => "/blocks".to_string(),
};
let blocks: Vec<BlockSummary> = self.get_response_json(&path)?;
if blocks.is_empty() {
return Err(Error::InvalidResponse);
}
Ok(blocks)
}

/// Get all UTXOs locked to an address.
pub fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
let path = format!("/address/{address}/utxo");
Expand Down
34 changes: 17 additions & 17 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ mod test {

#[cfg(all(feature = "blocking", feature = "async"))]
#[tokio::test]
async fn test_scripthash_txs() {
async fn test_get_script_hash_txs() {
let env = TestEnv::new();
let (blocking_client, async_client) = env.setup_clients();

Expand All @@ -973,20 +973,20 @@ mod test {
.unwrap()
.tx;
let script = &expected_tx.output[0].script_pubkey;
let scripthash_txs_txids: Vec<Txid> = blocking_client
.scripthash_txs(script, None)
let script_hash_txs_txids_blocking: Vec<Txid> = blocking_client
.get_script_hash_txs(script, None)
.unwrap()
.iter()
.map(|tx| tx.txid)
.collect();
let scripthash_txs_txids_async: Vec<Txid> = async_client
.scripthash_txs(script, None)
let script_hash_txs_txids_async: Vec<Txid> = async_client
.get_scripthash_txs(script, None)
.await
.unwrap()
.iter()
.map(|tx| tx.txid)
.collect();
assert_eq!(scripthash_txs_txids, scripthash_txs_txids_async);
assert_eq!(script_hash_txs_txids_blocking, script_hash_txs_txids_async);
}

#[cfg(all(feature = "blocking", feature = "async"))]
Expand Down Expand Up @@ -1063,35 +1063,35 @@ mod test {

#[cfg(all(feature = "blocking", feature = "async"))]
#[tokio::test]
async fn test_get_blocks() {
async fn test_get_block_infos() {
let env = TestEnv::new();
let (blocking_client, async_client) = env.setup_clients();

let start_height = env.bitcoind_client().get_block_count().unwrap().0;
let blocks1 = blocking_client.get_blocks(None).unwrap();
let blocks_async1 = async_client.get_blocks(None).await.unwrap();
assert_eq!(blocks1[0].time.height, start_height as u32);
let blocks1 = blocking_client.get_block_infos(None).unwrap();
let blocks_async1 = async_client.get_block_infos(None).await.unwrap();
assert_eq!(blocks1[0].height, start_height as u32);
assert_eq!(blocks1, blocks_async1);
env.mine_and_wait(1);

let blocks2 = blocking_client.get_blocks(None).unwrap();
let blocks_async2 = async_client.get_blocks(None).await.unwrap();
let blocks2 = blocking_client.get_block_infos(None).unwrap();
let blocks_async2 = async_client.get_block_infos(None).await.unwrap();
assert_eq!(blocks2, blocks_async2);
assert_ne!(blocks2, blocks1);

let blocks3 = blocking_client
.get_blocks(Some(start_height as u32))
.get_block_infos(Some(start_height as u32))
.unwrap();
let blocks_async3 = async_client
.get_blocks(Some(start_height as u32))
.get_block_infos(Some(start_height as u32))
.await
.unwrap();
assert_eq!(blocks3, blocks_async3);
assert_eq!(blocks3[0].time.height, start_height as u32);
assert_eq!(blocks3[0].height, start_height as u32);
assert_eq!(blocks3, blocks1);

let blocks_genesis = blocking_client.get_blocks(Some(0)).unwrap();
let blocks_genesis_async = async_client.get_blocks(Some(0)).await.unwrap();
let blocks_genesis = blocking_client.get_block_infos(Some(0)).unwrap();
let blocks_genesis_async = async_client.get_block_infos(Some(0)).await.unwrap();
assert_eq!(blocks_genesis, blocks_genesis_async);
}

Expand Down