Skip to content

feat: add get_address_utxos method #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@ pub struct AddressTxsSummary {
pub tx_count: u32,
}

/// Information about an UTXO's status: confirmation status,
/// confirmation height, confirmation block hash and confirmation block time.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub struct UtxoStatus {
Comment on lines +126 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding docs here. Question: isn't this the same as the TxStatus type?

Copy link
Member Author

@luisschwab luisschwab Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, structurally they are the same. But I preferred to create another struct with a name that matches what it models. We could change the name of TxStatus to something generic, but I don't think that's very elegant, since these types are ad-hoc to what the API returns.

/// Whether or not the UTXO is confirmed.
pub confirmed: bool,
/// The block height in which the UTXO was confirmed.
pub block_height: Option<u32>,
/// The block hash in which the UTXO was confirmed.
pub block_hash: Option<BlockHash>,
/// The UNIX timestamp in which the UTXO was confirmed.
pub block_time: Option<u64>,
}

/// Information about an UTXO's outpoint, confirmation status and value.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub struct Utxo {
/// The [`Txid`] of the transaction that created the UTXO.
pub txid: Txid,
/// The output index of the UTXO on the transaction that created the it.
pub vout: u32,
/// The confirmation status of the UTXO.
pub status: UtxoStatus,
/// The value of the UTXO as an [`Amount`].
pub value: Amount,
}

impl Tx {
pub fn to_tx(&self) -> Transaction {
Transaction {
Expand Down
8 changes: 7 additions & 1 deletion src/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use reqwest::{header, Client, Response};

use crate::api::AddressStats;
use crate::{
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus, Utxo,
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
};

Expand Down Expand Up @@ -449,6 +449,12 @@ impl<S: Sleeper> AsyncClient<S> {
Ok(blocks)
}

/// Get all UTXOs locked to an address.
pub async fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
self.get_response_json(&format!("/address/{address}/utxo"))
.await
}

/// Get the underlying base URL.
pub fn url(&self) -> &str {
&self.url
Expand Down
7 changes: 6 additions & 1 deletion src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use bitcoin::{

use crate::api::AddressStats;
use crate::{
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus, Utxo,
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
};

Expand Down Expand Up @@ -377,6 +377,11 @@ impl BlockingClient {
Ok(blocks)
}

/// Get all UTXOs locked to an address.
pub fn get_address_utxos(&self, address: &Address) -> Result<Vec<Utxo>, Error> {
self.get_response_json(&format!("/address/{address}/utxo"))
}

/// Sends a GET request to the given `url`, retrying failed attempts
/// for retryable error codes until max retries hit.
fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
Expand Down
28 changes: 28 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,4 +1037,32 @@ mod test {
assert_eq!(address_txs_blocking, address_txs_async);
assert_eq!(address_txs_async[0].txid, txid);
}

#[cfg(all(feature = "blocking", feature = "async"))]
#[tokio::test]
async fn test_get_address_utxos() {
let (blocking_client, async_client) = setup_clients().await;

let address = BITCOIND
.client
.new_address_with_type(AddressType::Legacy)
.unwrap();

let _txid = BITCOIND
.client
.send_to_address(&address, Amount::from_sat(21000))
.unwrap()
.txid()
.unwrap();

let _miner = MINER.lock().await;
generate_blocks_and_wait(1);

let address_utxos_blocking = blocking_client.get_address_utxos(&address).unwrap();
let address_utxos_async = async_client.get_address_utxos(&address).await.unwrap();

assert_ne!(address_utxos_blocking.len(), 0);
assert_ne!(address_utxos_async.len(), 0);
assert_eq!(address_utxos_blocking, address_utxos_async);
Comment on lines +1064 to +1066
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also add assertions for each field.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so? Wouldn't deserialization to the Utxo type fail if the values were incorrect?

}
}
Loading