From 4c80f38addb8b5bbcf91381984823ddccddfa4cc Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Thu, 7 May 2026 16:13:53 +0100 Subject: [PATCH 1/4] feat: add `server.version`, `blockchain.silentpayments.subscribe` and `blockchain.silentpayments.unsubscribe` --- Cargo.toml | 1 + src/notification.rs | 37 +++++++++++++++++ src/pending_request.rs | 28 +++++++++++++ src/request.rs | 90 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) 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..9e7a8dd 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"` //! - [`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,30 @@ impl ScriptHashNotification { self.param_1 } } + +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct SpSubscription { + pub address: String, + pub labels: Vec, + pub start_height: u32, +} + +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct TxTweak { + pub height: u32, + pub tx_hash: bitcoin::Txid, + pub tweak_key: bitcoin::secp256k1::PublicKey, +} + +/// A notification indicating new confirmed transactions +/// +/// Corresponds to `"blockchain.silentpayments.subscribe"` Frigate Electrum notification method +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct SpNotification { + pub subscription: SpSubscription, + pub progress: f32, + pub history: Vec, +} diff --git a/src/pending_request.rs b/src/pending_request.rs index 4686b9c..52396c8 100644 --- a/src/pending_request.rs +++ b/src/pending_request.rs @@ -82,6 +82,7 @@ macro_rules! gen_pending_request_types { }; } +#[cfg(not(feature = "frigate"))] gen_pending_request_types! { Header, HeaderWithProof, @@ -106,6 +107,33 @@ gen_pending_request_types! { Custom } +#[cfg(feature = "frigate")] +gen_pending_request_types! { + Header, + HeaderWithProof, + Headers, + HeadersWithCheckpoint, + EstimateFee, + HeadersSubscribe, + RelayFee, + GetBalance, + GetHistory, + GetMempool, + ListUnspent, + ScriptHashSubscribe, + ScriptHashUnsubscribe, + BroadcastTx, + GetTx, + GetTxMerkle, + GetTxidFromPos, + GetFeeHistogram, + Banner, + Ping, + Version, + SpSubscribe, + SpUnSubscribe +} + type Handler = Box) -> Result, serde_json::Error> + Send + Sync>; diff --git a/src/request.rs b/src/request.rs index 9973529..e0d11cf 100644 --- a/src/request.rs +++ b/src/request.rs @@ -656,3 +656,93 @@ impl Request for Ping { ("server.ping".into(), vec![]) } } + +/// A request to establish connection with Frigate Electrum client +/// +/// This corresponds to the `"server.version"` Frigate Electrum RPC method +/// +/// See: https://github.com/sparrowwallet/frigate +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Version { + pub client_name: CowStr, + pub version: CowStr, +} + +#[cfg(feature = "frigate")] +impl Request for Version { + type Response = Vec; + + fn to_method_and_params(&self) -> MethodAndParams { + ( + "server.version".into(), + vec![self.client_name.clone().into(), self.version.clone().into()], + ) + } +} + +/// A request to subscribe to payment outputs belonging to the provided keys +/// +/// This corresponds to the `"blockchain.silentpayments.subscribe"` Frigate Electrum RPC method. +/// It returns The silent payment address that has been subscribed. +/// +/// See: https://github.com/sparrowwallet/frigate#blockchainsilentpaymentssubscribe +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpSubscribe { + pub scan_priv_key: bitcoin::secp256k1::SecretKey, + pub scan_pub_key: bitcoin::secp256k1::PublicKey, + pub start_height: Option, + 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.scan_pub_key), + ]; + + if let Some(start_height) = self.start_height { + params.push(start_height.into()); + } + + if let Some(labels) = &self.labels { + params.push(labels.clone().into()); + } + + ("blockchain.silentpayments.subscribe".into(), params) + } +} + +/// A request to unsubscribe to 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 subscribed.This should cancel any scans that +/// may be currently running for this address. +/// +/// See: https://github.com/sparrowwallet/frigate#blockchainsilentpaymentsunsubscribe +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SpUnSubscribe { + pub scan_priv_key: bitcoin::secp256k1::SecretKey, + pub scan_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.scan_pub_key), + ], + ) + } +} From 3a63c94a9031beeb53b76cd6a772b3341a0f52df Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Mon, 3 Aug 2026 09:24:35 +0100 Subject: [PATCH 2/4] fix: consistency with current implementation --- src/notification.rs | 38 +++++++++++++++++++------------------- src/response.rs | 16 ++++++++++++++++ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/notification.rs b/src/notification.rs index 9e7a8dd..7f90c64 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -113,29 +113,29 @@ impl ScriptHashNotification { } } -#[cfg(feature = "frigate")] -#[derive(Debug, Clone, serde::Deserialize)] -pub struct SpSubscription { - pub address: String, - pub labels: Vec, - pub start_height: u32, -} - -#[cfg(feature = "frigate")] -#[derive(Debug, Clone, serde::Deserialize)] -pub struct TxTweak { - pub height: u32, - pub tx_hash: bitcoin::Txid, - pub tweak_key: bitcoin::secp256k1::PublicKey, -} - /// A notification indicating new confirmed transactions /// /// Corresponds to `"blockchain.silentpayments.subscribe"` Frigate Electrum notification method #[cfg(feature = "frigate")] #[derive(Debug, Clone, serde::Deserialize)] pub struct SpNotification { - pub subscription: SpSubscription, - pub progress: f32, - pub history: Vec, + pub param_0: response::SpSubscribeResp, + pub param_1: f32, + pub param_2: Vec, +} + +#[cfg(feature = "frigate")] +impl SpNotification { + /// Returns the subscription this notification belongs to. + pub fn subscription(&self) -> &response::SpSubscribeResp { + &self.param_0 + } + /// Returns the scan progress, where `1.0` means up to date. + pub fn progress(&self) -> f32 { + self.param_1 + } + /// Returns the transactions discovered by this notification. + pub fn history(&self) -> &[response::TxTweak] { + &self.param_2 + } } diff --git a/src/response.rs b/src/response.rs index 7510ec4..a4b9b1e 100644 --- a/src/response.rs +++ b/src/response.rs @@ -318,3 +318,19 @@ pub struct ServerHostValues { /// TCP Port. pub tcp_port: Option, } + +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct SpSubscribeResp { + pub address: String, + pub labels: Vec, + pub start_height: u32, +} + +#[cfg(feature = "frigate")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct TxTweak { + pub height: u32, + pub tx_hash: bitcoin::Txid, + pub tweak_key: bitcoin::secp256k1::PublicKey, +} From 99312dbcf344f44d62861247199b7490181569ab Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Tue, 4 Aug 2026 08:58:39 +0100 Subject: [PATCH 3/4] feat: allow gen_pending_request_types to support cfg feature attribute --- 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 52396c8..5ca6915 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 df15c6bcdfc224c240cb8f5f1e822ef1855a46b5 Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Tue, 4 Aug 2026 09:05:04 +0100 Subject: [PATCH 4/4] fix: remove server.version, make comment doc clear about supported version --- src/notification.rs | 22 +++------------------- src/pending_request.rs | 31 ++----------------------------- src/request.rs | 37 +++++++++---------------------------- 3 files changed, 14 insertions(+), 76 deletions(-) diff --git a/src/notification.rs b/src/notification.rs index 7f90c64..aeed417 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -119,23 +119,7 @@ impl ScriptHashNotification { #[cfg(feature = "frigate")] #[derive(Debug, Clone, serde::Deserialize)] pub struct SpNotification { - pub param_0: response::SpSubscribeResp, - pub param_1: f32, - pub param_2: Vec, -} - -#[cfg(feature = "frigate")] -impl SpNotification { - /// Returns the subscription this notification belongs to. - pub fn subscription(&self) -> &response::SpSubscribeResp { - &self.param_0 - } - /// Returns the scan progress, where `1.0` means up to date. - pub fn progress(&self) -> f32 { - self.param_1 - } - /// Returns the transactions discovered by this notification. - pub fn history(&self) -> &[response::TxTweak] { - &self.param_2 - } + pub subscription: response::SpSubscribeResp, + pub progress: f32, + pub history: Vec, } diff --git a/src/pending_request.rs b/src/pending_request.rs index 5ca6915..493971e 100644 --- a/src/pending_request.rs +++ b/src/pending_request.rs @@ -94,7 +94,6 @@ macro_rules! gen_pending_request_types { }; } -#[cfg(not(feature = "frigate"))] gen_pending_request_types! { Header, HeaderWithProof, @@ -116,34 +115,8 @@ gen_pending_request_types! { GetFeeHistogram, Banner, Ping, - Custom -} - -#[cfg(feature = "frigate")] -gen_pending_request_types! { - Header, - HeaderWithProof, - Headers, - HeadersWithCheckpoint, - EstimateFee, - HeadersSubscribe, - RelayFee, - GetBalance, - GetHistory, - GetMempool, - ListUnspent, - ScriptHashSubscribe, - ScriptHashUnsubscribe, - BroadcastTx, - GetTx, - GetTxMerkle, - GetTxidFromPos, - GetFeeHistogram, - Banner, - Ping, - Version, - SpSubscribe, - SpUnSubscribe + #[cfg(feature = "frigate")] SpSubscribe, + #[cfg(feature = "frigate")] SpUnsubscribe } type Handler = diff --git a/src/request.rs b/src/request.rs index e0d11cf..b0043ce 100644 --- a/src/request.rs +++ b/src/request.rs @@ -657,36 +657,14 @@ impl Request for Ping { } } -/// A request to establish connection with Frigate Electrum client -/// -/// This corresponds to the `"server.version"` Frigate Electrum RPC method -/// -/// See: https://github.com/sparrowwallet/frigate -#[cfg(feature = "frigate")] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Version { - pub client_name: CowStr, - pub version: CowStr, -} - -#[cfg(feature = "frigate")] -impl Request for Version { - type Response = Vec; - - fn to_method_and_params(&self) -> MethodAndParams { - ( - "server.version".into(), - vec![self.client_name.clone().into(), self.version.clone().into()], - ) - } -} - /// A request to subscribe to payment outputs belonging to the provided keys /// /// This corresponds to the `"blockchain.silentpayments.subscribe"` Frigate Electrum RPC method. /// It returns The silent payment address that has been subscribed. /// -/// See: https://github.com/sparrowwallet/frigate#blockchainsilentpaymentssubscribe +/// Supported Frigate version: <= 1.4.1 +/// +/// See: https://github.com/sparrowwallet/frigate/tree/1.4.1#blockchainsilentpaymentssubscribe #[cfg(feature = "frigate")] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SpSubscribe { @@ -711,6 +689,7 @@ impl Request for SpSubscribe { } if let Some(labels) = &self.labels { + params.push(serde_json::Value::Null); params.push(labels.clone().into()); } @@ -724,16 +703,18 @@ impl Request for SpSubscribe { /// It returns The silent payment address that has been subscribed.This should cancel any scans that /// may be currently running for this address. /// -/// See: https://github.com/sparrowwallet/frigate#blockchainsilentpaymentsunsubscribe +/// Supported Frigate version <= 1.4.1 +/// +/// See: See: https://github.com/sparrowwallet/frigate/tree/1.4.1#blockchainsilentpaymentsunsubscribe #[cfg(feature = "frigate")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SpUnSubscribe { +pub struct SpUnsubscribe { pub scan_priv_key: bitcoin::secp256k1::SecretKey, pub scan_pub_key: bitcoin::secp256k1::PublicKey, } #[cfg(feature = "frigate")] -impl Request for SpUnSubscribe { +impl Request for SpUnsubscribe { type Response = String; fn to_method_and_params(&self) -> MethodAndParams {