diff --git a/Cargo.toml b/Cargo.toml index 5e3c99a..3a42602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ tokio-util = { version = "0.7.15", features = ["compat"], optional = true } [features] default = ["tokio"] tokio = ["dep:tokio", "tokio-util"] +frigate = [] [dev-dependencies] async-std = "1.13.0" 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/custom_serde.rs b/src/custom_serde.rs index 722fffb..90ac406 100644 --- a/src/custom_serde.rs +++ b/src/custom_serde.rs @@ -19,25 +19,63 @@ 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", + )), + } +} + +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")); } - Ok(items) + 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> @@ -48,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 @@ -137,3 +174,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()); + } +} diff --git a/src/notification.rs b/src/notification.rs index 6975110..b1f07e1 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -5,6 +5,7 @@ //! //! - [`Notification::Header`] for `"blockchain.headers.subscribe"` //! - [`Notification::ScriptHash`] for `"blockchain.scripthash.subscribe"` +//! - `Notification::SpSubscribe` for `"blockchain.silentpayments.subscribe"` (requires the `frigate` feature) //! - [`Notification::Unknown`] for unrecognized or unsupported methods //! //! Each variant wraps a struct that contains the deserialized payload for that notification type. @@ -32,6 +33,11 @@ pub enum Notification { /// status. ScriptHash(ScriptHashNotification), + /// A notification from `"blockchain.silentpayments.subscribe"` indicating a new history + /// of transactions + #[cfg(feature = "frigate")] + SpSubscribe(SpNotification), + /// A catch-all for notifications with unrecognized methods. /// /// The original [`RawNotification`] is preserved for downstream inspection. @@ -52,6 +58,10 @@ impl Notification { "blockchain.scripthash.subscribe" => { ScriptHashNotification::deserialize(params).map(Notification::ScriptHash) } + #[cfg(feature = "frigate")] + "blockchain.silentpayments.subscribe" => { + SpNotification::deserialize(params).map(Notification::SpSubscribe) + } _ => Ok(Notification::Unknown(raw.clone())), } } @@ -102,3 +112,23 @@ impl ScriptHashNotification { self.param_1 } } + +/// An update for a Silent Payments subscription. +/// +/// Corresponds to `"blockchain.silentpayments.subscribe"` Frigate Electrum notification method. +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct SpNotification { + /// Identifies the subscription to which this notification belongs. + pub subscription: response::SpSubscribeResp, + + /// Historical scan progress from `0.0` through `1.0`. + /// + /// A value of `1.0` indicates that the scan is up to date. + pub progress: f32, + + /// Transactions discovered by the scan. + /// + /// Confirmed transactions are ordered by block height. + pub history: Vec, +} diff --git a/src/pending_request.rs b/src/pending_request.rs index 4686b9c..0397c30 100644 --- a/src/pending_request.rs +++ b/src/pending_request.rs @@ -17,7 +17,7 @@ pub trait RequestExt: Request + Sized { } macro_rules! gen_pending_request_types { - ($($name:ident),*) => { + ($($(#[$attr:meta])* $name:ident),* $(,)?) => { /// A successfully handled request and its decoded server response. /// /// This enum is returned when a request has been fully processed and the server replied @@ -33,10 +33,13 @@ macro_rules! gen_pending_request_types { /// [`Event::Response`]: crate::Event::Response #[derive(Debug, Clone)] pub enum CompletedRequest { - $($name { - req: crate::request::$name, - resp: ::Response, - }),*, + $( + $(#[$attr])* + $name { + req: crate::request::$name, + resp: ::Response, + }, + )* } /// A request that received an error response from the Electrum server. @@ -53,16 +56,24 @@ macro_rules! gen_pending_request_types { /// [`Event::ResponseError`]: crate::Event::ResponseError #[derive(Debug, Clone)] pub enum FailedRequest { - $($name { - req: crate::request::$name, - error: ResponseError, - }),*, + $( + $(#[$attr])* + $name { + req: crate::request::$name, + error: ResponseError, + }, + )* } impl core::fmt::Display for FailedRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - $(Self::$name { req, error } => write!(f, "Server responsed to {:?} with error: {}", req, error)),*, + $( + $(#[$attr])* + Self::$name { req, error } => { + write!(f, "Server responsed to {:?} with error: {}", req, error) + } + )* } } } @@ -70,6 +81,7 @@ macro_rules! gen_pending_request_types { impl std::error::Error for FailedRequest {} $( + $(#[$attr])* impl RequestExt for crate::request::$name { fn into_completed(self, resp: ::Response) -> CompletedRequest { CompletedRequest::$name { req: self, resp } @@ -97,13 +109,19 @@ gen_pending_request_types! { ScriptHashSubscribe, ScriptHashUnsubscribe, BroadcastTx, + BroadcastPackage, GetTx, GetTxMerkle, GetTxidFromPos, GetFeeHistogram, + GetMempoolInfo, + ServerVersion, Banner, + Features, Ping, - Custom + Custom, + #[cfg(feature = "frigate")] SpSubscribe, + #[cfg(feature = "frigate")] SpUnsubscribe } type Handler = diff --git a/src/request.rs b/src/request.rs index 9973529..3ff1223 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, @@ -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,38 @@ 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(), - ] - }) + ], + ) + } +} + +/// 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 estimate: potentially higher feerate, more likely to meet the target, less + /// responsive to short-term fee drops. + Conservative, + /// Economical estimate: potentially lower feerate, more responsive to short-term fee drops, may + /// take longer to confirm. + Economical, +} + +impl EstimateFeeMode { + fn as_str(&self) -> &'static str { + match self { + Self::Conservative => "CONSERVATIVE", + Self::Economical => "ECONOMICAL", + } } } @@ -239,13 +265,24 @@ impl Request for HeadersWithCheckpoint { 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) } } @@ -268,10 +305,13 @@ 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. /// -/// See: +/// 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; @@ -503,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()], ) } } @@ -589,6 +627,30 @@ 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| serialize_hex(tx).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 +669,82 @@ 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 +/// 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 range the client supports. + pub protocol_version: SupportedVersion, +} + +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).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`. +/// +/// Added in Electrum protocol v1.6 and replaces the `blockchain.relayfee` method. +/// +/// 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 @@ -656,3 +794,91 @@ impl Request for Ping { ("server.ping".into(), vec![]) } } + +/// A request to subscribe to payment outputs belonging to the provided keys +/// +/// This corresponds to the `"blockchain.silentpayments.subscribe"` Frigate Electrum RPC method. +/// The server returns the subscribed silent payment address. +/// +/// Supported Frigate version: <= 1.4.1 +/// +/// See: +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpSubscribe { + /// Private scan key sent to the server to detect matching Silent Payments outputs. + pub scan_priv_key: bitcoin::secp256k1::SecretKey, + + /// Public spend key paired with the scan key for this subscription. + pub spend_pub_key: bitcoin::secp256k1::PublicKey, + + /// Optional block height or timestamp from which to start scanning. + /// + /// Values above 500,000,000 are treated as seconds since the Unix epoch. + pub start_height: Option, + + /// Optional positive silent payment labels to scan for. + /// + /// Label `0` is scanned regardless of this value. + pub labels: Option>, +} + +#[cfg(feature = "frigate")] +impl Request for SpSubscribe { + type Response = String; + + fn to_method_and_params(&self) -> MethodAndParams { + let mut params = vec![ + serde_json::json!(self.scan_priv_key), + serde_json::json!(self.spend_pub_key), + ]; + + match (self.start_height, &self.labels) { + (Some(start_height), Some(labels)) => { + params.extend([start_height.into(), labels.clone().into()]); + } + + (Some(start_height), None) => params.push(start_height.into()), + (None, Some(labels)) => { + params.extend([serde_json::Value::Null, labels.clone().into()]); + } + (None, None) => {} + } + + ("blockchain.silentpayments.subscribe".into(), params) + } +} + +/// A request to unsubscribe from payment outputs belonging to the provided keys +/// +/// This corresponds to the `"blockchain.silentpayments.unsubscribe"` Frigate Electrum RPC method. +/// It returns the silent payment address that has been unsubscribed. This should cancel any scans +/// that may be currently running for this address. +/// +/// Supported Frigate version <= 1.4.1 +/// +/// See: +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SpUnsubscribe { + /// Private scan key identifying the subscription to cancel. + pub scan_priv_key: bitcoin::secp256k1::SecretKey, + + /// Public spend key paired with the scan key for the subscription to cancel. + pub spend_pub_key: bitcoin::secp256k1::PublicKey, +} + +#[cfg(feature = "frigate")] +impl Request for SpUnsubscribe { + type Response = String; + + fn to_method_and_params(&self) -> MethodAndParams { + ( + "blockchain.silentpayments.unsubscribe".into(), + vec![ + serde_json::json!(self.scan_priv_key), + serde_json::json!(self.spend_pub_key), + ], + ) + } +} diff --git a/src/response.rs b/src/response.rs index 7510ec4..5f14da4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -14,6 +14,30 @@ 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)] +#[serde(from = "(String, String)")] +pub struct ServerVersionResp { + /// Server software version (e.g. `"ElectrumX 1.18.0"`). + pub server_software: 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)] @@ -38,6 +62,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 +72,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 +93,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. @@ -100,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 { @@ -281,6 +313,50 @@ 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, + + /// 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. +/// +/// Provides fee-related information about the server's mempool. +/// +/// See: +#[derive(Debug, Clone, serde::Deserialize)] +pub struct MempoolInfoResp { + /// 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. + #[serde(deserialize_with = "crate::custom_serde::feerate_from_btc_per_kb")] + pub minrelaytxfee: bitcoin::FeeRate, + + /// 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. #[derive(Debug, Clone, serde::Deserialize)] pub struct ServerFeatures { @@ -318,3 +394,31 @@ pub struct ServerHostValues { /// TCP Port. pub tcp_port: Option, } + +/// Response entry from the `"blockchain.silentpayments.subscribe"` method. +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct SpSubscribeResp { + /// The silent payment address that has been subscribed. + pub address: String, + + /// An array of the labels that are subscribed to (must include 0). + pub labels: Vec, + + /// The block height from which the subscription scan was started. + pub start_height: u32, +} + +/// A transaction returned by `"blockchain.silentpayments.subscribe"` notification. +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct TxTweak { + /// The block height at which the transaction was confirmed, or `0` for a mempool transaction. + pub height: u32, + + /// The transaction hash in hexadecimal. + pub tx_hash: bitcoin::Txid, + + /// The tweak key (input_hash*A) for the transaction in compressed format. + pub tweak_key: bitcoin::secp256k1::PublicKey, +}