Skip to content

Commit 9b6bf23

Browse files
committed
feat(api): add /address/:address and /address/:address/txs endpoints
1 parent 1f4acad commit 9b6bf23

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

src/api.rs

+16
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ pub struct BlockSummary {
9797
pub merkle_root: bitcoin::hash_types::TxMerkleNode,
9898
}
9999

100+
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
101+
pub struct AddressStats {
102+
pub address: String,
103+
pub chain_stats: AddressTxsSummary,
104+
pub mempool_stats: AddressTxsSummary,
105+
}
106+
107+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
108+
pub struct AddressTxsSummary {
109+
pub funded_txo_count: u32,
110+
pub funded_txo_sum: u64,
111+
pub spent_txo_count: u32,
112+
pub spent_txo_sum: u64,
113+
pub tx_count: u32,
114+
}
115+
100116
impl Tx {
101117
pub fn to_tx(&self) -> Transaction {
102118
Transaction {

src/async.rs

+25
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::str::FromStr;
1818
use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable};
1919
use bitcoin::hashes::{sha256, Hash};
2020
use bitcoin::hex::{DisplayHex, FromHex};
21+
use bitcoin::Address;
2122
use bitcoin::{
2223
block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid,
2324
};
@@ -27,6 +28,7 @@ use log::{debug, error, info, trace};
2728

2829
use reqwest::{header, Client, Response};
2930

31+
use crate::api::AddressStats;
3032
use crate::{
3133
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
3234
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
@@ -378,6 +380,29 @@ impl AsyncClient {
378380
.map(|block_hash| BlockHash::from_str(&block_hash).map_err(Error::HexToArray))?
379381
}
380382

383+
/// Get information about a specific address, includes confirmed balance and transactions in
384+
/// the mempool.
385+
pub async fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
386+
let path = format!("/address/{address}");
387+
self.get_response_json(&path).await
388+
}
389+
390+
/// Get confirmed transaction history for the specified address, sorted with newest first.
391+
/// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
392+
/// More can be requested by specifying the last txid seen by the previous query.
393+
pub async fn get_address_txs(
394+
&self,
395+
address: &Address,
396+
last_seen: Option<Txid>,
397+
) -> Result<Vec<Tx>, Error> {
398+
let path = match last_seen {
399+
Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"),
400+
None => format!("/address/{address}/txs"),
401+
};
402+
403+
self.get_response_json(&path).await
404+
}
405+
381406
/// Get confirmed transaction history for the specified address/scripthash,
382407
/// sorted with newest first. Returns 25 transactions per page.
383408
/// More can be requested by specifying the last txid seen by the previous

src/blocking.rs

+25
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::convert::TryFrom;
1616
use std::str::FromStr;
1717
use std::thread;
1818

19+
use bitcoin::Address;
1920
#[allow(unused_imports)]
2021
use log::{debug, error, info, trace};
2122

@@ -28,6 +29,7 @@ use bitcoin::{
2829
block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid,
2930
};
3031

32+
use crate::api::AddressStats;
3133
use crate::{
3234
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
3335
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
@@ -317,6 +319,29 @@ impl BlockingClient {
317319
self.get_response_json("/fee-estimates")
318320
}
319321

322+
/// Get information about a specific address, includes confirmed balance and transactions in
323+
/// the mempool.
324+
pub fn get_address_stats(&self, address: &Address) -> Result<AddressStats, Error> {
325+
let path = format!("/address/{address}");
326+
self.get_response_json(&path)
327+
}
328+
329+
/// Get transaction history for the specified address/scripthash, sorted with newest first.
330+
/// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
331+
/// More can be requested by specifying the last txid seen by the previous query.
332+
pub fn get_address_txs(
333+
&self,
334+
address: &Address,
335+
last_seen: Option<Txid>,
336+
) -> Result<Vec<Tx>, Error> {
337+
let path = match last_seen {
338+
Some(last_seen) => format!("/address/{address}/txs/chain/{last_seen}"),
339+
None => format!("/address/{address}/txs"),
340+
};
341+
342+
self.get_response_json(&path)
343+
}
344+
320345
/// Get confirmed transaction history for the specified address/scripthash,
321346
/// sorted with newest first. Returns 25 transactions per page.
322347
/// More can be requested by specifying the last txid seen by the previous

src/lib.rs

+75
Original file line numberDiff line numberDiff line change
@@ -992,4 +992,79 @@ mod test {
992992
let tx_async = async_client.get_tx(&txid).await.unwrap();
993993
assert_eq!(tx, tx_async);
994994
}
995+
996+
#[cfg(all(feature = "blocking", feature = "async"))]
997+
#[tokio::test]
998+
async fn test_get_address_stats() {
999+
let (blocking_client, async_client) = setup_clients().await;
1000+
1001+
let address = BITCOIND
1002+
.client
1003+
.get_new_address(Some("test"), Some(AddressType::Legacy))
1004+
.unwrap()
1005+
.assume_checked();
1006+
1007+
let _txid = BITCOIND
1008+
.client
1009+
.send_to_address(
1010+
&address,
1011+
Amount::from_sat(1000),
1012+
None,
1013+
None,
1014+
None,
1015+
None,
1016+
None,
1017+
None,
1018+
)
1019+
.unwrap();
1020+
1021+
let address_blocking = blocking_client.get_address_stats(&address).unwrap();
1022+
let address_async = async_client.get_address_stats(&address).await.unwrap();
1023+
assert_eq!(address_blocking, address_async);
1024+
assert_eq!(address_async.chain_stats.funded_txo_count, 0);
1025+
1026+
let _miner = MINER.lock().await;
1027+
generate_blocks_and_wait(1);
1028+
1029+
let address_blocking = blocking_client.get_address_stats(&address).unwrap();
1030+
let address_async = async_client.get_address_stats(&address).await.unwrap();
1031+
assert_eq!(address_blocking, address_async);
1032+
assert_eq!(address_async.chain_stats.funded_txo_count, 1);
1033+
assert_eq!(address_async.chain_stats.funded_txo_sum, 1000);
1034+
}
1035+
1036+
#[cfg(all(feature = "blocking", feature = "async"))]
1037+
#[tokio::test]
1038+
async fn test_get_address_txs() {
1039+
let (blocking_client, async_client) = setup_clients().await;
1040+
1041+
let address = BITCOIND
1042+
.client
1043+
.get_new_address(Some("test"), Some(AddressType::Legacy))
1044+
.unwrap()
1045+
.assume_checked();
1046+
1047+
let txid = BITCOIND
1048+
.client
1049+
.send_to_address(
1050+
&address,
1051+
Amount::from_sat(1000),
1052+
None,
1053+
None,
1054+
None,
1055+
None,
1056+
None,
1057+
None,
1058+
)
1059+
.unwrap();
1060+
1061+
let _miner = MINER.lock().await;
1062+
generate_blocks_and_wait(1);
1063+
1064+
let address_txs_blocking = blocking_client.get_address_txs(&address, None).unwrap();
1065+
let address_txs_async = async_client.get_address_txs(&address, None).await.unwrap();
1066+
1067+
assert_eq!(address_txs_blocking, address_txs_async);
1068+
assert_eq!(address_txs_async[0].txid, txid);
1069+
}
9951070
}

0 commit comments

Comments
 (0)