From 6300377fbc6ff40b215315fb2ae7128047aa8c20 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 16 Feb 2024 12:22:16 +0100 Subject: [PATCH 1/6] Add `nostr-zapper` crate --- crates/nostr-zapper/Cargo.toml | 17 ++++ crates/nostr-zapper/README.md | 15 +++ crates/nostr-zapper/src/error.rs | 48 +++++++++ crates/nostr-zapper/src/lib.rs | 158 +++++++++++++++++++++++++++++ crates/nostr-zapper/src/prelude.rs | 11 ++ 5 files changed, 249 insertions(+) create mode 100644 crates/nostr-zapper/Cargo.toml create mode 100644 crates/nostr-zapper/README.md create mode 100644 crates/nostr-zapper/src/error.rs create mode 100644 crates/nostr-zapper/src/lib.rs create mode 100644 crates/nostr-zapper/src/prelude.rs diff --git a/crates/nostr-zapper/Cargo.toml b/crates/nostr-zapper/Cargo.toml new file mode 100644 index 000000000..c9d5970ed --- /dev/null +++ b/crates/nostr-zapper/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nostr-zapper" +version = "0.27.0" +edition = "2021" +description = "Zapper abstraction for Nostr apps" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +readme = "README.md" +rust-version.workspace = true +keywords = ["nostr", "zapper"] + +[dependencies] +async-trait.workspace = true +nostr = { workspace = true, features = ["std"] } +thiserror.workspace = true \ No newline at end of file diff --git a/crates/nostr-zapper/README.md b/crates/nostr-zapper/README.md new file mode 100644 index 000000000..48f50d218 --- /dev/null +++ b/crates/nostr-zapper/README.md @@ -0,0 +1,15 @@ +# Nostr Zapper + +Zapper abstraction for Nostr apps + +## State + +**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. + +## Donations + +`rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). + +## License + +This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details \ No newline at end of file diff --git a/crates/nostr-zapper/src/error.rs b/crates/nostr-zapper/src/error.rs new file mode 100644 index 000000000..0f4521146 --- /dev/null +++ b/crates/nostr-zapper/src/error.rs @@ -0,0 +1,48 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! Nostr Zapper Error + +use thiserror::Error; + +/// Zapper Error +#[derive(Debug, Error)] +pub enum ZapperError { + /// An error happened in the underlying zapper backend. + #[error("zapper: {0}")] + Backend(Box), + /// Nostr error + #[error("nostr: {0}")] + Nostr(Box), + /// Not supported + #[error("method not supported by current backend")] + NotSupported, + /// Feature disabled + #[error("feature disabled for current backend")] + FeatureDisabled, +} + +impl ZapperError { + /// Create a new `Backend` error. + /// + /// Shorthand for `Error::Backend(Box::new(error))`. + #[inline] + pub fn backend(error: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::Backend(Box::new(error)) + } + + /// Create a new `Nostr` error. + /// + /// Shorthand for `Error::Nostr(Box::new(error))`. + #[inline] + pub fn nostr(error: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::Nostr(Box::new(error)) + } +} diff --git a/crates/nostr-zapper/src/lib.rs b/crates/nostr-zapper/src/lib.rs new file mode 100644 index 000000000..9077b095b --- /dev/null +++ b/crates/nostr-zapper/src/lib.rs @@ -0,0 +1,158 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! Nostr Zapper + +#![warn(missing_docs)] +#![warn(rustdoc::bare_urls)] +#![allow(unknown_lints)] + +use core::fmt; +use std::sync::Arc; + +pub extern crate nostr; + +pub use async_trait::async_trait; +use nostr::prelude::*; + +pub mod error; +pub mod prelude; + +pub use self::error::ZapperError; + +/// Backend +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ZapperBackend { + /// WebLN + WebLN, + /// Nostr Wallet Connect + NWC, + /// Custom + Custom(String), +} + +/// A type-erased [`NostrZapper`]. +pub type DynNostrZapper = dyn NostrZapper; + +/// A type that can be type-erased into `Arc`. +pub trait IntoNostrZapper { + #[doc(hidden)] + fn into_nostr_zapper(self) -> Arc; +} + +impl IntoNostrZapper for Arc { + fn into_nostr_zapper(self) -> Arc { + self + } +} + +impl IntoNostrZapper for T +where + T: NostrZapper + Sized + 'static, +{ + fn into_nostr_zapper(self) -> Arc { + Arc::new(EraseNostrZapperError(self)) + } +} + +// Turns a given `Arc` into `Arc` by attaching the +// NostrZapper impl vtable of `EraseNostrZapperError`. +impl IntoNostrZapper for Arc +where + T: NostrZapper + 'static, +{ + fn into_nostr_zapper(self) -> Arc { + let ptr: *const T = Arc::into_raw(self); + let ptr_erased = ptr as *const EraseNostrZapperError; + // SAFETY: EraseNostrZapperError is repr(transparent) so T and + // EraseNostrZapperError have the same layout and ABI + unsafe { Arc::from_raw(ptr_erased) } + } +} + +/// Nostr Database +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait NostrZapper: AsyncTraitDeps { + /// Error + type Err: From + Into; + + /// Name of the backend zapper used (ex. WebLN, NWC, ...) + fn backend(&self) -> ZapperBackend; + + /// Pay invoice + async fn pay_invoice(&self, invoice: String) -> Result<(), Self::Err>; + + /// Pay multiple invoices + async fn pay_multi_invoices(&self, invoices: Vec) -> Result<(), Self::Err> { + for invoice in invoices.into_iter() { + self.pay_invoice(invoice).await?; + } + Ok(()) + } +} + +#[repr(transparent)] +struct EraseNostrZapperError(T); + +impl fmt::Debug for EraseNostrZapperError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl NostrZapper for EraseNostrZapperError { + type Err = ZapperError; + + fn backend(&self) -> ZapperBackend { + self.0.backend() + } + + async fn pay_invoice(&self, invoice: String) -> Result<(), Self::Err> { + self.0.pay_invoice(invoice).await.map_err(Into::into) + } + + async fn pay_multi_invoices(&self, invoices: Vec) -> Result<(), Self::Err> { + self.0 + .pay_multi_invoices(invoices) + .await + .map_err(Into::into) + } +} + +/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(not(target_arch = "wasm32"))] +pub trait SendOutsideWasm: Send {} +#[cfg(not(target_arch = "wasm32"))] +impl SendOutsideWasm for T {} + +/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(target_arch = "wasm32")] +pub trait SendOutsideWasm {} +#[cfg(target_arch = "wasm32")] +impl SendOutsideWasm for T {} + +/// Alias for `Sync` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(not(target_arch = "wasm32"))] +pub trait SyncOutsideWasm: Sync {} +#[cfg(not(target_arch = "wasm32"))] +impl SyncOutsideWasm for T {} + +/// Alias for `Sync` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(target_arch = "wasm32")] +pub trait SyncOutsideWasm {} +#[cfg(target_arch = "wasm32")] +impl SyncOutsideWasm for T {} + +/// Super trait that is used for our store traits, this trait will differ if +/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have +/// implemented, while other targets will. +pub trait AsyncTraitDeps: std::fmt::Debug + SendOutsideWasm + SyncOutsideWasm {} +impl AsyncTraitDeps for T {} diff --git a/crates/nostr-zapper/src/prelude.rs b/crates/nostr-zapper/src/prelude.rs new file mode 100644 index 000000000..c948bda71 --- /dev/null +++ b/crates/nostr-zapper/src/prelude.rs @@ -0,0 +1,11 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! Prelude + +#![allow(unknown_lints)] +#![allow(ambiguous_glob_reexports)] +#![doc(hidden)] + +pub use crate::*; From 19c4415e3b14307a396a7432c33c2063812e66d0 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 16 Feb 2024 12:23:13 +0100 Subject: [PATCH 2/6] Add `nostr-webln` --- crates/nostr-webln/Cargo.toml | 16 +++++++ crates/nostr-webln/README.md | 15 +++++++ crates/nostr-webln/src/lib.rs | 81 +++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 crates/nostr-webln/Cargo.toml create mode 100644 crates/nostr-webln/README.md create mode 100644 crates/nostr-webln/src/lib.rs diff --git a/crates/nostr-webln/Cargo.toml b/crates/nostr-webln/Cargo.toml new file mode 100644 index 000000000..14d81fb51 --- /dev/null +++ b/crates/nostr-webln/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nostr-webln" +version = "0.27.0" +edition = "2021" +description = "WebLN zapper backend for Nostr apps" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +readme = "README.md" +rust-version.workspace = true +keywords = ["nostr", "zapper", "webln"] + +[dependencies] +nostr-zapper.workspace = true +webln = "0.1" \ No newline at end of file diff --git a/crates/nostr-webln/README.md b/crates/nostr-webln/README.md new file mode 100644 index 000000000..df7062677 --- /dev/null +++ b/crates/nostr-webln/README.md @@ -0,0 +1,15 @@ +# Nostr WebLN Zapper + +WebLN zapper backend for Nostr apps + +## State + +**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. + +## Donations + +`rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). + +## License + +This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details \ No newline at end of file diff --git a/crates/nostr-webln/src/lib.rs b/crates/nostr-webln/src/lib.rs new file mode 100644 index 000000000..3388c8290 --- /dev/null +++ b/crates/nostr-webln/src/lib.rs @@ -0,0 +1,81 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! WebLN zapper backend for Nostr apps + +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(rustdoc::bare_urls)] +#![allow(unknown_lints, clippy::arc_with_non_send_sync)] +#![cfg_attr(not(target_arch = "wasm32"), allow(unused))] + +pub extern crate nostr_zapper as zapper; +pub extern crate webln; + +use std::ops::Deref; + +#[cfg(target_arch = "wasm32")] +use nostr_zapper::NostrZapper; +use nostr_zapper::{ZapperBackend, ZapperError}; +use webln::WebLN; + +/// [WebLN] zapper +#[derive(Debug, Clone)] +pub struct WebLNZapper { + inner: WebLN, +} + +impl Deref for WebLNZapper { + type Target = WebLN; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl WebLNZapper { + /// New [WebLN] zapper + /// + /// Internally, automatically call `webln.enable()`. + pub async fn new() -> Result { + let inner = WebLN::new().map_err(ZapperError::backend)?; + inner.enable().await.map_err(ZapperError::backend)?; + Ok(Self { inner }) + } +} + +#[cfg(target_arch = "wasm32")] +macro_rules! impl_nostr_zapper { + ({ $($body:tt)* }) => { + #[nostr_zapper::async_trait(?Send)] + impl NostrZapper for WebLNZapper { + type Err = ZapperError; + + $($body)* + } + }; +} + +#[cfg(not(target_arch = "wasm32"))] +macro_rules! impl_nostr_zapper { + ({ $($body:tt)* }) => { + impl WebLNZapper { + $($body)* + } + }; +} + +impl_nostr_zapper!({ + fn backend(&self) -> ZapperBackend { + ZapperBackend::WebLN + } + + async fn pay_invoice(&self, invoice: String) -> Result<(), ZapperError> { + self.inner + .send_payment(invoice) + .await + .map_err(ZapperError::backend)?; + Ok(()) + } +}); From f28bacb752552d1468427d9ac7b91fb2a068e51a Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 16 Feb 2024 12:26:10 +0100 Subject: [PATCH 3/6] Add `nwc` crate --- crates/nostr-sdk/examples/nip47.rs | 2 + crates/nwc/Cargo.toml | 24 ++++ crates/nwc/README.md | 15 +++ crates/nwc/examples/nwc.rs | 36 ++++++ crates/nwc/src/error.rs | 35 ++++++ crates/nwc/src/lib.rs | 196 +++++++++++++++++++++++++++++ crates/nwc/src/options.rs | 41 ++++++ crates/nwc/src/prelude.rs | 14 +++ 8 files changed, 363 insertions(+) create mode 100644 crates/nwc/Cargo.toml create mode 100644 crates/nwc/README.md create mode 100644 crates/nwc/examples/nwc.rs create mode 100644 crates/nwc/src/error.rs create mode 100644 crates/nwc/src/lib.rs create mode 100644 crates/nwc/src/options.rs create mode 100644 crates/nwc/src/prelude.rs diff --git a/crates/nostr-sdk/examples/nip47.rs b/crates/nostr-sdk/examples/nip47.rs index 07423ae8f..322e91915 100644 --- a/crates/nostr-sdk/examples/nip47.rs +++ b/crates/nostr-sdk/examples/nip47.rs @@ -5,6 +5,8 @@ use std::str::FromStr; use nostr_sdk::prelude::*; +// Check `nwc` crate for high level client library! + #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); diff --git a/crates/nwc/Cargo.toml b/crates/nwc/Cargo.toml new file mode 100644 index 000000000..39b6cb0bd --- /dev/null +++ b/crates/nwc/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "nwc" +version = "0.27.0" +edition = "2021" +description = "NWC client and zapper backend for Nostr apps" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +readme = "README.md" +rust-version.workspace = true +keywords = ["nostr", "zapper", "nwc"] + +[dependencies] +async-utility.workspace = true +nostr = { workspace = true, features = ["std", "nip47"] } +nostr-relay-pool.workspace = true +nostr-zapper.workspace = true +thiserror.workspace = true +tracing = { workspace = true, features = ["std"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tracing-subscriber.workspace = true \ No newline at end of file diff --git a/crates/nwc/README.md b/crates/nwc/README.md new file mode 100644 index 000000000..06dd94b98 --- /dev/null +++ b/crates/nwc/README.md @@ -0,0 +1,15 @@ +# NWC + +NWC client and zapper backend for Nostr apps + +## State + +**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. + +## Donations + +`rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). + +## License + +This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details \ No newline at end of file diff --git a/crates/nwc/examples/nwc.rs b/crates/nwc/examples/nwc.rs new file mode 100644 index 000000000..0184c8eab --- /dev/null +++ b/crates/nwc/examples/nwc.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +use std::str::FromStr; + +use nwc::prelude::*; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let mut nwc_uri_string = String::new(); + let mut invoice = String::new(); + + println!("Please enter a NWC string"); + std::io::stdin() + .read_line(&mut nwc_uri_string) + .expect("Failed to read line"); + + println!("Please enter a BOLT 11 invoice"); + std::io::stdin() + .read_line(&mut invoice) + .expect("Failed to read line"); + + invoice = String::from(invoice.trim()); + + // Parse URI and compose NWC client + let uri = NostrWalletConnectURI::from_str(&nwc_uri_string).expect("Failed to parse NWC URI"); + let nwc = NWC::new(uri).await?; + + // Pay invoice + nwc.send_payment(invoice).await?; + + Ok(()) +} diff --git a/crates/nwc/src/error.rs b/crates/nwc/src/error.rs new file mode 100644 index 000000000..e0ff995c9 --- /dev/null +++ b/crates/nwc/src/error.rs @@ -0,0 +1,35 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! NWC error + +use nostr::nips::nip47; +use nostr_zapper::ZapperError; +use thiserror::Error; + +/// NWC error +#[derive(Debug, Error)] +pub enum Error { + /// Zapper error + #[error(transparent)] + Zapper(#[from] ZapperError), + /// NIP47 error + #[error(transparent)] + NIP47(#[from] nip47::Error), + /// Relay + #[error(transparent)] + Relay(#[from] nostr_relay_pool::relay::Error), + /// Pool + #[error(transparent)] + Pool(#[from] nostr_relay_pool::pool::Error), + /// Request timeout + #[error("timeout")] + Timeout, +} + +impl From for ZapperError { + fn from(e: Error) -> Self { + Self::backend(e) + } +} diff --git a/crates/nwc/src/lib.rs b/crates/nwc/src/lib.rs new file mode 100644 index 000000000..7157f3742 --- /dev/null +++ b/crates/nwc/src/lib.rs @@ -0,0 +1,196 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! NWC client and zapper backend for Nostr apps + +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(rustdoc::bare_urls)] +#![allow(unknown_lints)] +#![allow(clippy::arc_with_non_send_sync)] + +use std::time::Duration; + +pub extern crate nostr; +pub extern crate nostr_zapper as zapper; + +use async_utility::time; +use nostr::nips::nip47::{ + MakeInvoiceRequestParams, MakeInvoiceResponseResult, Method, NostrWalletConnectURI, + PayInvoiceRequestParams, PayInvoiceResponseResult, Request, RequestParams, Response, +}; +use nostr::{Filter, Kind, SubscriptionId}; +use nostr_relay_pool::{FilterOptions, RelayPool, RelayPoolNotification, RelaySendOptions}; +use nostr_zapper::{async_trait, NostrZapper, ZapperBackend}; + +pub mod error; +pub mod options; +pub mod prelude; + +pub use self::error::Error; +pub use self::options::NostrWalletConnectOptions; + +/// Nostr Wallet Connect client +#[derive(Debug, Clone)] +pub struct NWC { + uri: NostrWalletConnectURI, + pool: RelayPool, +} + +impl NWC { + /// Compose new [NWC] client + pub async fn new(uri: NostrWalletConnectURI) -> Result { + Self::with_opts(uri, NostrWalletConnectOptions::default()).await + } + + /// Compose new [NWC] client with [NostrWalletConnectOptions] + pub async fn with_opts( + uri: NostrWalletConnectURI, + opts: NostrWalletConnectOptions, + ) -> Result { + // Compose pool + let pool = RelayPool::new(opts.pool); + pool.add_relay(&uri.relay_url, opts.relay).await?; + pool.connect(Some(Duration::from_secs(10))).await; + + Ok(Self { uri, pool }) + } + + /// Create invoice + pub async fn make_invoice( + &self, + satoshi: u64, + description: Option, + expiry: Option, + ) -> Result { + // Compose NWC request event + let req = Request { + method: Method::MakeInvoice, + params: RequestParams::MakeInvoice(MakeInvoiceRequestParams { + amount: satoshi * 1000, + description, + description_hash: None, + expiry, + }), + }; + let event = req.to_event(&self.uri)?; + let event_id = event.id; + + // Subscribe + let relay = self.pool.relay(&self.uri.relay_url).await?; + let id = SubscriptionId::generate(); + let filter = Filter::new() + .author(self.uri.public_key) + .kind(Kind::WalletConnectResponse) + .event(event_id) + .limit(1); + + // Subscribe + relay + .send_req( + id, + vec![filter], + Some(FilterOptions::WaitForEventsAfterEOSE(1)), + ) + .await?; + + let mut notifications = self.pool.notifications(); + + // Send request + self.pool + .send_event_to([&self.uri.relay_url], event, RelaySendOptions::new()) + .await?; + + time::timeout(Some(Duration::from_secs(10)), async { + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + if event.kind() == Kind::WalletConnectResponse + && event.event_ids().next().copied() == Some(event_id) + { + let res = Response::from_event(&self.uri, &event)?; + let MakeInvoiceResponseResult { invoice, .. } = res.to_make_invoice()?; + return Ok(invoice); + } + } + } + + Err(Error::Timeout) + }) + .await + .ok_or(Error::Timeout)? + } + + /// Pay invoice + pub async fn send_payment(&self, invoice: String) -> Result<(), Error> { + // Compose NWC request event + let req = Request { + method: Method::PayInvoice, + params: RequestParams::PayInvoice(PayInvoiceRequestParams { + id: None, + invoice, + amount: None, + }), + }; + let event = req.to_event(&self.uri)?; + let event_id = event.id; + + // Subscribe + let relay = self.pool.relay(&self.uri.relay_url).await?; + let id = SubscriptionId::generate(); + let filter = Filter::new() + .author(self.uri.public_key) + .kind(Kind::WalletConnectResponse) + .event(event_id) + .limit(1); + + // Subscribe + relay + .send_req( + id, + vec![filter], + Some(FilterOptions::WaitForEventsAfterEOSE(1)), + ) + .await?; + + let mut notifications = self.pool.notifications(); + + // Send request + self.pool + .send_event_to([&self.uri.relay_url], event, RelaySendOptions::new()) + .await?; + + time::timeout(Some(Duration::from_secs(10)), async { + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + if event.kind() == Kind::WalletConnectResponse + && event.event_ids().next().copied() == Some(event_id) + { + let res = Response::from_event(&self.uri, &event)?; + let PayInvoiceResponseResult { preimage } = res.to_pay_invoice()?; + tracing::info!("Invoice paid! Preimage: {preimage}"); + break; + } + } + } + + Ok::<(), Error>(()) + }) + .await + .ok_or(Error::Timeout)? + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl NostrZapper for NWC { + type Err = Error; + + fn backend(&self) -> ZapperBackend { + ZapperBackend::NWC + } + + async fn pay_invoice(&self, invoice: String) -> Result<(), Self::Err> { + self.send_payment(invoice).await + } +} diff --git a/crates/nwc/src/options.rs b/crates/nwc/src/options.rs new file mode 100644 index 000000000..90a99dfbc --- /dev/null +++ b/crates/nwc/src/options.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! NWC Options + +#[cfg(not(target_arch = "wasm32"))] +use std::net::SocketAddr; + +use nostr_relay_pool::{RelayOptions, RelayPoolOptions}; + +/// NWC options +#[derive(Debug, Clone, Default)] +pub struct NostrWalletConnectOptions { + pub(super) relay: RelayOptions, + pub(super) pool: RelayPoolOptions, +} + +impl NostrWalletConnectOptions { + /// New default NWC options + pub fn new() -> Self { + Self::default() + } + + /// Set proxy + #[cfg(not(target_arch = "wasm32"))] + pub fn proxy(self, proxy: Option) -> Self { + Self { + relay: self.relay.proxy(proxy), + ..self + } + } + + /// Automatically shutdown relay pool on drop + pub fn shutdown_on_drop(self, shutdown_on_drop: bool) -> Self { + Self { + pool: self.pool.shutdown_on_drop(shutdown_on_drop), + ..self + } + } +} diff --git a/crates/nwc/src/prelude.rs b/crates/nwc/src/prelude.rs new file mode 100644 index 000000000..b83686a2f --- /dev/null +++ b/crates/nwc/src/prelude.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! Prelude + +#![allow(unknown_lints)] +#![allow(ambiguous_glob_reexports)] +#![doc(hidden)] + +pub use nostr::prelude::*; +pub use nostr_zapper::prelude::*; + +pub use crate::*; From 398b9fca1c878d52834e62969f723862c0bebe01 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 16 Feb 2024 12:30:56 +0100 Subject: [PATCH 4/6] sdk: use `nostr-zapper` crate --- Cargo.lock | 35 +++- Cargo.toml | 1 + bindings/nostr-ffi/src/nips/nip47.rs | 14 +- bindings/nostr-ffi/src/nips/nip57.rs | 22 +- bindings/nostr-sdk-ffi/src/client/mod.rs | 1 + bindings/nostr-sdk-ffi/src/client/zapper.rs | 117 +++++++++++ bindings/nostr-sdk-ffi/src/error.rs | 6 + bindings/nostr-sdk-js/src/client/zapper.rs | 32 ++- contrib/scripts/check-crates.sh | 3 + contrib/scripts/check-docs.sh | 2 + contrib/scripts/release.sh | 3 + crates/nostr-sdk/Cargo.toml | 9 +- crates/nostr-sdk/README.md | 5 +- crates/nostr-sdk/examples/zapper.rs | 8 +- crates/nostr-sdk/src/client/builder.rs | 13 +- crates/nostr-sdk/src/client/mod.rs | 77 +++---- crates/nostr-sdk/src/client/options.rs | 39 +--- crates/nostr-sdk/src/client/zapper.rs | 216 ++++---------------- crates/nostr-sdk/src/lib.rs | 9 +- crates/nostr-sdk/src/prelude.rs | 2 + crates/nostr/src/nips/nip47.rs | 6 +- 21 files changed, 330 insertions(+), 290 deletions(-) create mode 100644 bindings/nostr-sdk-ffi/src/client/zapper.rs diff --git a/Cargo.lock b/Cargo.lock index 895d70ef5..b97383c57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1716,12 +1716,14 @@ dependencies = [ "nostr-rocksdb", "nostr-signer", "nostr-sqlite", + "nostr-webln", + "nostr-zapper", + "nwc", "once_cell", "thiserror", "tokio", "tracing", "tracing-subscriber", - "webln", ] [[package]] @@ -1762,6 +1764,23 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "nostr-webln" +version = "0.27.0" +dependencies = [ + "nostr-zapper", + "webln", +] + +[[package]] +name = "nostr-zapper" +version = "0.27.0" +dependencies = [ + "async-trait", + "nostr", + "thiserror", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1782,6 +1801,20 @@ dependencies = [ "libc", ] +[[package]] +name = "nwc" +version = "0.27.0" +dependencies = [ + "async-utility", + "nostr", + "nostr-relay-pool", + "nostr-zapper", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "object" version = "0.32.2" diff --git a/Cargo.toml b/Cargo.toml index a9a6fea8b..ff4e68fa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ nostr = { version = "0.27", path = "./crates/nostr", default-features = false } nostr-database = { version = "0.27", path = "./crates/nostr-database", default-features = false } nostr-relay-pool = { version = "0.27", path = "./crates/nostr-relay-pool", default-features = false } nostr-signer = { version = "0.27", path = "./crates/nostr-signer", default-features = false } +nostr-zapper = { version = "0.27", path = "./crates/nostr-zapper", default-features = false } once_cell = "1.19" serde_json = { version = "1.0", default-features = false } thiserror = "1.0" diff --git a/bindings/nostr-ffi/src/nips/nip47.rs b/bindings/nostr-ffi/src/nips/nip47.rs index ae33b27f9..74942272b 100644 --- a/bindings/nostr-ffi/src/nips/nip47.rs +++ b/bindings/nostr-ffi/src/nips/nip47.rs @@ -331,7 +331,7 @@ pub struct PayKeysendRequestParams { /// Optional id pub id: Option, /// Amount in millisatoshis - pub amount: i64, + pub amount: u64, /// Receiver's node id pub pubkey: String, /// Optional preimage @@ -418,13 +418,13 @@ impl From for TransactionType { #[derive(Record)] pub struct MakeInvoiceRequestParams { /// Amount in millisatoshis - pub amount: i64, + pub amount: u64, /// Invoice description pub description: Option, /// Invoice description hash pub description_hash: Option, /// Invoice expiry in seconds - pub expiry: Option, + pub expiry: Option, } impl From for MakeInvoiceRequestParams { @@ -917,6 +917,14 @@ pub struct NostrWalletConnectURI { inner: nip47::NostrWalletConnectURI, } +impl Deref for NostrWalletConnectURI { + type Target = nip47::NostrWalletConnectURI; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + impl From for NostrWalletConnectURI { fn from(inner: nip47::NostrWalletConnectURI) -> Self { Self { inner } diff --git a/bindings/nostr-ffi/src/nips/nip57.rs b/bindings/nostr-ffi/src/nips/nip57.rs index 2112e5a58..e6308456b 100644 --- a/bindings/nostr-ffi/src/nips/nip57.rs +++ b/bindings/nostr-ffi/src/nips/nip57.rs @@ -6,12 +6,32 @@ use std::ops::Deref; use std::sync::Arc; use nostr::nips::nip57; -use uniffi::Object; +use uniffi::{Enum, Object}; use crate::error::Result; use crate::helper::unwrap_or_clone_arc; use crate::{Event, EventId, Keys, PublicKey, SecretKey}; +#[derive(Enum)] +pub enum ZapType { + /// Public + Public, + /// Private + Private, + /// Anonymous + Anonymous, +} + +impl From for nip57::ZapType { + fn from(value: ZapType) -> Self { + match value { + ZapType::Public => Self::Public, + ZapType::Private => Self::Private, + ZapType::Anonymous => Self::Anonymous, + } + } +} + #[derive(Clone, Object)] pub struct ZapRequestData { inner: nip57::ZapRequestData, diff --git a/bindings/nostr-sdk-ffi/src/client/mod.rs b/bindings/nostr-sdk-ffi/src/client/mod.rs index 58279ef08..07df1174e 100644 --- a/bindings/nostr-sdk-ffi/src/client/mod.rs +++ b/bindings/nostr-sdk-ffi/src/client/mod.rs @@ -21,6 +21,7 @@ use uniffi::Object; mod builder; mod options; pub mod signer; +pub mod zapper; pub use self::builder::ClientBuilder; pub use self::options::Options; diff --git a/bindings/nostr-sdk-ffi/src/client/zapper.rs b/bindings/nostr-sdk-ffi/src/client/zapper.rs new file mode 100644 index 000000000..b32e185a0 --- /dev/null +++ b/bindings/nostr-sdk-ffi/src/client/zapper.rs @@ -0,0 +1,117 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +use core::ops::Deref; +use std::sync::Arc; + +use nostr_ffi::helper::unwrap_or_clone_arc; +use nostr_ffi::nips::nip47::NostrWalletConnectURI; +use nostr_ffi::nips::nip57::ZapType; +use nostr_ffi::{EventId, PublicKey}; +use nostr_sdk::zapper::{DynNostrZapper, IntoNostrZapper}; +use nostr_sdk::{block_on, client, NostrWalletConnectOptions, NWC}; +use uniffi::Object; + +use crate::error::Result; + +/// Zap entity +#[derive(Object)] +pub struct ZapEntity { + inner: client::ZapEntity, +} + +impl Deref for ZapEntity { + type Target = client::ZapEntity; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[uniffi::export] +impl ZapEntity { + #[uniffi::constructor] + pub fn event(event_id: Arc) -> Self { + Self { + inner: client::ZapEntity::Event(**event_id), + } + } + + #[uniffi::constructor] + pub fn public_key(public_key: Arc) -> Self { + Self { + inner: client::ZapEntity::PublicKey(**public_key), + } + } +} + +/// Nostr Zapper +#[derive(Object)] +pub struct NostrZapper { + inner: Arc, +} + +impl Deref for NostrZapper { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl From> for NostrZapper { + fn from(inner: Arc) -> Self { + Self { inner } + } +} + +#[uniffi::export] +impl NostrZapper { + #[uniffi::constructor] + pub fn nwc(uri: Arc) -> Result { + block_on(async move { + let uri = uri.as_ref().deref(); + let zapper = NWC::with_opts( + uri.to_owned(), + NostrWalletConnectOptions::new().shutdown_on_drop(true), + ) + .await?; + Ok(Self { + inner: zapper.into_nostr_zapper(), + }) + }) + } +} + +/// Zap Details +#[derive(Clone, Object)] +pub struct ZapDetails { + inner: client::ZapDetails, +} + +impl From for client::ZapDetails { + fn from(value: ZapDetails) -> Self { + value.inner + } +} + +#[uniffi::export] +impl ZapDetails { + /// Create new Zap Details + /// + /// **Note: `private` zaps are not currently supported here!** + #[uniffi::constructor] + pub fn new(zap_type: ZapType) -> Self { + Self { + inner: client::ZapDetails::new(zap_type.into()), + } + } + + /// Add message + pub fn message(self: Arc, message: String) -> Self { + let mut builder = unwrap_or_clone_arc(self); + builder.inner = builder.inner.message(message); + builder + } +} diff --git a/bindings/nostr-sdk-ffi/src/error.rs b/bindings/nostr-sdk-ffi/src/error.rs index 3ff4c414e..dc0564a21 100644 --- a/bindings/nostr-sdk-ffi/src/error.rs +++ b/bindings/nostr-sdk-ffi/src/error.rs @@ -86,6 +86,12 @@ impl From for NostrSdkError { } } +impl From for NostrSdkError { + fn from(e: nostr_sdk::nwc::Error) -> NostrSdkError { + Self::Generic(e.to_string()) + } +} + impl From for NostrSdkError { fn from(e: async_utility::thread::Error) -> NostrSdkError { Self::Generic(e.to_string()) diff --git a/bindings/nostr-sdk-js/src/client/zapper.rs b/bindings/nostr-sdk-js/src/client/zapper.rs index 84558fa4b..674ad28e4 100644 --- a/bindings/nostr-sdk-js/src/client/zapper.rs +++ b/bindings/nostr-sdk-js/src/client/zapper.rs @@ -3,13 +3,14 @@ // Distributed under the MIT software license use core::ops::Deref; +use std::sync::Arc; use nostr_js::error::{into_err, Result}; use nostr_js::event::JsEventId; use nostr_js::key::JsPublicKey; use nostr_js::nips::nip47::JsNostrWalletConnectURI; use nostr_js::nips::nip57::JsZapType; -use nostr_sdk::client::{NostrZapper, ZapDetails, ZapEntity}; +use nostr_sdk::prelude::*; use wasm_bindgen::prelude::*; /// Zap entity @@ -45,30 +46,43 @@ impl JsZapEntity { /// Nostr Zapper #[wasm_bindgen(js_name = NostrZapper)] pub struct JsNostrZapper { - inner: NostrZapper, + inner: Arc, } impl Deref for JsNostrZapper { - type Target = NostrZapper; + type Target = Arc; fn deref(&self) -> &Self::Target { &self.inner } } +impl From> for JsNostrZapper { + fn from(inner: Arc) -> Self { + Self { inner } + } +} + #[wasm_bindgen(js_class = NostrZapper)] impl JsNostrZapper { /// Create new `WebLN` instance and compose `NostrZapper` - pub fn webln() -> Result { + pub async fn webln() -> Result { + let zapper = WebLNZapper::new().await.map_err(into_err)?; Ok(Self { - inner: NostrZapper::webln().map_err(into_err)?, + inner: zapper.into_nostr_zapper(), }) } - pub fn nwc(uri: &JsNostrWalletConnectURI) -> Self { - Self { - inner: NostrZapper::NWC(uri.deref().clone()), - } + pub async fn nwc(uri: &JsNostrWalletConnectURI) -> Result { + let zapper = NWC::with_opts( + uri.deref().clone(), + NostrWalletConnectOptions::new().shutdown_on_drop(true), + ) + .await + .map_err(into_err)?; + Ok(Self { + inner: zapper.into_nostr_zapper(), + }) } } diff --git a/contrib/scripts/check-crates.sh b/contrib/scripts/check-crates.sh index 3b925151b..046205b3b 100644 --- a/contrib/scripts/check-crates.sh +++ b/contrib/scripts/check-crates.sh @@ -29,8 +29,11 @@ buildargs=( "-p nostr --no-default-features --features alloc,all-nips" "-p nostr --features blocking" "-p nostr-database" + "-p nostr-zapper" "-p nostr-sdk" "-p nostr-sdk --no-default-features" + "-p nostr-sdk --features nip47,nip57" + "-p nostr-sdk --features nip47,nip57 --target wasm32-unknown-unknown" "-p nostr-sdk --features indexeddb,webln --target wasm32-unknown-unknown" "-p nostr-sdk --features sqlite" ) diff --git a/contrib/scripts/check-docs.sh b/contrib/scripts/check-docs.sh index a1012887f..3a7f35c3a 100644 --- a/contrib/scripts/check-docs.sh +++ b/contrib/scripts/check-docs.sh @@ -8,6 +8,8 @@ buildargs=( "-p nostr-database" "-p nostr-relay-pool" "-p nostr-signer" + "-p nostr-zapper" + "-p nwc" "-p nostr-sdk" ) diff --git a/contrib/scripts/release.sh b/contrib/scripts/release.sh index acccd565e..18316a4f2 100644 --- a/contrib/scripts/release.sh +++ b/contrib/scripts/release.sh @@ -9,6 +9,9 @@ args=( "-p nostr-indexeddb" "-p nostr-relay-pool" "-p nostr-signer" + "-p nostr-zapper" + "-p nostr-webln" + "-p nwc" "-p nostr-sdk" ) diff --git a/crates/nostr-sdk/Cargo.toml b/crates/nostr-sdk/Cargo.toml index 979739c71..b59ecdd5f 100644 --- a/crates/nostr-sdk/Cargo.toml +++ b/crates/nostr-sdk/Cargo.toml @@ -21,6 +21,7 @@ blocking = ["dep:once_cell", "nostr/blocking"] rocksdb = ["dep:nostr-rocksdb"] sqlite = ["dep:nostr-sqlite"] indexeddb = ["dep:nostr-indexeddb"] +webln = ["nip57", "dep:nostr-webln"] all-nips = ["nip04", "nip05", "nip06", "nip07", "nip11", "nip44", "nip46", "nip47", "nip49", "nip57", "nip59"] nip03 = ["nostr/nip03"] nip04 = ["nostr/nip04", "nostr-signer/nip04"] @@ -30,9 +31,9 @@ nip07 = ["nostr/nip07", "nostr-signer/nip07"] nip11 = ["nostr/nip11", "nostr-relay-pool/nip11"] nip44 = ["nostr/nip44", "nostr-signer/nip44"] nip46 = ["nostr/nip46", "nostr-signer/nip46"] -nip47 = ["nostr/nip47"] +nip47 = ["nostr/nip47", "dep:nwc"] nip49 = ["nostr/nip49"] -nip57 = ["nostr/nip57", "dep:lnurl-pay"] +nip57 = ["nostr/nip57", "dep:nostr-zapper", "dep:lnurl-pay"] nip59 = ["nostr/nip59"] [dependencies] @@ -42,6 +43,8 @@ nostr = { workspace = true, features = ["std"] } nostr-database.workspace = true nostr-relay-pool.workspace = true nostr-signer.workspace = true +nostr-zapper = { workspace = true, optional = true } +nwc = { version = "0.27", path = "../nwc", optional = true } once_cell = { workspace = true, optional = true } thiserror.workspace = true tracing = { workspace = true, features = ["std", "attributes"] } @@ -53,8 +56,8 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] } [target.'cfg(target_arch = "wasm32")'.dependencies] nostr-indexeddb = { version = "0.27", path = "../nostr-indexeddb", optional = true } +nostr-webln = { version = "0.27", path = "../nostr-webln", optional = true } tokio = { workspace = true, features = ["rt", "macros", "sync"] } -webln = { version = "0.1", optional = true } [dev-dependencies] tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/crates/nostr-sdk/README.md b/crates/nostr-sdk/README.md index 2cb4b289f..de10bbdbd 100644 --- a/crates/nostr-sdk/README.md +++ b/crates/nostr-sdk/README.md @@ -91,8 +91,8 @@ async fn main() -> Result<()> { // Configure zapper let uri = NostrWalletConnectURI::from_str("nostr+walletconnect://...")?; - let zapper = NostrZapper::nwc(uri); - client.set_zapper(Some(zapper)).await; + let zapper = NWC::new(uri).await?; // Use `WebLNZapper::new().await` for WebLN + client.set_zapper(zapper).await; // Send SAT without zap event let public_key = PublicKey::from_bech32( @@ -140,6 +140,7 @@ The following crate feature flags are available: | `sqlite` | No | Enable SQLite Storage backend | | `rocksdb` | No | Enable RocksDB Storage backend | | `indexeddb` | No | Enable Web's IndexedDb Storage backend | +| `webln` | No | Enable WebLN zapper | | `all-nips` | Yes | Enable all NIPs | | `nip03` | No | Enable NIP-03: OpenTimestamps Attestations for Events | | `nip04` | Yes | Enable NIP-04: Encrypted Direct Message | diff --git a/crates/nostr-sdk/examples/zapper.rs b/crates/nostr-sdk/examples/zapper.rs index d0e6bb468..70226b724 100644 --- a/crates/nostr-sdk/examples/zapper.rs +++ b/crates/nostr-sdk/examples/zapper.rs @@ -17,15 +17,15 @@ async fn main() -> Result<()> { .read_line(&mut nwc_uri_string) .expect("Failed to read line"); - // Parse NWC URI - let nwc_uri = - NostrWalletConnectURI::from_str(&nwc_uri_string).expect("Failed to parse NWC URI"); + // Parse NWC URI and compose NWC client + let uri = NostrWalletConnectURI::from_str(&nwc_uri_string).expect("Failed to parse NWC URI"); + let nwc = NWC::new(uri).await?; // Compose client let secret_key = SecretKey::from_bech32("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; let keys = Keys::new(secret_key); - let client = ClientBuilder::new().signer(keys).zapper(nwc_uri).build(); + let client = ClientBuilder::new().signer(keys).zapper(nwc).build(); client.add_relay("wss://relay.nostr.band").await?; client.add_relay("wss://relay.damus.io").await?; diff --git a/crates/nostr-sdk/src/client/builder.rs b/crates/nostr-sdk/src/client/builder.rs index e21434d71..06ae14df1 100644 --- a/crates/nostr-sdk/src/client/builder.rs +++ b/crates/nostr-sdk/src/client/builder.rs @@ -9,9 +9,9 @@ use std::sync::Arc; use nostr_database::memory::MemoryDatabase; use nostr_database::{DynNostrDatabase, IntoNostrDatabase}; use nostr_signer::NostrSigner; - #[cfg(feature = "nip57")] -use super::zapper::NostrZapper; +use nostr_zapper::{DynNostrZapper, IntoNostrZapper}; + use crate::{Client, Options}; /// Client builder @@ -21,7 +21,7 @@ pub struct ClientBuilder { pub signer: Option, /// Nostr Zapper #[cfg(feature = "nip57")] - pub zapper: Option, + pub zapper: Option>, /// Database pub database: Arc, /// Client options @@ -68,12 +68,11 @@ impl ClientBuilder { /// Set zapper #[cfg(feature = "nip57")] - #[allow(unused_mut, unreachable_code)] - pub fn zapper(mut self, zapper: S) -> Self + pub fn zapper(mut self, zapper: Z) -> Self where - S: Into, + Z: IntoNostrZapper, { - self.zapper = Some(zapper.into()); + self.zapper = Some(zapper.into_nostr_zapper()); self } diff --git a/crates/nostr-sdk/src/client/mod.rs b/crates/nostr-sdk/src/client/mod.rs index d4c693bc3..2fc85ebc7 100644 --- a/crates/nostr-sdk/src/client/mod.rs +++ b/crates/nostr-sdk/src/client/mod.rs @@ -16,47 +16,40 @@ use nostr::prelude::*; use nostr::types::metadata::Error as MetadataError; use nostr_database::DynNostrDatabase; use nostr_relay_pool::pool::{self, Error as RelayPoolError, RelayPool}; -use nostr_relay_pool::relay::Error as RelayError; use nostr_relay_pool::{ FilterOptions, NegentropyOptions, Relay, RelayOptions, RelayPoolNotification, RelaySendOptions, }; use nostr_signer::prelude::*; +#[cfg(feature = "nip57")] +use nostr_zapper::{DynNostrZapper, IntoNostrZapper, ZapperError}; use tokio::sync::{broadcast, RwLock}; pub mod builder; pub mod options; #[cfg(feature = "nip57")] -pub mod zapper; +mod zapper; pub use self::builder::ClientBuilder; pub use self::options::Options; #[cfg(feature = "nip57")] -pub use self::zapper::{NostrZapper, ZapDetails, ZapEntity}; +pub use self::zapper::{ZapDetails, ZapEntity}; /// [`Client`] error #[derive(Debug, thiserror::Error)] pub enum Error { - /// Url parse error - #[error("impossible to parse URL: {0}")] - Url(#[from] nostr::types::url::ParseError), /// [`RelayPool`] error #[error("relay pool error: {0}")] RelayPool(#[from] RelayPoolError), - /// [`Relay`] error - #[error("relay error: {0}")] - Relay(#[from] RelayError), /// Signer error #[error(transparent)] Signer(#[from] nostr_signer::Error), + /// Zapper error + #[cfg(feature = "nip57")] + #[error(transparent)] + Zapper(#[from] ZapperError), /// [`EventBuilder`] error #[error("event builder error: {0}")] EventBuilder(#[from] EventBuilderError), - /// Secp256k1 error - #[error("secp256k1 error: {0}")] - Secp256k1(#[from] nostr::secp256k1::Error), - /// Hex error - #[error("hex decoding error: {0}")] - Hex(#[from] nostr::hashes::hex::Error), /// Metadata error #[error(transparent)] Metadata(#[from] MetadataError), @@ -70,18 +63,10 @@ pub enum Error { #[cfg(feature = "nip57")] #[error("zapper not configured")] ZapperNotConfigured, - /// NIP04 error - #[cfg(feature = "nip04")] - #[error(transparent)] - NIP04(#[from] nostr::nips::nip04::Error), /// NIP46 signer error #[cfg(feature = "nip46")] #[error(transparent)] - Nip46Signer(#[from] nostr_signer::nip46::Error), - /// NIP47 error - #[cfg(feature = "nip47")] - #[error(transparent)] - NIP47(#[from] nostr::nips::nip47::Error), + Nip46Signer(#[from] nostr_signer::nip46::Error), // TODO remove /// NIP57 error #[cfg(feature = "nip57")] #[error(transparent)] @@ -90,19 +75,9 @@ pub enum Error { #[cfg(feature = "nip57")] #[error(transparent)] LnUrlPay(#[from] lnurl_pay::Error), - /// WebLN error - #[cfg(all(feature = "webln", target_arch = "wasm32"))] - #[error(transparent)] - WebLN(#[from] webln::Error), - /// Timeout - #[error("timeout")] - Timeout, /// Event not found #[error("event not found: {0}")] EventNotFound(EventId), - /// Event not found - #[error("event not found")] - GenericEventNotFound, /// Impossible to zap #[error("impossible to send zap: {0}")] ImpossibleToZap(String), @@ -114,7 +89,7 @@ pub struct Client { pool: RelayPool, signer: Arc>>, #[cfg(feature = "nip57")] - zapper: Arc>>, + zapper: Arc>>>, opts: Options, dropped: Arc, } @@ -225,16 +200,26 @@ impl Client { /// /// Rise error if it not set. #[cfg(feature = "nip57")] - pub async fn zapper(&self) -> Result { + pub async fn zapper(&self) -> Result, Error> { let zapper = self.zapper.read().await; zapper.clone().ok_or(Error::ZapperNotConfigured) } /// Set nostr zapper #[cfg(feature = "nip57")] - pub async fn set_zapper(&self, zapper: Option) { + pub async fn set_zapper(&self, zapper: Z) + where + Z: IntoNostrZapper, + { + let mut s = self.zapper.write().await; + *s = Some(zapper.into_nostr_zapper()); + } + + /// Unset nostr zapper + #[cfg(feature = "nip57")] + pub async fn unset_zapper(&self) { let mut s = self.zapper.write().await; - *s = zapper; + *s = None; } /// Get [`RelayPool`] @@ -1221,6 +1206,22 @@ impl Client { self.send_event_builder(builder).await } + /// Send a Zap! + /// + /// This method automatically create a split zap to support Rust Nostr development. + #[cfg(feature = "nip57")] + pub async fn zap( + &self, + to: T, + satoshi: u64, + details: Option, + ) -> Result<(), Error> + where + T: Into, + { + self.internal_zap(to, satoshi, details).await + } + /// Gift Wrap /// /// diff --git a/crates/nostr-sdk/src/client/options.rs b/crates/nostr-sdk/src/client/options.rs index e7abe0cb8..14c89f642 100644 --- a/crates/nostr-sdk/src/client/options.rs +++ b/crates/nostr-sdk/src/client/options.rs @@ -6,18 +6,13 @@ #[cfg(not(target_arch = "wasm32"))] use std::net::SocketAddr; -use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::Arc; use std::time::Duration; use nostr_relay_pool::options::DEFAULT_SEND_TIMEOUT; use nostr_relay_pool::{RelayPoolOptions, RelaySendOptions}; -/// Default Support Rust Nostr LUD16 -pub const SUPPORT_RUST_NOSTR_LUD16: &str = "yuki@getalby.com"; // TODO: use a rust-nostr dedicated LUD16 -/// Default Support Rust Nostr basis points -pub const DEFAULT_SUPPORT_RUST_NOSTR_BSP: u64 = 500; // 5% - /// Options #[derive(Debug, Clone)] pub struct Options { @@ -50,10 +45,6 @@ pub struct Options { pub shutdown_on_drop: bool, /// Pool Options pub pool: RelayPoolOptions, - /// Support Rust Nostr in basis points (default: 5%) - /// - /// 100 bps = 1% - support_rust_nostr_bps: Arc, } impl Default for Options { @@ -72,7 +63,6 @@ impl Default for Options { proxy: None, shutdown_on_drop: false, pool: RelayPoolOptions::default(), - support_rust_nostr_bps: Arc::new(AtomicU64::new(DEFAULT_SUPPORT_RUST_NOSTR_BSP)), } } } @@ -217,31 +207,4 @@ impl Options { pub fn pool(self, opts: RelayPoolOptions) -> Self { Self { pool: opts, ..self } } - - /// Support Rust Nostr with a % of zaps (default: 5%) - /// - /// 100 bps = 1% - pub fn support_rust_nostr(mut self, bps: u64) -> Self { - self.support_rust_nostr_bps = Arc::new(AtomicU64::new(bps)); - self - } - - /// Update Support Rust Nostr basis points - /// - /// 100 bps = 1% - pub fn update_support_rust_nostr(&self, bps: u64) { - let _ = - self.support_rust_nostr_bps - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |_| Some(bps)); - } - - /// Get Support Rust Nostr percentage - pub fn get_support_rust_nostr_percentage(&self) -> Option { - let bps: u64 = self.support_rust_nostr_bps.load(Ordering::SeqCst); - if bps != 0 { - Some(bps as f64 / 10_000.0) - } else { - None - } - } } diff --git a/crates/nostr-sdk/src/client/zapper.rs b/crates/nostr-sdk/src/client/zapper.rs index 4e425ff2b..acaee8b3b 100644 --- a/crates/nostr-sdk/src/client/zapper.rs +++ b/crates/nostr-sdk/src/client/zapper.rs @@ -2,23 +2,16 @@ // Copyright (c) 2023-2024 Rust Nostr Developers // Distributed under the MIT software license -//! Nostr Zapper - use std::str::FromStr; -#[cfg(feature = "nip47")] -use std::time::Duration; -use async_utility::time; use lnurl_pay::api::Lud06OrLud16; use lnurl_pay::{LightningAddress, LnUrl}; use nostr::prelude::*; -use nostr_relay_pool::{FilterOptions, RelayPoolNotification}; -#[cfg(all(feature = "webln", target_arch = "wasm32"))] -use webln::WebLN; -use super::options::SUPPORT_RUST_NOSTR_LUD16; use super::{Client, Error}; +const SUPPORT_RUST_NOSTR_LUD16: &str = "yuki@getalby.com"; // TODO: use a rust-nostr dedicated LUD16 +const SUPPORT_RUST_NOSTR_PERCENTAGE: f64 = 0.05; // 5% const SUPPORT_RUST_NOSTR_MSG: &str = "Zap split to support Rust Nostr development!"; /// Zap entity @@ -57,45 +50,6 @@ impl ZapEntity { } } -/// Nostr Zapper -#[derive(Debug, Clone)] -pub enum NostrZapper { - /// WebLN - #[cfg(all(feature = "webln", target_arch = "wasm32"))] - WebLN(WebLN), - /// NWC - #[cfg(feature = "nip47")] - NWC(NostrWalletConnectURI), -} - -impl NostrZapper { - /// Create a new [WebLN] instance and compose [NostrZapper] - #[cfg(all(feature = "webln", target_arch = "wasm32"))] - pub fn webln() -> Result { - let instance = WebLN::new()?; - Ok(Self::WebLN(instance)) - } - - /// Compose [NostrZapper] with [NostrWalletConnectURI] - pub fn nwc(uri: NostrWalletConnectURI) -> Self { - Self::NWC(uri) - } -} - -#[cfg(all(feature = "webln", target_arch = "wasm32"))] -impl From for NostrZapper { - fn from(value: WebLN) -> Self { - Self::WebLN(value) - } -} - -#[cfg(feature = "nip47")] -impl From for NostrZapper { - fn from(value: NostrWalletConnectURI) -> Self { - Self::NWC(value) - } -} - /// Zap Details #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ZapDetails { @@ -125,10 +79,12 @@ impl ZapDetails { } impl Client { - /// Send a Zap! - /// - /// This method automatically create a split zap to support Rust Nostr development. - pub async fn zap( + /// Steps + /// 1. Check if zapper is set and availabe + /// 2. Get metadata of pubkey/author of event + /// 3. Get invoice + /// 4. Send payment + pub(super) async fn internal_zap( &self, to: T, satoshi: u64, @@ -137,12 +93,6 @@ impl Client { where T: Into, { - // Steps - // 1. Check if zapper is set and availabe - // 2. Get metadata of pubkey/author of event - // 3. Get invoice - // 4. Send payment - // Check if zapper is set if !self.has_zapper().await { return Err(Error::ZapperNotConfigured); @@ -180,101 +130,8 @@ impl Client { .zap_split(public_key, lud, satoshi, details, to.event_id()) .await?; - self.pay_invoices(invoices).await - } - - /// Pay invoices with [NostrZapper] - pub async fn pay_invoices(&self, _invoices: I) -> Result<(), Error> - where - I: IntoIterator, - S: Into, - { - match self.zapper().await? { - #[cfg(all(feature = "webln", target_arch = "wasm32"))] - NostrZapper::WebLN(webln) => { - webln.enable().await?; - for invoice in _invoices.into_iter() { - webln.send_payment(invoice.into()).await?; - } - Ok(()) - } - #[cfg(feature = "nip47")] - NostrZapper::NWC(uri) => self.pay_invoices_with_nwc(&uri, _invoices).await, - } - } - - /// Pay invoices with [NostrWalletConnectURI] - pub async fn pay_invoices_with_nwc( - &self, - uri: &NostrWalletConnectURI, - invoices: I, - ) -> Result<(), Error> - where - I: IntoIterator, - S: Into, - { - // Add relay and connect if not exists - if self.add_relay(uri.relay_url.clone()).await? { - self.connect_relay(uri.relay_url.clone()).await?; - } - - for invoice in invoices.into_iter() { - // Compose NWC request event - let req = nip47::Request { - method: Method::PayInvoice, - params: RequestParams::PayInvoice(PayInvoiceRequestParams { - id: None, - invoice: invoice.into(), - amount: None, - }), - }; - let event = req.to_event(uri)?; - let event_id = event.id; - - // Subscribe - let relay = self.relay(uri.relay_url.clone()).await?; - let id = SubscriptionId::generate(); - let filter = Filter::new() - .author(uri.public_key) - .kind(Kind::WalletConnectResponse) - .event(event_id) - .limit(1); - - // Subscribe - relay - .send_req( - id, - vec![filter], - Some(FilterOptions::WaitForEventsAfterEOSE(1)), - ) - .await?; - - let mut notifications = self.notifications(); - - // Send request - self.send_event_to([uri.relay_url.clone()], event).await?; - - time::timeout(Some(Duration::from_secs(10)), async { - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if event.kind() == Kind::WalletConnectResponse - && event.event_ids().next().copied() == Some(event_id) - { - let res = nip47::Response::from_event(uri, &event)?; - let PayInvoiceResponseResult { preimage } = res.to_pay_invoice()?; - tracing::info!("Invoice paid! Preimage: {preimage}"); - - break; - } - } - } - - Ok::<(), Error>(()) - }) - .await - .ok_or(Error::Timeout)??; - } - + let zapper = self.zapper().await?; + zapper.pay_multi_invoices(invoices).await?; Ok(()) } @@ -290,40 +147,41 @@ impl Client { let mut invoices: Vec = Vec::with_capacity(2); let mut msats: u64 = satoshi * 1000; - // Check if is set a percentage - if let Some(percentage) = self.opts.get_support_rust_nostr_percentage() { - let rust_nostr_msats = (satoshi as f64 * percentage * 1000.0) as u64; - let rust_nostr_lud = LightningAddress::parse(SUPPORT_RUST_NOSTR_LUD16)?; - let rust_nostr_lud = Lud06OrLud16::Lud16(rust_nostr_lud); - - // Check if LUD is equal to Rust Nostr LUD - if rust_nostr_lud != lud { - match lnurl_pay::api::get_invoice( - rust_nostr_lud, - rust_nostr_msats, - Some(SUPPORT_RUST_NOSTR_MSG.to_string()), - None, - None, - ) - .await - { - Ok(invoice) => { - invoices.push(invoice); - msats = satoshi * 1000 - rust_nostr_msats; - } - Err(e) => { - tracing::error!("Impossible to get invoice for Rust Nostr: {e}"); - } + let rust_nostr_sats: u64 = (satoshi as f64 * SUPPORT_RUST_NOSTR_PERCENTAGE) as u64; + let rust_nostr_msats: u64 = rust_nostr_sats * 1000; + let rust_nostr_lud = LightningAddress::parse(SUPPORT_RUST_NOSTR_LUD16)?; + let rust_nostr_lud = Lud06OrLud16::Lud16(rust_nostr_lud); + + // Check if LUD is equal to Rust Nostr LUD + if rust_nostr_lud != lud { + match lnurl_pay::api::get_invoice( + rust_nostr_lud, + rust_nostr_msats, + Some(SUPPORT_RUST_NOSTR_MSG.to_string()), + None, + None, + ) + .await + { + Ok(invoice) => { + invoices.push(invoice); + msats = satoshi * 1000 - rust_nostr_msats; + } + Err(e) => { + tracing::error!("Impossible to get invoice for Rust Nostr: {e}"); } } - }; + } // Compose zap request let zap_request: Option = match details { Some(details) => { let mut data = ZapRequestData::new( public_key, - [UncheckedUrl::from("wss://nostr.mutinywallet.com")], + [ + UncheckedUrl::from("wss://nostr.mutinywallet.com"), + UncheckedUrl::from("wss://relay.mutinywallet.com"), + ], ) .amount(msats) .message(details.message); diff --git a/crates/nostr-sdk/src/lib.rs b/crates/nostr-sdk/src/lib.rs index 2f159fcf3..8f95799cc 100644 --- a/crates/nostr-sdk/src/lib.rs +++ b/crates/nostr-sdk/src/lib.rs @@ -34,14 +34,19 @@ pub use nostr_rocksdb::RocksDatabase; pub use nostr_signer::{self as signer, NostrSigner, NostrSignerType}; #[cfg(feature = "sqlite")] pub use nostr_sqlite::{Error as SQLiteError, SQLiteDatabase}; +#[cfg(all(target_arch = "wasm32", feature = "webln"))] +pub use nostr_webln::WebLNZapper; +#[cfg(feature = "nip57")] +pub use nostr_zapper::{self as zapper, NostrZapper, ZapperBackend, ZapperError}; +#[cfg(feature = "nip47")] +pub use nwc::{self, NostrWalletConnectOptions, NWC}; #[cfg(feature = "blocking")] use once_cell::sync::Lazy; #[cfg(feature = "blocking")] use tokio::runtime::Runtime; +#[doc(hidden)] #[cfg(feature = "blocking")] pub use tokio::task::spawn_blocking; -#[cfg(all(feature = "webln", target_arch = "wasm32"))] -pub use webln; pub mod client; pub mod prelude; diff --git a/crates/nostr-sdk/src/prelude.rs b/crates/nostr-sdk/src/prelude.rs index 44b5a7d89..763f8e5eb 100644 --- a/crates/nostr-sdk/src/prelude.rs +++ b/crates/nostr-sdk/src/prelude.rs @@ -13,6 +13,8 @@ pub use nostr::prelude::*; pub use nostr_database::*; pub use nostr_relay_pool::*; pub use nostr_signer::prelude::*; +#[cfg(feature = "nip57")] +pub use nostr_zapper::prelude::*; // Internal modules pub use crate::client::*; diff --git a/crates/nostr/src/nips/nip47.rs b/crates/nostr/src/nips/nip47.rs index a695c26fe..bedfb0877 100644 --- a/crates/nostr/src/nips/nip47.rs +++ b/crates/nostr/src/nips/nip47.rs @@ -305,7 +305,7 @@ pub struct PayKeysendRequestParams { #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, /// Amount in millisatoshis - pub amount: i64, + pub amount: u64, /// Receiver's node id pub pubkey: String, /// Optional preimage @@ -328,13 +328,13 @@ pub struct MultiPayKeysendRequestParams { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MakeInvoiceRequestParams { /// Amount in millisatoshis - pub amount: i64, + pub amount: u64, /// Invoice description pub description: Option, /// Invoice description hash pub description_hash: Option, /// Invoice expiry in seconds - pub expiry: Option, + pub expiry: Option, } /// Lookup Invoice Request Params From 2ab328fa4d06b861f75d79882edf159b14d6655c Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 16 Feb 2024 13:43:28 +0100 Subject: [PATCH 5/6] zapper: temp remove `pay_multi_invoices` --- crates/nostr-sdk/src/client/zapper.rs | 4 +++- crates/nostr-zapper/src/lib.rs | 15 --------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/nostr-sdk/src/client/zapper.rs b/crates/nostr-sdk/src/client/zapper.rs index acaee8b3b..09f0ab9d5 100644 --- a/crates/nostr-sdk/src/client/zapper.rs +++ b/crates/nostr-sdk/src/client/zapper.rs @@ -131,7 +131,9 @@ impl Client { .await?; let zapper = self.zapper().await?; - zapper.pay_multi_invoices(invoices).await?; + for invoice in invoices.into_iter() { + zapper.pay_invoice(invoice).await?; + } Ok(()) } diff --git a/crates/nostr-zapper/src/lib.rs b/crates/nostr-zapper/src/lib.rs index 9077b095b..0bc4ece4e 100644 --- a/crates/nostr-zapper/src/lib.rs +++ b/crates/nostr-zapper/src/lib.rs @@ -83,14 +83,6 @@ pub trait NostrZapper: AsyncTraitDeps { /// Pay invoice async fn pay_invoice(&self, invoice: String) -> Result<(), Self::Err>; - - /// Pay multiple invoices - async fn pay_multi_invoices(&self, invoices: Vec) -> Result<(), Self::Err> { - for invoice in invoices.into_iter() { - self.pay_invoice(invoice).await?; - } - Ok(()) - } } #[repr(transparent)] @@ -114,13 +106,6 @@ impl NostrZapper for EraseNostrZapperError { async fn pay_invoice(&self, invoice: String) -> Result<(), Self::Err> { self.0.pay_invoice(invoice).await.map_err(Into::into) } - - async fn pay_multi_invoices(&self, invoices: Vec) -> Result<(), Self::Err> { - self.0 - .pay_multi_invoices(invoices) - .await - .map_err(Into::into) - } } /// Alias for `Send` on non-wasm, empty trait (implemented by everything) on From 675fe5c160fafe5c3beccd749d14a0d82fadecbe Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 16 Feb 2024 13:47:45 +0100 Subject: [PATCH 6/6] sdk: retrun error if metadata not found in `Client::metadata` --- crates/nostr-sdk/src/client/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/nostr-sdk/src/client/mod.rs b/crates/nostr-sdk/src/client/mod.rs index 2fc85ebc7..2b6a85c13 100644 --- a/crates/nostr-sdk/src/client/mod.rs +++ b/crates/nostr-sdk/src/client/mod.rs @@ -81,6 +81,9 @@ pub enum Error { /// Impossible to zap #[error("impossible to send zap: {0}")] ImpossibleToZap(String), + /// Metadata not found + #[error("metadata not found")] + MetadataNotFound, } /// Nostr client @@ -765,7 +768,7 @@ impl Client { let events: Vec = self.get_events_of(vec![filter], None).await?; match events.first() { Some(event) => Ok(Metadata::from_json(event.content())?), - None => Ok(Metadata::default()), + None => Err(Error::MetadataNotFound), } }