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
76 changes: 64 additions & 12 deletions src/custom_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<T>, 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<Vec<T>, D::Error>
where
T: bitcoin::consensus::encode::Decodable,
D: Deserializer<'de>,
{
let hex_str = String::deserialize(deserializer)?;
let data = Vec::<u8>::from_hex(&hex_str).map_err(serde::de::Error::custom)?;

let mut items = Vec::<T>::new();
let mut read_start = 0_usize;
while read_start < data.len() {
let (item, read_count) =
deserialize_partial::<T>(&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::<u8>::from_hex(&hex_str).map_err(serde::de::Error::custom)?;
let mut items = Vec::<T>::new();
let mut read_start = 0_usize;
while read_start < data.len() {
let (item, read_count) = deserialize_partial::<T>(&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>(
Expand Down Expand Up @@ -137,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<bitcoin::block::Header>,
}

#[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::<Wrapper>(serde_json::json!({ "headers": 42 })).is_err());
}
}
4 changes: 4 additions & 0 deletions src/pending_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,15 @@ gen_pending_request_types! {
ScriptHashSubscribe,
ScriptHashUnsubscribe,
BroadcastTx,
BroadcastPackage,
GetTx,
GetTxMerkle,
GetTxidFromPos,
GetFeeHistogram,
GetMempoolInfo,
ServerVersion,
Banner,
Features,
Ping,
Custom
}
Expand Down
124 changes: 123 additions & 1 deletion src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#blockchain-estimatefee>
/// The fee estimation mode passed to the server's `estimatesmartfee` RPC.
///
/// Added in Electrum protocol v1.6.
///
/// See: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#blockchain-estimatefee>
#[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<EstimateFeeMode>,
}

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<serde_json::Value> = vec![self.number.into()];
if let Some(mode) = &self.mode {
params.push(mode.as_str().into());
}
("blockchain.estimatefee".into(), params)
}
}

Expand All @@ -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: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#server-relayfee>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RelayFee;
Expand Down Expand Up @@ -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: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-broadcast-package>
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BroadcastPackage(pub Vec<bitcoin::Transaction>);

impl Request for BroadcastPackage {
type Response = response::BroadcastPackageResp;

fn to_method_and_params(&self) -> MethodAndParams {
let txs: Vec<serde_json::Value> = self
.0
.iter()
.map(|tx| {
let mut tx_bytes = Vec::<u8>::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
Expand All @@ -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: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#server-version>
#[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: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#mempool-get-info>
#[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
Expand Down
63 changes: 57 additions & 6 deletions src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#server-version>
#[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)]
Expand All @@ -38,36 +52,44 @@ 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.
pub count: usize,

/// 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<bitcoin::block::Header>,

/// The servers 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.
pub count: usize,

/// 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<bitcoin::block::Header>,

/// The servers 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.
Expand Down Expand Up @@ -281,6 +303,35 @@ pub struct FeePair {
pub weight: bitcoin::Weight,
}

/// Response to the `"blockchain.transaction.broadcast_package"` method (non-verbose mode).
///
/// See: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-broadcast-package>
#[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: <https://electrum-protocol.readthedocs.io/en/latest/protocol-methods.html#mempool-get-info>
#[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<bitcoin::FeeRate>,

/// The minimum relay fee rate (BTC/kvB).
#[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")]
pub minrelaytxfee: Option<bitcoin::FeeRate>,

/// The incremental relay fee rate (BTC/kvB).
#[serde(deserialize_with = "crate::custom_serde::feerate_opt_from_btc_per_kb")]
pub incrementalrelayfee: Option<bitcoin::FeeRate>,
}

/// Response to the `"server.features"` method.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct ServerFeatures {
Expand Down