Skip to content

Commit 684c695

Browse files
frnanduyukibtc
authored andcommitted
nwc: add notifications support
Closes #952 Pull-Request: #953 Signed-off-by: Yuki Kishimoto <[email protected]>
1 parent 66b588a commit 684c695

File tree

7 files changed

+229
-3
lines changed

7 files changed

+229
-3
lines changed

crates/nostr/src/event/kind.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ kind_variants! {
123123
Authentication => 22242, "Client Authentication", "<https://github.com/nostr-protocol/nips/blob/master/42.md>",
124124
WalletConnectRequest => 23194, "Wallet Connect Request", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
125125
WalletConnectResponse => 23195, "Wallet Connect Response", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
126+
WalletConnectNotification => 23196, "Wallet Connect Notification", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
126127
NostrConnect => 24133, "Nostr Connect", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
127128
LiveEvent => 30311, "Live Event", "<https://github.com/nostr-protocol/nips/blob/master/53.md>",
128129
LiveEventMessage => 1311, "Live Event Message", "<https://github.com/nostr-protocol/nips/blob/master/53.md>",

crates/nwc/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
2424
-->
2525

26+
## Unreleased
27+
28+
### Added
29+
30+
- Add notification support for real-time payment updates (https://github.com/rust-nostr/nostr/pull/953)
31+
2632
## v0.42.0 - 2025/05/20
2733

2834
No notable changes in this release.

crates/nwc/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ nostr-relay-pool.workspace = true
2222
tracing = { workspace = true, features = ["std"] }
2323

2424
[dev-dependencies]
25-
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
26-
tracing-subscriber.workspace = true
25+
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
26+
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }

crates/nwc/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
NWC client and zapper backend for Nostr apps
66

7+
## Notifications Support
8+
9+
NWC supports real-time payment notifications as specified in NIP-47.
10+
11+
This allows applications to receive instant updates when payments are sent or received.
12+
See [`examples/notifications.rs`](examples/notifications.rs) for a simple example of how to use this feature.
13+
714
## Changelog
815

916
All notable changes to this library are documented in the [CHANGELOG.md](CHANGELOG.md).
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) 2022-2023 Yuki Kishimoto
2+
// Copyright (c) 2023-2025 Rust Nostr Developers
3+
// Distributed under the MIT software license
4+
5+
use std::env;
6+
use std::str::FromStr;
7+
8+
use nostr::nips::nip47::{NotificationType, PaymentNotification};
9+
use nwc::prelude::*;
10+
11+
#[tokio::main]
12+
async fn main() -> Result<()> {
13+
let log_level = env::var("RUST_LOG")
14+
.unwrap_or_else(|_| "info,nwc=debug,nostr_relay_pool=debug".to_string());
15+
16+
tracing_subscriber::fmt()
17+
.with_env_filter(log_level)
18+
.with_target(true)
19+
.with_line_number(true)
20+
.init();
21+
22+
let uri_str = env::args()
23+
.nth(1)
24+
.or_else(|| env::var("NWC_URI").ok())
25+
.ok_or("Please provide NWC URI as argument or set NWC_URI environment variable")?;
26+
27+
let uri =
28+
NostrWalletConnectURI::from_str(&uri_str).map_err(|e| format!("Invalid NWC URI: {}", e))?;
29+
30+
println!("📡 Relay: {:?}", uri.relays);
31+
println!("🔑 Wallet pubkey: {}", uri.public_key);
32+
33+
println!("🔗 Connecting to wallet service...");
34+
35+
let nwc = NWC::new(uri);
36+
37+
nwc.subscribe_to_notifications().await?;
38+
39+
println!("✅ NWC client started and listening for notifications...");
40+
println!("📱 Wallet notifications will appear here in real-time");
41+
println!("💡 Try making or receiving payments with your wallet");
42+
println!("🛑 Press Ctrl+C to stop");
43+
44+
let shutdown = tokio::signal::ctrl_c();
45+
tokio::pin!(shutdown);
46+
47+
tokio::select! {
48+
_ = shutdown => {
49+
println!("\n👋 Shutting down...");
50+
}
51+
52+
result = nwc.handle_notifications(|notification| async move {
53+
match notification.notification_type {
54+
NotificationType::PaymentReceived => {
55+
if let Ok(payment) = notification.to_pay_notification() {
56+
println!("🟢 Payment Received!");
57+
print_payment_details(&payment);
58+
}
59+
}
60+
NotificationType::PaymentSent => {
61+
if let Ok(payment) = notification.to_pay_notification() {
62+
println!("🔴 Payment Sent!");
63+
print_payment_details(&payment);
64+
}
65+
}
66+
}
67+
Ok(false) // Continue processing
68+
}) => {
69+
if let Err(e) = result {
70+
eprintln!("Error handling notifications: {}", e);
71+
}
72+
}
73+
}
74+
75+
nwc.unsubscribe_from_notifications().await?;
76+
77+
nwc.shutdown().await;
78+
79+
Ok(())
80+
}
81+
82+
fn print_payment_details(payment: &PaymentNotification) {
83+
println!(" 💰 Amount: {} msat", payment.amount);
84+
if let Some(description) = &payment.description {
85+
println!(" 📝 Description: {}", description);
86+
}
87+
println!(" 🔗 Payment Hash: {}", payment.payment_hash);
88+
println!(" 📅 Settled At: {}", payment.settled_at);
89+
if payment.fees_paid > 0 {
90+
println!(" 💸 Fees: {} msat", payment.fees_paid);
91+
}
92+
println!();
93+
}

