diff --git a/src/api.rs b/src/api.rs index 1d30bb6..296835c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -97,6 +97,32 @@ pub struct BlockSummary { pub merkle_root: bitcoin::hash_types::TxMerkleNode, } +/// Address statistics, includes the address, and the utxo information for the address. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct AddressStats { + /// The address. + pub address: String, + /// The summary of transactions for this address, already on chain. + pub chain_stats: AddressTxsSummary, + /// The summary of transactions for this address, currently in the mempool. + pub mempool_stats: AddressTxsSummary, +} + +/// Contains a summary of the transactions for an address. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] +pub struct AddressTxsSummary { + /// The number of funded transaction outputs. + pub funded_txo_count: u32, + /// The sum of the funded transaction outputs, in satoshis. + pub funded_txo_sum: u64, + /// The number of spent transaction outputs. + pub spent_txo_count: u32, + /// The sum of the spent transaction outputs, in satoshis. + pub spent_txo_sum: u64, + /// The total number of transactions. + pub tx_count: u32, +} + impl Tx { pub fn to_tx(&self) -> Transaction { Transaction { diff --git a/src/async.rs b/src/async.rs index 93e4449..73bf386 100644 --- a/src/async.rs +++ b/src/async.rs @@ -18,6 +18,7 @@ use std::str::FromStr; 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, }; @@ -27,6 +28,7 @@ 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, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES, @@ -378,6 +380,30 @@ impl AsyncClient { .map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))? } + /// Get information about a specific address, includes confirmed balance and transactions in + /// the mempool. + pub async fn get_address_stats(&self, address: &Address) -> Result { + let path = format!("/address/{address}"); + self.get_response_json(&path).await + } + + /// Get transaction history for the specified address/scripthash, 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. + pub async fn get_address_txs( + &self, + address: &Address, + last_seen: Option, + ) -> Result, Error> { + let path = match last_seen { + Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"), + None => format!("/address/{address}/txs"), + }; + + self.get_response_json(&path).await + } + /// Get confirmed 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 diff --git a/src/blocking.rs b/src/blocking.rs index dc86a38..80de41b 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -24,10 +24,12 @@ use minreq::{Proxy, Request, Response}; 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 crate::api::AddressStats; use crate::{ BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus, BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES, @@ -317,6 +319,30 @@ impl BlockingClient { self.get_response_json("/fee-estimates") } + /// Get information about a specific address, includes confirmed balance and transactions in + /// the mempool. + pub fn get_address_stats(&self, address: &Address) -> Result { + let path = format!("/address/{address}"); + self.get_response_json(&path) + } + + /// Get transaction history for the specified address/scripthash, 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. + pub fn get_address_txs( + &self, + address: &Address, + last_seen: Option, + ) -> Result, Error> { + let path = match last_seen { + Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"), + None => format!("/address/{address}/txs"), + }; + + self.get_response_json(&path) + } + /// Get confirmed 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 diff --git a/src/lib.rs b/src/lib.rs index 75b4730..743f118 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -992,4 +992,79 @@ mod test { let tx_async = async_client.get_tx(&txid).await.unwrap(); assert_eq!(tx, tx_async); } + + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_address_stats() { + let (blocking_client, async_client) = setup_clients().await; + + let address = BITCOIND + .client + .get_new_address(Some("test"), Some(AddressType::Legacy)) + .unwrap() + .assume_checked(); + + let _txid = BITCOIND + .client + .send_to_address( + &address, + Amount::from_sat(1000), + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + + let address_stats_blocking = blocking_client.get_address_stats(&address).unwrap(); + let address_stats_async = async_client.get_address_stats(&address).await.unwrap(); + assert_eq!(address_stats_blocking, address_stats_async); + assert_eq!(address_stats_async.chain_stats.funded_txo_count, 0); + + let _miner = MINER.lock().await; + generate_blocks_and_wait(1); + + let address_stats_blocking = blocking_client.get_address_stats(&address).unwrap(); + let address_stats_async = async_client.get_address_stats(&address).await.unwrap(); + assert_eq!(address_stats_blocking, address_stats_async); + assert_eq!(address_stats_async.chain_stats.funded_txo_count, 1); + assert_eq!(address_stats_async.chain_stats.funded_txo_sum, 1000); + } + + #[cfg(all(feature = "blocking", feature = "async"))] + #[tokio::test] + async fn test_get_address_txs() { + let (blocking_client, async_client) = setup_clients().await; + + let address = BITCOIND + .client + .get_new_address(Some("test"), Some(AddressType::Legacy)) + .unwrap() + .assume_checked(); + + let txid = BITCOIND + .client + .send_to_address( + &address, + Amount::from_sat(1000), + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + + let _miner = MINER.lock().await; + generate_blocks_and_wait(1); + + let address_txs_blocking = blocking_client.get_address_txs(&address, None).unwrap(); + let address_txs_async = async_client.get_address_txs(&address, None).await.unwrap(); + + assert_eq!(address_txs_blocking, address_txs_async); + assert_eq!(address_txs_async[0].txid, txid); + } }