diff --git a/src/client.rs b/src/client.rs
index 4bf37165..a35a158d 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -7,6 +7,7 @@ use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::oneshot;
use crate::chain::block_subsidy;
+use crate::chain::IndexedHeader;
use crate::messages::ClientRequest;
use crate::{Event, HeaderCheckpoint, Info, TrustedPeer, Warning};
@@ -221,6 +222,21 @@ impl Requester {
rx.await.map_err(|_| ClientError::RecvError)
}
+ /// Look up a header at a specific height in the locally synced chain of most work.
+ /// Returns `None` if the height is not in the header chain.
+ ///
+ /// # Errors
+ ///
+ /// If the node has stopped running.
+ pub async fn get_header(&self, height: u32) -> Result, ClientError> {
+ let (tx, rx) = tokio::sync::oneshot::channel:: >();
+ let request = ClientRequest::new(height, tx);
+ self.ntx
+ .send(ClientMessage::GetHeader(request))
+ .map_err(|_| ClientError::SendError)?;
+ rx.await.map_err(|_| ClientError::RecvError)
+ }
+
/// Check if the node is running.
pub fn is_running(&self) -> bool {
self.ntx.send(ClientMessage::NoOp).is_ok()
diff --git a/src/messages.rs b/src/messages.rs
index 7021c859..269c663a 100644
--- a/src/messages.rs
+++ b/src/messages.rs
@@ -7,7 +7,7 @@ use bitcoin::{
block::Header, p2p::message_network::RejectReason, BlockHash, FeeRate, Transaction, Wtxid,
};
-use crate::chain::BlockHeaderChanges;
+use crate::chain::{BlockHeaderChanges, IndexedHeader};
use crate::IndexedFilter;
use crate::{chain::checkpoints::HeaderCheckpoint, IndexedBlock, TrustedPeer};
@@ -153,6 +153,8 @@ pub(crate) enum ClientMessage {
GetBroadcastMinFeeRate(ClientRequest<(), FeeRate>),
/// Get info on connections
GetPeerInfo(ClientRequest<(), Vec<(AddrV2, ServiceFlags)>>),
+ /// Look up a header at a specific height in the chain of most work.
+ GetHeader(ClientRequest>),
/// Send an empty message to see if the node is running.
NoOp,
}
diff --git a/src/node.rs b/src/node.rs
index 94bd4293..f08ed8ba 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -29,7 +29,7 @@ use crate::{
chain::Chain,
checkpoints::HeaderCheckpoint,
error::HeaderSyncError,
- CFHeaderChanges, ChainState, FilterCheck, HeightMonitor,
+ CFHeaderChanges, ChainState, FilterCheck, HeightMonitor, IndexedHeader,
},
error::FetchBlockError,
messages::ClientRequest,
@@ -268,6 +268,17 @@ impl Node {
self.dialog.send_warning(Warning::ChannelDropped);
};
}
+ ClientMessage::GetHeader(request) => {
+ let (height, oneshot) = request.into_values();
+ let header = self
+ .chain
+ .header_chain
+ .header_at_height(height)
+ .map(|h| IndexedHeader::new(height, h));
+ if oneshot.send(header).is_err() {
+ self.dialog.send_warning(Warning::ChannelDropped);
+ };
+ }
ClientMessage::NoOp => (),
}
}
diff --git a/tests/core.rs b/tests/core.rs
index 5d62e984..74d690e1 100644
--- a/tests/core.rs
+++ b/tests/core.rs
@@ -253,6 +253,20 @@ async fn various_client_methods() {
let peers = requester.peer_info().await.unwrap();
assert_eq!(peers.len(), 1);
assert!(requester.is_running());
+ // get_header should return a header within the synced range
+ let header_at_1 = requester.get_header(1).await.unwrap();
+ assert!(header_at_1.is_some());
+ let header_at_1 = header_at_1.unwrap();
+ assert_eq!(header_at_1.height, 1);
+ // get_header at the tip should match the chain tip hash
+ let tip_header = requester.get_header(cp.height).await.unwrap();
+ assert!(tip_header.is_some());
+ let tip_header = tip_header.unwrap();
+ assert_eq!(tip_header.height, cp.height);
+ assert_eq!(tip_header.block_hash(), cp.hash);
+ // get_header beyond the chain should return None
+ let too_high = requester.get_header(cp.height + 1).await.unwrap();
+ assert!(too_high.is_none());
requester.shutdown().unwrap();
rpc.stop().unwrap();
}