crates/nwc/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ pub enum Error {
2020
PrematureExit,
2121
/// Request timeout
2222
Timeout,
23+
/// Handler error
24+
Handler(String),
2325
}
2426

2527
impl std::error::Error for Error {}
@@ -31,6 +33,7 @@ impl fmt::Display for Error {
3133
Self::Pool(e) => write!(f, "{e}"),
3234
Self::PrematureExit => write!(f, "premature exit"),
3335
Self::Timeout => write!(f, "timeout"),
36+
Self::Handler(e) => write!(f, "handler error: {e}"),
3437
}
3538
}
3639
}

crates/nwc/src/lib.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
#![allow(clippy::arc_with_non_send_sync)]
1313

1414
use std::collections::HashMap;
15+
use std::future::Future;
1516
use std::sync::atomic::{AtomicBool, Ordering};
1617
use std::sync::Arc;
1718

1819
pub extern crate nostr;
1920

2021
use async_utility::time;
21-
use nostr::nips::nip47::{Request, Response};
22+
use nostr::nips::nip47::{Notification, Request, Response};
2223
use nostr_relay_pool::prelude::*;
2324

2425
pub mod error;
@@ -31,6 +32,7 @@ pub use self::error::Error;
3132
pub use self::options::NostrWalletConnectOptions;
3233

3334
const ID: &str = "nwc";
35+
const NOTIFICATIONS_ID: &str = "nwc-notifications";
3436

3537
/// Nostr Wallet Connect client
3638
#[derive(Debug, Clone)]
@@ -39,6 +41,7 @@ pub struct NWC {
3941
pool: RelayPool,
4042
opts: NostrWalletConnectOptions,
4143
bootstrapped: Arc<AtomicBool>,
44+
notifications_subscribed: Arc<AtomicBool>,
4245
}
4346

4447
impl NWC {
@@ -55,6 +58,7 @@ impl NWC {
5558
pool: RelayPool::default(),
5659
opts,
5760
bootstrapped: Arc::new(AtomicBool::new(false)),
61+
notifications_subscribed: Arc::new(AtomicBool::new(false)),
5862
}
5963
}
6064

@@ -191,6 +195,118 @@ impl NWC {
191195
Ok(res.to_get_info()?)
192196
}
193197

