diff --git a/src/api.rs b/src/api.rs index 2a31b4f..da64f12 100644 --- a/src/api.rs +++ b/src/api.rs @@ -174,7 +174,7 @@ where &self, height: usize, tx_pos: usize, - ) -> Result { + ) -> Result { (**self).txid_from_pos_with_merkle(height, tx_pos) } @@ -444,7 +444,7 @@ pub trait ElectrumApi { &self, height: usize, tx_pos: usize, - ) -> Result; + ) -> Result; /// Returns the capabilities of the server. fn server_features(&self) -> Result; @@ -689,7 +689,7 @@ mod test { &self, _: usize, _: usize, - ) -> Result { + ) -> Result { unreachable!() } diff --git a/src/client.rs b/src/client.rs index d86880f..473cfdb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -406,7 +406,7 @@ impl ElectrumApi for Client { &self, height: usize, tx_pos: usize, - ) -> Result { + ) -> Result { impl_inner_call!(self, txid_from_pos_with_merkle, height, tx_pos) } diff --git a/src/raw_client.rs b/src/raw_client.rs index 3754851..6f6abe6 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -1367,7 +1367,13 @@ impl ElectrumApi for RawClient { } fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result { - let params = vec![Param::Usize(height), Param::Usize(tx_pos)]; + // The `merkle` flag is optional per the protocol, but some server implementations (e.g. + // `electrs`) reject requests that omit it, so send it explicitly. + let params = vec![ + Param::Usize(height), + Param::Usize(tx_pos), + Param::Bool(false), + ]; let req = Request::new_id( self.last_id.fetch_add(1, Ordering::SeqCst), "blockchain.transaction.id_from_pos", @@ -1375,14 +1381,14 @@ impl ElectrumApi for RawClient { ); let result = self.call(req)?; - Ok(serde_json::from_value(result)?) + Ok(serde_json::from_value::(result)?.into()) } fn txid_from_pos_with_merkle( &self, height: usize, tx_pos: usize, - ) -> Result { + ) -> Result { let params = vec![ Param::Usize(height), Param::Usize(tx_pos), diff --git a/src/types.rs b/src/types.rs index b6a2c9a..d454591 100644 --- a/src/types.rs +++ b/src/types.rs @@ -344,11 +344,39 @@ pub struct GetMerkleRes { pub merkle: Vec<[u8; 32]>, } +/// Response to a [`txid_from_pos`](../client/struct.Client.html#method.txid_from_pos) request. +/// +/// The protocol specifies a bare transaction hash string when `merkle` is `false`, but some +/// server implementations (e.g. `electrs`) reply with a JSON object holding a `tx_hash` field +/// instead, so both shapes are accepted. `electrs` before v0.11.1 named that field `tx_id`. +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub(crate) enum TxidFromPosRes { + /// A bare transaction hash. + Hash(Txid), + /// An object holding the transaction hash. + Object { + #[serde(alias = "tx_id")] + tx_hash: Txid, + }, +} + +impl From for Txid { + fn from(raw: TxidFromPosRes) -> Self { + match raw { + TxidFromPosRes::Hash(tx_hash) | TxidFromPosRes::Object { tx_hash } => tx_hash, + } + } +} + /// Response to a [`txid_from_pos_with_merkle`](../client/struct.Client.html#method.txid_from_pos_with_merkle) /// request. #[derive(Clone, Debug, Deserialize)] -pub struct TxidFromPosRes { +pub struct TxidFromPosMerkleRes { /// Txid of the transaction. + /// + /// `electrs` before v0.11.1 named this field `tx_id`, which is accepted as an alias. + #[serde(alias = "tx_id")] pub tx_hash: Txid, /// The merkle path of the transaction. #[serde(deserialize_with = "from_hex_array")] @@ -556,6 +584,52 @@ mod tests { assert_eq!(script_status, script_status_back); } + #[test] + fn txid_from_pos_accepts_both_response_shapes() { + use super::TxidFromPosRes; + use bitcoin::Txid; + use std::str::FromStr; + + let expected = + Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d") + .unwrap(); + + // The bare hash string specified by the protocol. + let spec_shape = r#""1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d""#; + let parsed: TxidFromPosRes = serde_json::from_str(spec_shape).unwrap(); + assert_eq!(Txid::from(parsed), expected); + + // The two JSON keys used by electrs versions <0.11.1, and version 0.11.1. + for key in ["tx_id", "tx_hash"] { + let json = format!( + r#"{{"{key}":"1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d","merkle":["a9642263a86519a8b85a4d3297f58265c1e588803f6ef113f23bb889f5f9bc6e"]}}"# + ); + let parsed: TxidFromPosRes = serde_json::from_str(&json).unwrap(); + assert_eq!(Txid::from(parsed), expected, "failed for key {key}"); + } + } + + #[test] + fn txid_from_pos_with_merkle_accepts_legacy_electrs_key() { + use super::TxidFromPosMerkleRes; + use bitcoin::Txid; + use std::str::FromStr; + + let expected = + Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d") + .unwrap(); + + // The two JSON keys used by electrs versions <0.11.1, and version 0.11.1. + for key in ["tx_id", "tx_hash"] { + let json = format!( + r#"{{"{key}":"1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d","merkle":["a9642263a86519a8b85a4d3297f58265c1e588803f6ef113f23bb889f5f9bc6e"]}}"# + ); + let parsed: TxidFromPosMerkleRes = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.tx_hash, expected, "failed for key {key}"); + assert_eq!(parsed.merkle.len(), 1); + } + } + #[test] fn test_request_serialization_without_authorization() { let req = Request::new_id(1, "server.version", vec![]);