Skip to content

Commit 8d2dc26

Browse files
committed
feat: Support v1.6 block_headers response format with backwards compatibility
Parse `blockchain.block.headers` response based on negotiated protocol version: - v1.6+: Headers returned as array of hex strings in `headers` field - v1.4 (legacy): Headers returned as concatenated hex in `hex` field - Add `is_protocol_version_at_least()` helper for semantic version comparison - Rename old response type to `GetHeadersResLegacy` (internal) - New `GetHeadersRes` handles v1.6 format with `header_hexes` field - Both formats produce the same public `headers: Vec<block::Header>` output Co-Authored-By: Claude Code AI
1 parent 39d262b commit 8d2dc26

File tree

2 files changed

+60
-13
lines changed

2 files changed

+60
-13
lines changed

src/raw_client.rs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ pub const PROTOCOL_VERSION_MIN: &str = "1.4";
5454
/// Maximum protocol version supported by this client.
5555
pub const PROTOCOL_VERSION_MAX: &str = "1.6";
5656

57+
/// Checks if a protocol version string is at least the specified major.minor version.
58+
fn is_protocol_version_at_least(version: &str, major: u32, minor: u32) -> bool {
59+
let mut parts = version.split('.');
60+
let v_major = parts.next().and_then(|s| s.parse::<u32>().ok());
61+
let v_minor = parts.next().and_then(|s| s.parse::<u32>().ok());
62+
match (v_major, v_minor) {
63+
(Some(v_major), Some(v_minor)) => v_major > major || (v_major == major && v_minor >= minor),
64+
_ => false,
65+
}
66+
}
67+
5768
macro_rules! impl_batch_call {
5869
( $self:expr, $data:expr, $call:ident ) => {{
5970
impl_batch_call!($self, $data, $call, )
@@ -931,16 +942,38 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
931942
);
932943
let result = self.call(req)?;
933944

934-
let mut deserialized: GetHeadersRes = serde_json::from_value(result)?;
935-
for i in 0..deserialized.count {
936-
let (start, end) = (i * 80, (i + 1) * 80);
937-
deserialized
938-
.headers
939-
.push(deserialize(&deserialized.raw_headers[start..end])?);
940-
}
941-
deserialized.raw_headers.clear();
945+
// Check protocol version to determine response format
946+
let is_v1_6_or_later = {
947+
let protocol_version = self.protocol_version.lock()?;
948+
protocol_version
949+
.as_ref()
950+
.map(|v| is_protocol_version_at_least(v, 1, 6))
951+
.unwrap_or(false)
952+
};
942953

943-
Ok(deserialized)
954+
if is_v1_6_or_later {
955+
// v1.6+: headers field contains array of hex strings
956+
let mut deserialized: GetHeadersRes = serde_json::from_value(result)?;
957+
for header_hex in &deserialized.header_hexes {
958+
let header_bytes = Vec::<u8>::from_hex(header_hex)?;
959+
deserialized.headers.push(deserialize(&header_bytes)?);
960+
}
961+
Ok(deserialized)
962+
} else {
963+
// v1.4: hex field contains concatenated headers
964+
let deserialized: GetHeadersResLegacy = serde_json::from_value(result)?;
965+
let mut headers = Vec::new();
966+
for i in 0..deserialized.count {
967+
let (start, end) = (i * 80, (i + 1) * 80);
968+
headers.push(deserialize(&deserialized.raw_headers[start..end])?);
969+
}
970+
Ok(GetHeadersRes {
971+
max: deserialized.max,
972+
count: deserialized.count,
973+
header_hexes: Vec::new(),
974+
headers,
975+
})
976+
}
944977
}
945978

946979
fn estimate_fee(&self, number: usize, mode: Option<EstimationMode>) -> Result<f64, Error> {

src/types.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,31 @@ impl<'de> Deserialize<'de> for ServerVersionRes {
271271
}
272272
}
273273

274-
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
274+
/// Response to a [`block_headers`](../client/struct.Client.html#method.block_headers) request (protocol v1.4, legacy format).
275+
///
276+
/// In protocol v1.4, the headers are returned as a single concatenated hex string.
275277
#[derive(Clone, Debug, Deserialize)]
276-
pub struct GetHeadersRes {
278+
pub(crate) struct GetHeadersResLegacy {
277279
/// Maximum number of headers returned in a single response.
278280
pub max: usize,
279281
/// Number of headers in this response.
280282
pub count: usize,
281-
/// Raw headers concatenated. Normally cleared before returning.
283+
/// Raw headers concatenated.
282284
#[serde(rename(deserialize = "hex"), deserialize_with = "from_hex")]
283285
pub raw_headers: Vec<u8>,
284-
/// Array of block headers.
286+
}
287+
288+
/// Response to a [`block_headers`](../client/struct.Client.html#method.block_headers) request.
289+
#[derive(Clone, Debug, Deserialize)]
290+
pub struct GetHeadersRes {
291+
/// Maximum number of headers returned in a single response.
292+
pub max: usize,
293+
/// Number of headers in this response.
294+
pub count: usize,
295+
/// Array of header hex strings (v1.6 format).
296+
#[serde(default, rename(deserialize = "headers"))]
297+
pub(crate) header_hexes: Vec<String>,
298+
/// Array of block headers (populated after parsing).
285299
#[serde(skip)]
286300
pub headers: Vec<block::Header>,
287301
}

0 commit comments

Comments
 (0)