Skip to content

Commit

Permalink
feat(api): add /address/:address and /address/:address/txs endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
praveenperera committed Oct 15, 2024
1 parent 1f4acad commit 69caadd
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 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,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<AddressStats, Error> {
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<Txid>,
) -> Result<Vec<Tx>, Error> {
let path = match last_seen {
Some(after_txid) => format!("/address/{address}/txs/chain/{after_txid}"),
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
25 changes: 25 additions & 0 deletions src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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,
Expand Down Expand Up @@ -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<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(after_txid) => format!("/address/{address}/txs/chain/{after_txid}"),
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_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);
}
}

0 comments on commit 69caadd

Please sign in to comment.