From 581156deddbb634b2c170a718354d7b8d78cef08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 6 May 2026 16:19:22 +0000 Subject: [PATCH 1/7] feat!: Add Electrum protocol v1.6 method support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `ServerVersion` request type (`server.version`) - Add optional `mode` parameter to `EstimateFee` (breaking: new field) - Support both pre-1.6 (concatenated hex) and v1.6 (list of hex strings) response formats for `blockchain.block.headers` - Add `BroadcastPackage` request type (`blockchain.transaction.broadcast_package`) - Add `GetMempoolInfo` request type (`mempool.get_info`) - Add missing `Features` to `gen_pending_request_types!` macro Closes bitcoindevkit#8 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/custom_serde.rs | 45 +++++++++++---- src/pending_request.rs | 4 ++ src/request.rs | 124 ++++++++++++++++++++++++++++++++++++++++- src/response.rs | 63 +++++++++++++++++++-- 4 files changed, 217 insertions(+), 19 deletions(-) diff --git a/src/custom_serde.rs b/src/custom_serde.rs index 722fffb..1102f97 100644 --- a/src/custom_serde.rs +++ b/src/custom_serde.rs @@ -19,23 +19,44 @@ where deserialize_hex(&hex_str).map_err(serde::de::Error::custom) } -pub fn from_cancat_consensus_hex<'de, T, D>(deserializer: D) -> Result, D::Error> +/// Deserializes headers from either: +/// - A single concatenated hex string (pre-1.6: `"hex"` field) +/// - An array of individual hex strings (v1.6+: `"headers"` field) +pub fn headers_from_hex_or_list<'de, T, D>(deserializer: D) -> Result, D::Error> where T: bitcoin::consensus::encode::Decodable, D: Deserializer<'de>, { - let hex_str = String::deserialize(deserializer)?; - let data = Vec::::from_hex(&hex_str).map_err(serde::de::Error::custom)?; - - let mut items = Vec::::new(); - let mut read_start = 0_usize; - while read_start < data.len() { - let (item, read_count) = - deserialize_partial::(&data[read_start..]).map_err(serde::de::Error::custom)?; - read_start += read_count; - items.push(item); + let value = Value::deserialize(deserializer)?; + match value { + Value::String(hex_str) => { + // Pre-1.6: single concatenated hex string + let data = Vec::::from_hex(&hex_str).map_err(serde::de::Error::custom)?; + let mut items = Vec::::new(); + let mut read_start = 0_usize; + while read_start < data.len() { + let (item, read_count) = deserialize_partial::(&data[read_start..]) + .map_err(serde::de::Error::custom)?; + read_start += read_count; + items.push(item); + } + Ok(items) + } + Value::Array(arr) => { + // v1.6: array of hex strings + arr.into_iter() + .map(|v| { + let hex_str = v.as_str().ok_or_else(|| { + serde::de::Error::custom("expected hex string in headers array") + })?; + deserialize_hex(hex_str).map_err(serde::de::Error::custom) + }) + .collect() + } + _ => Err(serde::de::Error::custom( + "expected a hex string or array of hex strings for headers", + )), } - Ok(items) } pub fn feerate_opt_from_btc_per_kb<'de, D>( diff --git a/src/pending_request.rs b/src/pending_request.rs index 4686b9c..797fca4 100644 --- a/src/pending_request.rs +++ b/src/pending_request.rs @@ -97,11 +97,15 @@ gen_pending_request_types! { ScriptHashSubscribe, ScriptHashUnsubscribe, BroadcastTx, + BroadcastPackage, GetTx, GetTxMerkle, GetTxidFromPos, GetFeeHistogram, + GetMempoolInfo, + ServerVersion, Banner, + Features, Ping, Custom } diff --git a/src/request.rs b/src/request.rs index 9973529..b1629ec 100644 --- a/src/request.rs +++ b/src/request.rs @@ -235,17 +235,50 @@ impl Request for HeadersWithCheckpoint { /// fee rate (in BTC per kilobyte) required to be included within the specified number of blocks. /// /// See: +/// The fee estimation mode passed to the server's `estimatesmartfee` RPC. +/// +/// Added in Electrum protocol v1.6. +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EstimateFeeMode { + /// Conservative fee estimation (less likely to underestimate). + Conservative, + /// Economical fee estimation (may underestimate for faster inclusion). + Economical, +} + +impl EstimateFeeMode { + fn as_str(&self) -> &'static str { + match self { + Self::Conservative => "CONSERVATIVE", + Self::Economical => "ECONOMICAL", + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EstimateFee { /// The number of blocks to target for confirmation. pub number: usize, + + /// An optional estimation mode passed to the server's `estimatesmartfee` RPC. + /// + /// If `None`, the server uses its default mode. + /// + /// Added in Electrum protocol v1.6. + pub mode: Option, } impl Request for EstimateFee { type Response = response::EstimateFeeResp; fn to_method_and_params(&self) -> MethodAndParams { - ("blockchain.estimatefee".into(), vec![self.number.into()]) + let mut params: Vec = vec![self.number.into()]; + if let Some(mode) = &self.mode { + params.push(mode.as_str().into()); + } + ("blockchain.estimatefee".into(), params) } } @@ -271,6 +304,9 @@ impl Request for HeadersSubscribe { /// This corresponds to the `"server.relayfee"` Electrum RPC method. It returns the minimum /// fee rate (in BTC per kilobyte) that the server will accept for relaying transactions. /// +/// Removed in Electrum protocol v1.6 — use [`GetMempoolInfo`] (`mempool.get_info`) when +/// targeting v1.6+ servers. +/// /// See: #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct RelayFee; @@ -589,6 +625,37 @@ impl Request for GetTxidFromPos { } } +/// A request to broadcast a package of transactions to the network. +/// +/// This corresponds to the `"blockchain.transaction.broadcast_package"` Electrum RPC method, +/// which submits a package of related transactions (e.g., for CPFP or package relay). +/// +/// Added in Electrum protocol v1.6. +/// +/// See: +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct BroadcastPackage(pub Vec); + +impl Request for BroadcastPackage { + type Response = response::BroadcastPackageResp; + + fn to_method_and_params(&self) -> MethodAndParams { + let txs: Vec = self + .0 + .iter() + .map(|tx| { + let mut tx_bytes = Vec::::new(); + tx.consensus_encode(&mut tx_bytes).expect("must encode"); + tx_bytes.to_lower_hex_string().into() + }) + .collect(); + ( + "blockchain.transaction.broadcast_package".into(), + vec![txs.into()], + ) + } +} + /// A request for the current mempool fee histogram. /// /// This corresponds to the `"mempool.get_fee_histogram"` Electrum RPC method. It returns a compact @@ -607,6 +674,61 @@ impl Request for GetFeeHistogram { } } +/// A request to negotiate the protocol version with the Electrum server. +/// +/// This corresponds to the `"server.version"` Electrum RPC method. It identifies the client and +/// negotiates a compatible protocol version with the server. According to the Electrum protocol +/// specification, this should be the first message sent after connecting. +/// +/// The server will select the highest protocol version that both client and server support. +/// +/// See: +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ServerVersion { + /// A string identifying the client software (e.g., `"electrum_streaming_client/0.5"`). + pub client_name: CowStr, + + /// The protocol version or version range the client supports. + /// + /// Can be a single version string (e.g., `"1.6"`) or an array-style string for a range. + pub protocol_version: CowStr, +} + +impl Request for ServerVersion { + type Response = response::ServerVersionResp; + + fn to_method_and_params(&self) -> MethodAndParams { + ( + "server.version".into(), + vec![ + self.client_name.as_ref().into(), + self.protocol_version.as_ref().into(), + ], + ) + } +} + +/// A request for general mempool information from the Electrum server. +/// +/// This corresponds to the `"mempool.get_info"` Electrum RPC method. It returns fee-related +/// parameters including `mempoolminfee`, `minrelaytxfee`, and `incrementalrelayfee`. +/// +/// This replaces the `blockchain.relayfee` method, which was removed in v1.6. +/// +/// Added in Electrum protocol v1.6. +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GetMempoolInfo; + +impl Request for GetMempoolInfo { + type Response = response::MempoolInfoResp; + + fn to_method_and_params(&self) -> MethodAndParams { + ("mempool.get_info".into(), vec![]) + } +} + /// A request for the Electrum server's banner message. /// /// This corresponds to the `"server.banner"` Electrum RPC method, which returns a server-defined diff --git a/src/response.rs b/src/response.rs index 7510ec4..3731ed7 100644 --- a/src/response.rs +++ b/src/response.rs @@ -14,6 +14,20 @@ use bitcoin::{ use crate::DoubleSHA; +/// Response to the `"server.version"` method. +/// +/// Returns the server's software version and the negotiated protocol version. +/// +/// See: +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ServerVersionResp { + /// The server's software version string. + pub server_software: String, + + /// The negotiated protocol version string. + pub protocol_version: String, +} + /// Response to the `"blockchain.block.header"` method (without checkpoint). #[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)] #[serde(transparent)] @@ -38,6 +52,9 @@ pub struct HeaderWithProofResp { } /// Response to the `"blockchain.block.headers"` method (without checkpoint). +/// +/// Supports both the pre-1.6 format (concatenated hex in `"hex"` field) and the v1.6 format +/// (array of hex strings in `"headers"` field). #[derive(Debug, Clone, serde::Deserialize)] pub struct HeadersResp { /// The number of headers returned. @@ -45,16 +62,20 @@ pub struct HeadersResp { /// The deserialized headers returned by the server. #[serde( - rename = "hex", - deserialize_with = "crate::custom_serde::from_cancat_consensus_hex" + alias = "hex", + alias = "headers", + deserialize_with = "crate::custom_serde::headers_from_hex_or_list" )] pub headers: Vec, - /// The server’s maximum allowed headers per request. + /// The server's maximum allowed headers per request. pub max: usize, } /// Response to the `"blockchain.block.headers"` method with a `cp_height` parameter. +/// +/// Supports both the pre-1.6 format (concatenated hex in `"hex"` field) and the v1.6 format +/// (array of hex strings in `"headers"` field). #[derive(Debug, Clone, serde::Deserialize)] pub struct HeadersWithCheckpointResp { /// The number of headers returned. @@ -62,12 +83,13 @@ pub struct HeadersWithCheckpointResp { /// The deserialized headers returned by the server. #[serde( - rename = "hex", - deserialize_with = "crate::custom_serde::from_cancat_consensus_hex" + alias = "hex", + alias = "headers", + deserialize_with = "crate::custom_serde::headers_from_hex_or_list" )] pub headers: Vec, - /// The server’s maximum allowed headers per request. + /// The server's maximum allowed headers per request. pub max: usize, /// The Merkle root of all headers up to the checkpoint height. @@ -281,6 +303,35 @@ pub struct FeePair { pub weight: bitcoin::Weight, } +/// Response to the `"blockchain.transaction.broadcast_package"` method (non-verbose mode). +/// +/// See: +#[derive(Debug, Clone, serde::Deserialize)] +pub struct BroadcastPackageResp { + /// Whether the package was accepted by the server. + pub success: bool, +} + +/// Response to the `"mempool.get_info"` method. +/// +/// Provides fee-related information about the server's mempool. All fee rates are in BTC/kvB. +/// +/// See: +#[derive(Debug, Clone, serde::Deserialize)] +pub struct MempoolInfoResp { + /// The minimum fee rate (BTC/kvB) for a transaction to be accepted into the mempool. + #[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")] + pub mempoolminfee: Option, + + /// The minimum relay fee rate (BTC/kvB). + #[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")] + pub minrelaytxfee: Option, + + /// The incremental relay fee rate (BTC/kvB). + #[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")] + pub incrementalrelayfee: Option, +} + /// Response to the `"server.features"` method. #[derive(Debug, Clone, serde::Deserialize)] pub struct ServerFeatures { From de675c0e51759a03077310f2e71c5f94a8bba789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 6 May 2026 16:42:03 +0000 Subject: [PATCH 2/7] test: Add unit tests for `headers_from_hex_or_list` Cover both the pre-1.6 concatenated-hex path and the v1.6 array-of-hex path, asserting they produce equal `Vec
`, plus a sanity check that non-string/non-array inputs are rejected. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/custom_serde.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/custom_serde.rs b/src/custom_serde.rs index 1102f97..50ec32a 100644 --- a/src/custom_serde.rs +++ b/src/custom_serde.rs @@ -158,3 +158,34 @@ where } Ok(Version) } + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Deserialize)] + struct Wrapper { + #[serde(deserialize_with = "headers_from_hex_or_list")] + headers: Vec, + } + + #[test] + fn headers_from_hex_or_list_accepts_both_formats() { + // Any 80 bytes parse as a Header structurally; the test just checks both paths agree. + let h0 = "00".repeat(80); + let h1 = "ff".repeat(80); + + let concatenated: Wrapper = + serde_json::from_value(serde_json::json!({ "headers": format!("{h0}{h1}") })).unwrap(); + let array: Wrapper = + serde_json::from_value(serde_json::json!({ "headers": [h0, h1] })).unwrap(); + + assert_eq!(concatenated.headers.len(), 2); + assert_eq!(concatenated.headers, array.headers); + } + + #[test] + fn headers_from_hex_or_list_rejects_other_types() { + assert!(serde_json::from_value::(serde_json::json!({ "headers": 42 })).is_err()); + } +} From 559e8df41dde8d227b0ebee285c3a7fcb0e1cf19 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Fri, 24 Jul 2026 14:58:49 +0300 Subject: [PATCH 3/7] fix(docs): correct EstimateFee and EstimateFeeMode docs --- src/request.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/request.rs b/src/request.rs index b1629ec..eaad796 100644 --- a/src/request.rs +++ b/src/request.rs @@ -228,13 +228,6 @@ impl Request for HeadersWithCheckpoint { } } -/// A request for an estimated fee rate needed to confirm a transaction within a target number of -/// blocks. -/// -/// This corresponds to the `"blockchain.estimatefee"` Electrum RPC method. It returns the estimated -/// fee rate (in BTC per kilobyte) required to be included within the specified number of blocks. -/// -/// See: /// The fee estimation mode passed to the server's `estimatesmartfee` RPC. /// /// Added in Electrum protocol v1.6. @@ -242,9 +235,11 @@ impl Request for HeadersWithCheckpoint { /// See: #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EstimateFeeMode { - /// Conservative fee estimation (less likely to underestimate). + /// Conservative estimate: potentially higher feerate, more likely to meet the target, less + /// responsive to short-term fee drops. Conservative, - /// Economical fee estimation (may underestimate for faster inclusion). + /// Economical estimate: potentially lower feerate, more responsive to short-term fee drops, may + /// take longer to confirm. Economical, } @@ -257,6 +252,13 @@ impl EstimateFeeMode { } } +/// A request for an estimated fee rate needed to confirm a transaction within a target number of +/// blocks. +/// +/// This corresponds to the `"blockchain.estimatefee"` Electrum RPC method. It returns the estimated +/// fee rate (in BTC per kilobyte) required to be included within the specified number of blocks. +/// +/// See: #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EstimateFee { /// The number of blocks to target for confirmation. From a17547baea866ada485f8a6fb38f0f83ac407566 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Fri, 24 Jul 2026 16:18:12 +0300 Subject: [PATCH 4/7] fix: version request/response --- src/request.rs | 47 ++++++++++++++++++++++++++++++++++++----------- src/response.rs | 14 ++++++++++++-- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/request.rs b/src/request.rs index eaad796..3f6b264 100644 --- a/src/request.rs +++ b/src/request.rs @@ -185,9 +185,10 @@ impl Request for Headers { type Response = response::HeadersResp; fn to_method_and_params(&self) -> MethodAndParams { - ("blockchain.block.headers".into(), { - vec![self.start_height.into(), self.count.into()] - }) + ( + "blockchain.block.headers".into(), + vec![self.start_height.into(), self.count.into()], + ) } } @@ -218,13 +219,14 @@ impl Request for HeadersWithCheckpoint { type Response = response::HeadersWithCheckpointResp; fn to_method_and_params(&self) -> MethodAndParams { - ("blockchain.block.headers".into(), { + ( + "blockchain.block.headers".into(), vec![ self.start_height.into(), self.count.into(), self.cp_height.into(), - ] - }) + ], + ) } } @@ -676,6 +678,31 @@ impl Request for GetFeeHistogram { } } +/// The `protocol_version` param for [`ServerVersion`]. +/// +/// Corresponds to the second argument of `server.version`: either a single version string, or a +/// `[protocol_min, protocol_max]` range. +/// +/// See: +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SupportedVersion { + /// A single version string (e.g. `"1.6"`). Equivalent to a range where min == max. + Exact(CowStr), + /// A version range `[protocol_min, protocol_max]` (e.g. `["1.4", "1.6"]`). + Range([CowStr; 2]), +} + +impl From<&SupportedVersion> for serde_json::Value { + fn from(value: &SupportedVersion) -> Self { + match value { + SupportedVersion::Exact(version) => version.as_ref().into(), + SupportedVersion::Range([min, max]) => { + serde_json::Value::Array(vec![min.as_ref().into(), max.as_ref().into()]) + } + } + } +} + /// A request to negotiate the protocol version with the Electrum server. /// /// This corresponds to the `"server.version"` Electrum RPC method. It identifies the client and @@ -690,10 +717,8 @@ pub struct ServerVersion { /// A string identifying the client software (e.g., `"electrum_streaming_client/0.5"`). pub client_name: CowStr, - /// The protocol version or version range the client supports. - /// - /// Can be a single version string (e.g., `"1.6"`) or an array-style string for a range. - pub protocol_version: CowStr, + /// The protocol version range the client supports. + pub protocol_version: SupportedVersion, } impl Request for ServerVersion { @@ -704,7 +729,7 @@ impl Request for ServerVersion { "server.version".into(), vec![ self.client_name.as_ref().into(), - self.protocol_version.as_ref().into(), + (&self.protocol_version).into(), ], ) } diff --git a/src/response.rs b/src/response.rs index 3731ed7..6324d85 100644 --- a/src/response.rs +++ b/src/response.rs @@ -20,14 +20,24 @@ use crate::DoubleSHA; /// /// See: #[derive(Debug, Clone, serde::Deserialize)] +#[serde(from = "(String, String)")] pub struct ServerVersionResp { - /// The server's software version string. + /// Server software version (e.g. `"ElectrumX 1.18.0"`). pub server_software: String, - /// The negotiated protocol version string. + /// Negotiated protocol version (e.g. `"1.4"`). pub protocol_version: String, } +impl From<(String, String)> for ServerVersionResp { + fn from((server_software, protocol_version): (String, String)) -> Self { + Self { + server_software, + protocol_version, + } + } +} + /// Response to the `"blockchain.block.header"` method (without checkpoint). #[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)] #[serde(transparent)] From 6ef81e9012327dcb33d0002298025bd73b7cc107 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Fri, 24 Jul 2026 17:14:51 +0300 Subject: [PATCH 5/7] feat: Add per-tx errors to BroadcastPackageResp Capture rejection details when package broadcast fails, and simplify broadcast hex encoding with serialize_hex. --- src/request.rs | 17 ++++------------- src/response.rs | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/request.rs b/src/request.rs index 3f6b264..3dd291d 100644 --- a/src/request.rs +++ b/src/request.rs @@ -16,7 +16,7 @@ //! [`to_method_and_params`]: Request::to_method_and_params //! [`Response`]: Request::Response -use bitcoin::{consensus::Encodable, hex::DisplayHex, Script, Txid}; +use bitcoin::{consensus::encode::serialize_hex, Script, Txid}; use crate::{ response, CowStr, ElectrumScriptHash, ElectrumScriptStatus, MethodAndParams, RawRequest, @@ -543,11 +543,9 @@ impl Request for BroadcastTx { type Response = bitcoin::Txid; fn to_method_and_params(&self) -> MethodAndParams { - let mut tx_bytes = Vec::::new(); - self.0.consensus_encode(&mut tx_bytes).expect("must encode"); ( "blockchain.transaction.broadcast".into(), - vec![tx_bytes.to_lower_hex_string().into()], + vec![serialize_hex(&self.0).into()], ) } } @@ -644,15 +642,8 @@ impl Request for BroadcastPackage { type Response = response::BroadcastPackageResp; fn to_method_and_params(&self) -> MethodAndParams { - let txs: Vec = self - .0 - .iter() - .map(|tx| { - let mut tx_bytes = Vec::::new(); - tx.consensus_encode(&mut tx_bytes).expect("must encode"); - tx_bytes.to_lower_hex_string().into() - }) - .collect(); + let txs: Vec = + self.0.iter().map(|tx| serialize_hex(tx).into()).collect(); ( "blockchain.transaction.broadcast_package".into(), vec![txs.into()], diff --git a/src/response.rs b/src/response.rs index 6324d85..73821d1 100644 --- a/src/response.rs +++ b/src/response.rs @@ -320,6 +320,21 @@ pub struct FeePair { pub struct BroadcastPackageResp { /// Whether the package was accepted by the server. pub success: bool, + + /// Per-transaction errors for txs that were not accepted, if any. + /// + /// Present when `success` is `false`. + pub errors: Option>, +} + +/// A per-transaction rejection inside [`BroadcastPackageResp::errors`]. +#[derive(Debug, Clone, serde::Deserialize)] +pub struct BroadcastPackageError { + /// The rejected transaction's txid. + pub txid: bitcoin::Txid, + + /// The rejection reason (e.g. `"bad-txns-inputs-missingorspent"`). + pub error: String, } /// Response to the `"mempool.get_info"` method. From 3c9c991c5c38fab6180a98d689a421c997a70d5a Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Fri, 24 Jul 2026 17:31:28 +0300 Subject: [PATCH 6/7] fix: Require FeeRate fields on MempoolInfoResp mempool.get_info returns required BTC/kvB floats, so deserialize to FeeRate directly and share conversion with the estimatefee opt helper. --- src/custom_serde.rs | 20 ++++++++++++++++++-- src/request.rs | 6 ++---- src/response.rs | 20 ++++++++++---------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/custom_serde.rs b/src/custom_serde.rs index 50ec32a..90ac406 100644 --- a/src/custom_serde.rs +++ b/src/custom_serde.rs @@ -59,6 +59,23 @@ where } } +fn feerate_from_btc_per_kb_f32(btc_per_kvb: f32) -> Result { + if btc_per_kvb.is_sign_negative() { + return Err(E::custom("expected non-negative fee rate in BTC/kvB")); + } + let sat_per_kwu = btc_per_kvb * (100_000_000.0 / 4.0); + Ok(bitcoin::FeeRate::from_sat_per_kwu(sat_per_kwu as _)) +} + +/// BTC/kvB → [`bitcoin::FeeRate`]; errors if negative. +pub fn feerate_from_btc_per_kb<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + feerate_from_btc_per_kb_f32(f32::deserialize(deserializer)?) +} + +/// BTC/kvB → [`bitcoin::FeeRate`]; negative → `None`. pub fn feerate_opt_from_btc_per_kb<'de, D>( deserializer: D, ) -> Result, D::Error> @@ -69,8 +86,7 @@ where if btc_per_kvb.is_sign_negative() { return Ok(None); } - let sat_per_kwu = btc_per_kvb * (100_000_000.0 / 4.0); - Ok(Some(bitcoin::FeeRate::from_sat_per_kwu(sat_per_kwu as _))) + feerate_from_btc_per_kb_f32(btc_per_kvb).map(Some) } pub fn feerate_from_sat_per_byte<'de, D>(deserializer: D) -> Result diff --git a/src/request.rs b/src/request.rs index 3dd291d..9a02e43 100644 --- a/src/request.rs +++ b/src/request.rs @@ -311,7 +311,7 @@ impl Request for HeadersSubscribe { /// Removed in Electrum protocol v1.6 — use [`GetMempoolInfo`] (`mempool.get_info`) when /// targeting v1.6+ servers. /// -/// See: +/// See: #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct RelayFee; @@ -731,9 +731,7 @@ impl Request for ServerVersion { /// This corresponds to the `"mempool.get_info"` Electrum RPC method. It returns fee-related /// parameters including `mempoolminfee`, `minrelaytxfee`, and `incrementalrelayfee`. /// -/// This replaces the `blockchain.relayfee` method, which was removed in v1.6. -/// -/// Added in Electrum protocol v1.6. +/// Added in Electrum protocol v1.6 and replaces the `blockchain.relayfee` method. /// /// See: #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/src/response.rs b/src/response.rs index 73821d1..206d5c1 100644 --- a/src/response.rs +++ b/src/response.rs @@ -339,22 +339,22 @@ pub struct BroadcastPackageError { /// Response to the `"mempool.get_info"` method. /// -/// Provides fee-related information about the server's mempool. All fee rates are in BTC/kvB. +/// Provides fee-related information about the server's mempool. /// /// See: #[derive(Debug, Clone, serde::Deserialize)] pub struct MempoolInfoResp { - /// The minimum fee rate (BTC/kvB) for a transaction to be accepted into the mempool. - #[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")] - pub mempoolminfee: Option, + /// The minimum fee rate for a transaction to be accepted into the mempool. + #[serde(deserialize_with = "crate::custom_serde::feerate_from_btc_per_kb")] + pub mempoolminfee: bitcoin::FeeRate, - /// The minimum relay fee rate (BTC/kvB). - #[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")] - pub minrelaytxfee: Option, + /// The minimum relay fee rate. + #[serde(deserialize_with = "crate::custom_serde::feerate_from_btc_per_kb")] + pub minrelaytxfee: bitcoin::FeeRate, - /// The incremental relay fee rate (BTC/kvB). - #[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")] - pub incrementalrelayfee: Option, + /// The incremental relay fee rate. + #[serde(deserialize_with = "crate::custom_serde::feerate_from_btc_per_kb")] + pub incrementalrelayfee: bitcoin::FeeRate, } /// Response to the `"server.features"` method. From c1619bcadc9581f496e3c14610cc418f16d51969 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Fri, 24 Jul 2026 17:35:50 +0300 Subject: [PATCH 7/7] fix(docs): Correct relayfee method name and README example Use blockchain.relayfee in docs and switch the README sample to GetMempoolInfo for v1.6. --- README.md | 6 ++++-- src/request.rs | 2 +- src/response.rs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 77bac7c..620a76d 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ async fn main() -> anyhow::Result<()> { tokio::spawn(worker); // spawn the client worker task - let relay_fee = client.send_request(electrum_streaming_client::request::RelayFee).await?; - println!("Relay fee: {relay_fee:?}"); + let mempool_info = client + .send_request(electrum_streaming_client::request::GetMempoolInfo) + .await?; + println!("Mempool info: {mempool_info:?}"); while let Some(event) = events.next().await { println!("Event: {event:?}"); diff --git a/src/request.rs b/src/request.rs index 9a02e43..682b749 100644 --- a/src/request.rs +++ b/src/request.rs @@ -305,7 +305,7 @@ impl Request for HeadersSubscribe { /// A request for the minimum fee rate accepted by the Electrum server's mempool. /// -/// This corresponds to the `"server.relayfee"` Electrum RPC method. It returns the minimum +/// This corresponds to the `"blockchain.relayfee"` Electrum RPC method. It returns the minimum /// fee rate (in BTC per kilobyte) that the server will accept for relaying transactions. /// /// Removed in Electrum protocol v1.6 — use [`GetMempoolInfo`] (`mempool.get_info`) when diff --git a/src/response.rs b/src/response.rs index 206d5c1..914db6b 100644 --- a/src/response.rs +++ b/src/response.rs @@ -132,7 +132,7 @@ pub struct HeadersSubscribeResp { pub height: u32, } -/// Response to the `"server.relayfee"` method. +/// Response to the `"blockchain.relayfee"` method. #[derive(Debug, Clone, serde::Deserialize)] #[serde(transparent)] pub struct RelayFeeResp {