Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
62 changes: 23 additions & 39 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,11 +46,12 @@ 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.
pub value: u64,
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub value: Amount,
/// The ScriptPubKey that the output is locked to, as a [`ScriptBuf`].
pub scriptpubkey: ScriptBuf,
}
Expand Down Expand Up @@ -139,7 +130,8 @@ pub struct Tx {
/// The confirmation status of the [`Transaction`].
pub status: TxStatus,
/// The fee amount paid by the [`Transaction`], in satoshis.
pub fee: u64,
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub fee: Amount,
}

/// Information about a bitcoin [`Block`].
Expand Down Expand Up @@ -204,20 +196,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 All @@ -235,11 +213,13 @@ pub struct AddressTxsSummary {
/// The number of funded [`TxOut`]s.
pub funded_txo_count: u32,
/// The sum of the funded [`TxOut`]s, in satoshis.
pub funded_txo_sum: u64,
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub funded_txo_sum: Amount,
/// The number of spent [`TxOut`]s.
pub spent_txo_count: u32,
/// The sum of the spent [`TxOut`]s, in satoshis.
pub spent_txo_sum: u64,
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub spent_txo_sum: Amount,
/// The total number of [`Transaction`]s.
pub tx_count: u32,
}
Expand Down Expand Up @@ -280,6 +260,7 @@ pub struct Utxo {
/// The confirmation status of the [`TxOut`].
pub status: UtxoStatus,
/// The value of the [`TxOut`] as an [`Amount`].
Comment thread
oleonardolima marked this conversation as resolved.
Outdated
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub value: Amount,
}

Expand All @@ -291,7 +272,8 @@ pub struct MempoolStats {
/// The total size of mempool [`Transaction`]s, in virtual bytes.
pub vsize: usize,
/// The total fee paid by mempool [`Transaction`]s, in satoshis.
pub total_fee: u64,
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub total_fee: Amount,
/// The mempool's fee rate distribution histogram.
///
/// An array of `(feerate, vsize)` tuples, where each entry's `vsize` is the total vsize
Expand All @@ -306,11 +288,13 @@ pub struct MempoolRecentTx {
/// The [`Transaction`]'s ID, as a [`Txid`].
pub txid: Txid,
/// The [`Amount`] of fees paid by the transaction, in satoshis.
pub fee: u64,
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub fee: Amount,
/// The [`Transaction`]'s size, in virtual bytes.
pub vsize: usize,
/// Combined [`Amount`] of the [`Transaction`], in satoshis.
pub value: u64,
#[serde(with = "bitcoin::amount::serde::as_sat")]
pub value: Amount,
}

/// The result for a broadcasted package of [`Transaction`]s.
Expand Down Expand Up @@ -394,7 +378,7 @@ impl Tx {
.iter()
.cloned()
.map(|vout| TxOut {
value: Amount::from_sat(vout.value),
value: vout.value,
script_pubkey: vout.scriptpubkey,
})
.collect(),
Expand All @@ -420,9 +404,9 @@ impl Tx {
.iter()
.cloned()
.map(|vin| {
vin.prevout.map(|po| TxOut {
script_pubkey: po.scriptpubkey,
value: Amount::from_sat(po.value),
vin.prevout.map(|prevout| TxOut {
script_pubkey: prevout.scriptpubkey,
value: prevout.value,
})
})
.collect()
Expand All @@ -435,7 +419,7 @@ impl Tx {

/// Get the fee paid by a [`Tx`].
pub fn fee(&self) -> Amount {
Amount::from_sat(self.fee)
self.fee
}
}

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
Loading