Skip to content

Commit

Permalink
Merge bitcoindevkit#104: Add get_address_stats and `get_address_txn…
Browse files Browse the repository at this point in the history
…s` endpoints

3fdd66c feat(api): add `/address/:address` and `/address/:address/txs` endpoints (Praveen Perera)

Pull request description:

ACKs for top commit:
  oleonardolima:
    tACK 3fdd66c
  ValuedMammal:
    ACK 3fdd66c

Tree-SHA512: b27f06fde5191d4246b075e4a44fbd41310e60f92b4ece2ff8b794ed86e0de6e6e85d982bb969d69f677f72832ff8f2c7963b85607bc4814a5974e19d7e40c9c
  • Loading branch information
ValuedMammal committed Nov 17, 2024
2 parents 8a9edce + 3fdd66c commit 9fcaf01
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 0 deletions.
26 changes: 26 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions src/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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,
Expand Down Expand Up @@ -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<AddressStats, Error> {
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<Txid>,
) -> Result<Vec<Tx>, 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
Expand Down
26 changes: 26 additions & 0 deletions src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<AddressStats, Error> {
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<Txid>,
) -> Result<Vec<Tx>, 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
Expand Down
75 changes: 75 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit 9fcaf01

Please sign in to comment.