198+
/// Subscribe to wallet notifications
199+
pub async fn subscribe_to_notifications(&self) -> Result<(), Error> {
200+
if self.notifications_subscribed.load(Ordering::SeqCst) {
201+
tracing::debug!("Already subscribed to notifications");
202+
return Ok(());
203+
}
204+
205+
tracing::info!("Subscribing to wallet notifications...");
206+
207+
self.bootstrap().await?;
208+
209+
let client_keys = Keys::new(self.uri.secret.clone());
210+
let client_pubkey = client_keys.public_key();
211+
212+
tracing::debug!("Client pubkey: {}", client_pubkey);
213+
tracing::debug!("Wallet service pubkey: {}", self.uri.public_key);
214+
215+
let notification_filter = Filter::new()
216+
.author(self.uri.public_key)
217+
.pubkey(client_pubkey)
218+
.kind(Kind::WalletConnectNotification)
219+
.since(Timestamp::now());
220+
221+
tracing::debug!("Notification filter: {:?}", notification_filter);
222+
223+
self.pool
224+
.subscribe_with_id(
225+
SubscriptionId::new(NOTIFICATIONS_ID),
226+
notification_filter,
227+
SubscribeOptions::default(),
228+
)
229+
.await?;
230+
231+
self.notifications_subscribed.store(true, Ordering::SeqCst);
232+
233+
tracing::info!("Successfully subscribed to notifications");
234+
Ok(())
235+
}
236+
237+
/// Handle incoming notifications with a callback function
238+
pub async fn handle_notifications<F, Fut>(&self, func: F) -> Result<(), Error>
239+
where
240+
F: Fn(Notification) -> Fut,
241+
Fut: Future<Output = Result<bool>>,
242+
{
243+
let mut notifications = self.pool.notifications();
244+
245+
while let Ok(notification) = notifications.recv().await {
246+
tracing::trace!("Received relay pool notification: {:?}", notification);
247+
248+
match notification {
249+
RelayPoolNotification::Event {
250+
subscription_id,
251+
event,
252+
..
253+
} => {
254+
tracing::debug!(
255+
"Received event: kind={}, author={}, id={}",
256+
event.kind,
257+
event.pubkey,
258+
event.id
259+
);
260+
261+
if subscription_id.as_str() != NOTIFICATIONS_ID {
262+
tracing::trace!("Ignoring event with subscription id: {}", subscription_id);
263+
continue;
264+
}
265+
266+
if event.kind != Kind::WalletConnectNotification {
267+
tracing::trace!("Ignoring event with kind: {}", event.kind);
268+
continue;
269+
}
270+
271+
tracing::info!("Processing wallet notification event");
272+
273+
match Notification::from_event(&self.uri, &event) {
274+
Ok(nip47_notification) => {
275+
tracing::info!(
276+
"Successfully parsed notification: {:?}",
277+
nip47_notification.notification_type
278+
);
279+
let exit: bool = func(nip47_notification)
280+
.await
281+
.map_err(|e| Error::Handler(e.to_string()))?;
282+
if exit {
283+
break;
284+
}
285+
}
286+
Err(e) => {
287+
tracing::error!("Failed to parse notification: {}", e);
288+
tracing::debug!("Event content: {}", event.content);
289+
return Err(Error::from(e));
290+
}
291+
}
292+
}
293+
RelayPoolNotification::Shutdown => break,
294+
_ => {}
295+
}
296+
}
297+
298+
Ok(())
299+
}
300+
301+
/// Unsubscribe from notifications
302+
pub async fn unsubscribe_from_notifications(&self) -> Result<(), Error> {
303+
self.pool
304+
.unsubscribe(&SubscriptionId::new(NOTIFICATIONS_ID))
305+
.await;
306+
self.notifications_subscribed.store(false, Ordering::SeqCst);
307+
Ok(())
308+
}
309+
194310
/// Completely shutdown [NWC] client
195311
#[inline]
196312
pub async fn shutdown(self) {

0 commit comments

Comments
 (0)