#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::large_futures)]
#![warn(rustdoc::bare_urls)]
#![allow(unknown_lints)] #![allow(clippy::arc_with_non_send_sync)]
use std::collections::HashMap;
use std::future::Future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub extern crate nostr;
use async_utility::time;
use nostr::nips::nip47::{Notification, Request, Response};
use nostr_relay_pool::prelude::*;
pub mod error;
pub mod options;
pub mod prelude;
#[doc(hidden)]
pub use self::error::Error;
#[doc(hidden)]
pub use self::options::NostrWalletConnectOptions;
const ID: &str = "nwc";
const NOTIFICATIONS_ID: &str = "nwc-notifications";
#[derive(Debug, Clone)]
pub struct NWC {
uri: NostrWalletConnectURI,
pool: RelayPool,
opts: NostrWalletConnectOptions,
bootstrapped: Arc<AtomicBool>,
notifications_subscribed: Arc<AtomicBool>,
}
impl NWC {
#[inline]
pub fn new(uri: NostrWalletConnectURI) -> Self {
Self::with_opts(uri, NostrWalletConnectOptions::default())
}
pub fn with_opts(uri: NostrWalletConnectURI, opts: NostrWalletConnectOptions) -> Self {
let pool = match opts.monitor.as_ref() {
Some(monitor) => RelayPool::builder().monitor(monitor.clone()).build(),
None => RelayPool::default(),
};
Self {
uri,
pool,
opts,
bootstrapped: Arc::new(AtomicBool::new(false)),
notifications_subscribed: Arc::new(AtomicBool::new(false)),
}
}
pub async fn status(&self) -> HashMap<RelayUrl, RelayStatus> {
let relays = self.pool.relays().await;
relays.into_iter().map(|(u, r)| (u, r.status())).collect()
}
async fn bootstrap(&self) -> Result<(), Error> {
if self.bootstrapped.load(Ordering::SeqCst) {
return Ok(());
}
for url in self.uri.relays.iter() {
self.pool.add_relay(url, self.opts.relay.clone()).await?;
}
self.pool.connect().await;
let filter = Filter::new()
.author(self.uri.public_key)
.kind(Kind::WalletConnectResponse)
.limit(0);
self.pool
.subscribe_with_id(SubscriptionId::new(ID), filter, SubscribeOptions::default())
.await?;
self.bootstrapped.store(true, Ordering::SeqCst);
Ok(())
}
async fn send_request(&self, req: Request) -> Result<Response, Error> {
self.bootstrap().await?;
tracing::debug!("Sending request '{}'", req.as_json());
let event: Event = req.to_event(&self.uri)?;
let mut notifications = self.pool.notifications();
let output: Output<EventId> = self.pool.send_event(&event).await?;
time::timeout(Some(self.opts.timeout), async {
while let Ok(notification) = notifications.recv().await {
if let RelayPoolNotification::Event { event, .. } = notification {
if event.kind == Kind::WalletConnectResponse
&& event.tags.event_ids().next() == Some(output.id())
{
return Ok(Response::from_event(&self.uri, &event)?);
}
}
}
Err(Error::PrematureExit)
})
.await
.ok_or(Error::Timeout)?
}
pub async fn pay_invoice(
&self,
request: PayInvoiceRequest,
) -> Result<PayInvoiceResponse, Error> {
let req = Request::pay_invoice(request);
let res: Response = self.send_request(req).await?;
Ok(res.to_pay_invoice()?)
}
pub async fn pay_keysend(
&self,
request: PayKeysendRequest,
) -> Result<PayKeysendResponse, Error> {
let req = Request::pay_keysend(request);
let res: Response = self.send_request(req).await?;
Ok(res.to_pay_keysend()?)
}
pub async fn make_invoice(
&self,
request: MakeInvoiceRequest,
) -> Result<MakeInvoiceResponse, Error> {
let req: Request = Request::make_invoice(request);
let res: Response = self.send_request(req).await?;
Ok(res.to_make_invoice()?)
}
pub async fn lookup_invoice(
&self,
request: LookupInvoiceRequest,
) -> Result<LookupInvoiceResponse, Error> {
let req = Request::lookup_invoice(request);
let res: Response = self.send_request(req).await?;
Ok(res.to_lookup_invoice()?)
}
pub async fn list_transactions(
&self,
params: ListTransactionsRequest,
) -> Result<Vec<LookupInvoiceResponse>, Error> {
let req = Request::list_transactions(params);
let res: Response = self.send_request(req).await?;
Ok(res.to_list_transactions()?)
}
pub async fn get_balance(&self) -> Result<u64, Error> {
let req = Request::get_balance();
let res: Response = self.send_request(req).await?;
let GetBalanceResponse { balance } = res.to_get_balance()?;
Ok(balance)
}
pub async fn get_info(&self) -> Result<GetInfoResponse, Error> {
let req = Request::get_info();
let res: Response = self.send_request(req).await?;
Ok(res.to_get_info()?)
}
pub async fn subscribe_to_notifications(&self) -> Result<(), Error> {
if self.notifications_subscribed.load(Ordering::SeqCst) {
tracing::debug!("Already subscribed to notifications");
return Ok(());
}
tracing::info!("Subscribing to wallet notifications...");
self.bootstrap().await?;
let client_keys = Keys::new(self.uri.secret.clone());
let client_pubkey = client_keys.public_key();
tracing::debug!("Client pubkey: {}", client_pubkey);
tracing::debug!("Wallet service pubkey: {}", self.uri.public_key);
let notification_filter = Filter::new()
.author(self.uri.public_key)
.pubkey(client_pubkey)
.kind(Kind::WalletConnectNotification)
.since(Timestamp::now());
tracing::debug!("Notification filter: {:?}", notification_filter);
self.pool
.subscribe_with_id(
SubscriptionId::new(NOTIFICATIONS_ID),
notification_filter,
SubscribeOptions::default(),
)
.await?;
self.notifications_subscribed.store(true, Ordering::SeqCst);
tracing::info!("Successfully subscribed to notifications");
Ok(())
}
pub async fn handle_notifications<F, Fut>(&self, func: F) -> Result<(), Error>
where
F: Fn(Notification) -> Fut,
Fut: Future<Output = Result<bool>>,
{
let mut notifications = self.pool.notifications();
while let Ok(notification) = notifications.recv().await {
tracing::trace!("Received relay pool notification: {:?}", notification);
match notification {
RelayPoolNotification::Event {
subscription_id,
event,
..
} => {
tracing::debug!(
"Received event: kind={}, author={}, id={}",
event.kind,
event.pubkey,
event.id
);
if subscription_id.as_str() != NOTIFICATIONS_ID {
tracing::trace!("Ignoring event with subscription id: {}", subscription_id);
continue;
}
if event.kind != Kind::WalletConnectNotification {
tracing::trace!("Ignoring event with kind: {}", event.kind);
continue;
}
tracing::info!("Processing wallet notification event");
match Notification::from_event(&self.uri, &event) {
Ok(nip47_notification) => {
tracing::info!(
"Successfully parsed notification: {:?}",
nip47_notification.notification_type
);
let exit: bool = func(nip47_notification)
.await
.map_err(|e| Error::Handler(e.to_string()))?;
if exit {
break;
}
}
Err(e) => {
tracing::error!("Failed to parse notification: {}", e);
tracing::debug!("Event content: {}", event.content);
return Err(Error::from(e));
}
}
}
RelayPoolNotification::Shutdown => break,
_ => {}
}
}
Ok(())
}
pub async fn unsubscribe_from_notifications(&self) -> Result<(), Error> {
self.pool
.unsubscribe(&SubscriptionId::new(NOTIFICATIONS_ID))
.await;
self.notifications_subscribed.store(false, Ordering::SeqCst);
Ok(())
}
#[inline]
pub async fn shutdown(self) {
self.pool.disconnect().await
}
}