Skip to content
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
6 changes: 3 additions & 3 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ where
&self,
height: usize,
tx_pos: usize,
) -> Result<TxidFromPosRes, Error> {
) -> Result<TxidFromPosMerkleRes, Error> {
(**self).txid_from_pos_with_merkle(height, tx_pos)
}

Expand Down Expand Up @@ -444,7 +444,7 @@ pub trait ElectrumApi {
&self,
height: usize,
tx_pos: usize,
) -> Result<TxidFromPosRes, Error>;
) -> Result<TxidFromPosMerkleRes, Error>;

/// Returns the capabilities of the server.
fn server_features(&self) -> Result<ServerFeaturesRes, Error>;
Expand Down Expand Up @@ -689,7 +689,7 @@ mod test {
&self,
_: usize,
_: usize,
) -> Result<super::TxidFromPosRes, super::Error> {
) -> Result<super::TxidFromPosMerkleRes, super::Error> {
unreachable!()
}

Expand Down
2 changes: 1 addition & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ impl ElectrumApi for Client {
&self,
height: usize,
tx_pos: usize,
) -> Result<TxidFromPosRes, Error> {
) -> Result<TxidFromPosMerkleRes, Error> {
impl_inner_call!(self, txid_from_pos_with_merkle, height, tx_pos)
}

Expand Down
12 changes: 9 additions & 3 deletions src/raw_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1367,22 +1367,28 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
}

fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error> {
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",
params,
);
let result = self.call(req)?;

Ok(serde_json::from_value(result)?)
Ok(serde_json::from_value::<TxidFromPosRes>(result)?.into())
}

fn txid_from_pos_with_merkle(
&self,
height: usize,
tx_pos: usize,
) -> Result<TxidFromPosRes, Error> {
) -> Result<TxidFromPosMerkleRes, Error> {
let params = vec![
Param::Usize(height),
Param::Usize(tx_pos),
Expand Down
76 changes: 75 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TxidFromPosRes> 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")]
Expand Down Expand Up @@ -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![]);
Expand Down
Loading