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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions src/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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())),
}
}
Expand Down Expand Up @@ -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 {
Comment thread
sdmg15 marked this conversation as resolved.
/// 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<response::TxTweak>,
Comment thread
sdmg15 marked this conversation as resolved.
}
36 changes: 25 additions & 11 deletions src/pending_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: <crate::request::$name as Request>::Response,
}),*,
$(
$(#[$attr])*
$name {
req: crate::request::$name,
resp: <crate::request::$name as Request>::Response,
},
)*
}

/// A request that received an error response from the Electrum server.
Expand All @@ -53,23 +56,32 @@ 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)
}
)*
}
}
}

impl std::error::Error for FailedRequest {}

$(
$(#[$attr])*
impl RequestExt for crate::request::$name {
fn into_completed(self, resp: <Self as Request>::Response) -> CompletedRequest {
CompletedRequest::$name { req: self, resp }
Expand Down Expand Up @@ -103,7 +115,9 @@ gen_pending_request_types! {
GetFeeHistogram,
Banner,
Ping,
Custom
Comment thread
noahjoeris marked this conversation as resolved.
Custom,
#[cfg(feature = "frigate")] SpSubscribe,
#[cfg(feature = "frigate")] SpUnsubscribe
}

type Handler =
Expand Down
88 changes: 88 additions & 0 deletions src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://github.com/sparrowwallet/frigate/tree/1.4.1#blockchainsilentpaymentssubscribe>
#[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<u32>,
Comment thread
sdmg15 marked this conversation as resolved.

/// Optional positive silent payment labels to scan for.
///
/// Label `0` is scanned regardless of this value.
pub labels: Option<Vec<u32>>,
}

#[cfg(feature = "frigate")]
impl Request for SpSubscribe {
type Response = String;
Comment thread
sdmg15 marked this conversation as resolved.

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: <https://github.com/sparrowwallet/frigate/tree/1.4.1#blockchainsilentpaymentsunsubscribe>
#[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),
],
)
}
}
28 changes: 28 additions & 0 deletions src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,31 @@ pub struct ServerHostValues {
/// TCP Port.
pub tcp_port: Option<u16>,
}

/// 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<u32>,

/// The block height from which the subscription scan was started.
pub start_height: u32,
Comment thread
sdmg15 marked this conversation as resolved.
}

/// 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,
}
Comment thread
sdmg15 marked this conversation as resolved.