Skip to content

Commit 1d6bc57

Browse files
committed
feat(client): add get_block_txs
1 parent 3a054b9 commit 1d6bc57

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

src/async.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,39 @@ impl<S: Sleeper> AsyncClient<S> {
473473
self.get_response_json(&path).await
474474
}
475475

476+
/// Get up to 25 [`Transaction`]s from a [`Block`], given it's [`BlockHash`],
477+
/// beginning at `start_index` (starts from 0 if `start_index` is `None`).
478+
///
479+
/// The `start_index` value MUST be a multiple of 25,
480+
/// even though this is not documented on the Esplora specification.
481+
pub async fn get_block_txs(
482+
&self,
483+
blockhash: BlockHash,
484+
start_index: Option<u32>,
485+
) -> Result<Vec<Transaction>, Error> {
486+
// Check that `start_index` is a multiple of 25.
487+
if let Some(idx) = start_index {
488+
if idx % 25 != 0 {
489+
return Err(Error::InvalidStartIndexValue);
490+
}
491+
}
492+
493+
let path = match start_index {
494+
None => format!("/block/{blockhash}/txs"),
495+
Some(idx) => format!("/block/{blockhash}/txs/{idx}"),
496+
};
497+
498+
let esplora_txs: Vec<Tx> = self.get_response_json(&path).await?;
499+
500+
// Convert Esplora [`Tx`]s into [`Transaction`]s.
501+
let txs: Vec<Transaction> = esplora_txs
502+
.into_iter()
503+
.map(|esplora_tx| esplora_tx.to_tx())
504+
.collect();
505+
506+
Ok(txs)
507+
}
508+
476509
/// Gets some recent block summaries starting at the tip or at `height` if
477510
/// provided.
478511
///

src/blocking.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,39 @@ impl BlockingClient {
401401
self.get_response_json(&path)
402402
}
403403

404+
/// Get up to 25 [`Transaction`]s from a [`Block`], given it's [`BlockHash`],
405+
/// beginning at `start_index` (starts from 0 if `start_index` is `None`).
406+
///
407+
/// The `start_index` value MUST be a multiple of 25,
408+
/// even though this is not documented on the Esplora specification.
409+
pub fn get_block_txs(
410+
&self,
411+
blockhash: BlockHash,
412+
start_index: Option<u32>,
413+
) -> Result<Vec<Transaction>, Error> {
414+
// Check that `start_index` is a multiple of 25.
415+
if let Some(idx) = start_index {
416+
if idx % 25 != 0 {
417+
return Err(Error::InvalidStartIndexValue);
418+
}
419+
}
420+
421+
let path = match start_index {
422+
None => format!("/block/{blockhash}/txs"),
423+
Some(idx) => format!("/block/{blockhash}/txs/{idx}"),
424+
};
425+
426+
let esplora_txs: Vec<Tx> = self.get_response_json(&path)?;
427+
428+
// Convert Esplora [`Tx`]s into [`Transaction`]s.
429+
let txs: Vec<Transaction> = esplora_txs
430+
.into_iter()
431+
.map(|esplora_tx| esplora_tx.to_tx())
432+
.collect();
433+
434+
Ok(txs)
435+
}
436+
404437
/// Gets some recent block summaries starting at the tip or at `height` if
405438
/// provided.
406439
///

src/lib.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ mod test {
268268
use super::*;
269269
use electrsd::{corepc_node, ElectrsD};
270270
use lazy_static::lazy_static;
271+
use std::collections::HashSet;
271272
use std::env;
272273
use std::str::FromStr;
273274
use tokio::sync::Mutex;
@@ -1030,6 +1031,64 @@ mod test {
10301031
});
10311032
}
10321033

1034+
#[cfg(all(feature = "blocking", feature = "async"))]
1035+
#[tokio::test]
1036+
async fn test_get_block_txs() {
1037+
let (blocking_client, async_client) = setup_clients().await;
1038+
let address = BITCOIND
1039+
.client
1040+
.new_address_with_type(AddressType::Legacy)
1041+
.unwrap();
1042+
// Create 30 transactions and mine a block.
1043+
let mut mined_txids = Vec::new();
1044+
for _ in 0..30 {
1045+
let txid = BITCOIND
1046+
.client
1047+
.send_to_address(&address, Amount::from_sat(1000))
1048+
.unwrap()
1049+
.txid()
1050+
.unwrap();
1051+
mined_txids.push(txid);
1052+
}
1053+
let _miner = MINER.lock().await;
1054+
generate_blocks_and_wait(1);
1055+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1056+
// Get the chain tip's blockhash.
1057+
let blockhash = blocking_client.get_tip_hash().unwrap();
1058+
let txs_blocking = blocking_client.get_block_txs(blockhash, None).unwrap();
1059+
let txs_async = async_client.get_block_txs(blockhash, None).await.unwrap();
1060+
// Assert that we only get 25 transactions, as per the Esplora specification.
1061+
assert_eq!(txs_blocking.len(), 25);
1062+
assert_eq!(txs_blocking, txs_async);
1063+
1064+
// Compare the received transactions (skipping the coinbase transaction).
1065+
// All 24 non-coinbase transactions should be from our expected set.
1066+
let expected_txids: HashSet<Txid> = mined_txids.iter().copied().collect();
1067+
let received_txids: HashSet<Txid> = txs_blocking
1068+
.iter()
1069+
.skip(1)
1070+
.map(|tx| tx.compute_txid())
1071+
.collect();
1072+
assert_eq!(received_txids.len(), 24);
1073+
assert!(received_txids.is_subset(&expected_txids));
1074+
1075+
let txs_blocking_offset = blocking_client.get_block_txs(blockhash, Some(25)).unwrap();
1076+
let txs_async_offset = async_client
1077+
.get_block_txs(blockhash, Some(25))
1078+
.await
1079+
.unwrap();
1080+
// 31 transactions on the block minus `start_index` of 25 yields 6 transactions.
1081+
assert_eq!(txs_blocking_offset.len(), 6);
1082+
assert_eq!(txs_blocking_offset, txs_async_offset);
1083+
1084+
// Compare the expected and received transactions from index 25 through 30.
1085+
let received_offset_txids: HashSet<Txid> = txs_blocking_offset
1086+
.iter()
1087+
.map(|tx| tx.compute_txid())
1088+
.collect();
1089+
assert!(received_offset_txids.is_subset(&expected_txids));
1090+
}
1091+
10331092
#[cfg(all(feature = "blocking", feature = "async"))]
10341093
#[tokio::test]
10351094
async fn test_get_blocks() {

0 commit comments

Comments
 (0)