From 75a5f7b27b0c9e587562c4bd14c616eb6ed6e373 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Sun, 13 Oct 2024 20:15:52 -0500 Subject: [PATCH] feat(api): add `/address/:address` and `/address/:address/txs` endpoints Co-Authored-By: Leonardo L. --- src/api.rs | 16 +++++++++++ src/async.rs | 25 +++++++++++++++++ src/blocking.rs | 25 +++++++++++++++++ src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+) diff --git a/src/api.rs b/src/api.rs index 1d30bb6..55d4e00 100644 --- a/src/api.rs +++ b/src/api.rs @@ -97,6 +97,22 @@ pub struct BlockSummary { pub merkle_root: bitcoin::hash_types::TxMerkleNode, } +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct AddressStats { + pub address: String, + pub chain_stats: AddressTxsSummary, + pub mempool_stats: AddressTxsSummary, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] +pub struct AddressTxsSummary { + pub funded_txo_count: u32, + pub funded_txo_sum: u64, + pub spent_txo_count: u32, + pub spent_txo_sum: u64, + 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..45695f4 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,29 @@ 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 confirmed 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. + 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..eb68417 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -16,6 +16,7 @@ use std::convert::TryFrom; use std::str::FromStr; use std::thread; +use bitcoin::Address; #[allow(unused_imports)] use log::{debug, error, info, trace}; @@ -28,6 +29,7 @@ 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,29 @@ 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..943b277 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_blocking = blocking_client.get_address_stats(&address).unwrap(); + let address_async = async_client.get_address_stats(&address).await.unwrap(); + assert_eq!(address_blocking, address_async); + assert_eq!(address_async.chain_stats.funded_txo_count, 0); + + let _miner = MINER.lock().await; + generate_blocks_and_wait(1); + + let address_blocking = blocking_client.get_address_stats(&address).unwrap(); + let address_async = async_client.get_address_stats(&address).await.unwrap(); + assert_eq!(address_blocking, address_async); + assert_eq!(address_async.chain_stats.funded_txo_count, 1); + assert_eq!(address_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); + } }