From 0f8e1bec84a90f598ae7ebbfaa373324a640515b Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Mon, 31 Aug 2026 13:47:59 +0100 Subject: [PATCH 1/2] feat: support attributes in gen_pending_request_types! --- src/pending_request.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/pending_request.rs b/src/pending_request.rs index 4686b9c..f48814e 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 } From b06246bed3524fca1d910012a411737851ddb495 Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Mon, 31 Aug 2026 13:48:31 +0100 Subject: [PATCH 2/2] feat: add Frigate Silent Payments RPC support --- Cargo.toml | 1 + src/notification.rs | 30 ++++++++++++++ src/pending_request.rs | 4 +- src/request.rs | 88 ++++++++++++++++++++++++++++++++++++++++++ src/response.rs | 28 ++++++++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) 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/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 f48814e..3a3b435 100644 --- a/src/pending_request.rs +++ b/src/pending_request.rs @@ -115,7 +115,9 @@ gen_pending_request_types! { GetFeeHistogram, Banner, 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..99609a7 100644 --- a/src/request.rs +++ b/src/request.rs @@ -656,3 +656,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..14f2cc4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -318,3 +318,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, +}