From 0de15526d80b1908e5eb69d5e1446cf071e6dec4 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 19 Jan 2024 20:37:27 +0100 Subject: [PATCH] sdk: init `ClientZapper` --- Cargo.lock | 27 +++++ crates/nostr-sdk/Cargo.toml | 4 +- crates/nostr-sdk/src/client/builder.rs | 16 +++ crates/nostr-sdk/src/client/mod.rs | 56 ++++++++++ crates/nostr-sdk/src/client/zapper/mod.rs | 123 ++++++++++++++++++++++ crates/nostr-sdk/src/lib.rs | 2 + crates/nostr/src/nips/nip57.rs | 20 ++-- 7 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 crates/nostr-sdk/src/client/zapper/mod.rs diff --git a/Cargo.lock b/Cargo.lock index cd1eee687..5fd0aa0cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1158,6 +1158,18 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "lnurl-pay" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ddf38a88792ba1e1307b97f059941442c94341e9be479080386b0410050c235" +dependencies = [ + "bech32", + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "lock_api" version = "0.4.11" @@ -1435,6 +1447,7 @@ version = "0.27.0" dependencies = [ "async-utility", "async-wsocket", + "lnurl-pay", "nostr", "nostr-database", "nostr-indexeddb", @@ -1444,6 +1457,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "webln", ] [[package]] @@ -2785,6 +2799,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webln" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23f76df183e94dfe9e5c875c00f54539f02e1ba31c71668b9c8d83277fac599" +dependencies = [ + "js-sys", + "secp256k1", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "webpki-roots" version = "0.25.3" diff --git a/crates/nostr-sdk/Cargo.toml b/crates/nostr-sdk/Cargo.toml index 8663c4511..b9f08956c 100644 --- a/crates/nostr-sdk/Cargo.toml +++ b/crates/nostr-sdk/Cargo.toml @@ -30,11 +30,12 @@ nip11 = ["nostr/nip11"] nip44 = ["nostr/nip44"] nip46 = ["nostr/nip46"] nip47 = ["nostr/nip47"] -nip57 = ["nostr/nip57"] +nip57 = ["nostr/nip57", "dep:lnurl-pay"] [dependencies] async-utility.workspace = true async-wsocket = "0.1" +lnurl-pay = { version = "0.2", features = ["api"], optional = true } nostr = { workspace = true, features = ["std"] } nostr-database.workspace = true once_cell.workspace = true @@ -48,6 +49,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sy [target.'cfg(target_arch = "wasm32")'.dependencies] nostr-indexeddb = { version = "0.27", path = "../nostr-indexeddb", 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/src/client/builder.rs b/crates/nostr-sdk/src/client/builder.rs index 71c25dc0e..3f40f8809 100644 --- a/crates/nostr-sdk/src/client/builder.rs +++ b/crates/nostr-sdk/src/client/builder.rs @@ -10,12 +10,16 @@ use nostr_database::memory::MemoryDatabase; use nostr_database::{DynNostrDatabase, IntoNostrDatabase}; use super::signer::ClientSigner; +#[cfg(feature = "nip57")] +use super::zapper::ClientZapper; use crate::{Client, Options}; /// Client builder #[derive(Debug, Clone)] pub struct ClientBuilder { pub(super) signer: Option, + #[cfg(feature = "nip57")] + pub(super) zapper: Option, pub(super) database: Arc, pub(super) opts: Options, } @@ -24,6 +28,8 @@ impl Default for ClientBuilder { fn default() -> Self { Self { signer: None, + #[cfg(feature = "nip57")] + zapper: None, database: Arc::new(MemoryDatabase::default()), opts: Options::default(), } @@ -56,6 +62,16 @@ impl ClientBuilder { self } + /// Set zapper + #[cfg(feature = "nip57")] + pub fn zapper(mut self, zapper: S) -> Self + where + S: Into, + { + self.zapper = Some(zapper.into()); + self + } + /// Set database pub fn database(mut self, database: D) -> Self where diff --git a/crates/nostr-sdk/src/client/mod.rs b/crates/nostr-sdk/src/client/mod.rs index 68947bb54..1465fbd53 100644 --- a/crates/nostr-sdk/src/client/mod.rs +++ b/crates/nostr-sdk/src/client/mod.rs @@ -31,12 +31,16 @@ pub mod blocking; pub mod builder; pub mod options; pub mod signer; +#[cfg(feature = "nip57")] +pub mod zapper; pub use self::builder::ClientBuilder; pub use self::options::Options; #[cfg(feature = "nip46")] pub use self::signer::nip46::Nip46Signer; pub use self::signer::{ClientSigner, ClientSignerType}; +#[cfg(feature = "nip57")] +use self::zapper::ClientZapper; use crate::relay::pool::{self, Error as RelayPoolError, RelayPool}; use crate::relay::{ FilterOptions, NegentropyOptions, Relay, RelayOptions, RelayPoolNotification, RelaySendOptions, @@ -84,6 +88,10 @@ pub enum Error { /// Found client signer type found: ClientSignerType, }, + /// Zapper not configured + #[cfg(feature = "nip57")] + #[error("zapper not configured")] + ZapperNotConfigured, /// NIP04 error #[cfg(feature = "nip04")] #[error(transparent)] @@ -100,6 +108,13 @@ pub enum Error { #[cfg(feature = "nip46")] #[error(transparent)] JSON(#[from] nostr::serde_json::Error), + /// LNURL Pay + #[cfg(feature = "nip57")] + #[error(transparent)] + LnUrlPay(#[from] lnurl_pay::Error), + #[cfg(all(feature = "webln", target_arch = "wasm32"))] + #[error(transparent)] + WebLN(#[from] webln::Error), /// Generic NIP46 error #[cfg(feature = "nip46")] #[error("generic error")] @@ -120,6 +135,12 @@ pub enum Error { #[cfg(feature = "nip46")] #[error("response not match to the request")] ResponseNotMatchRequest, + /// Event not found + #[error("event not found: {0}")] + EventNotFound(EventId), + /// Impossible to zap + #[error("impossible to send zap: {0}")] + ImpossibleToZap(String), } /// Nostr client @@ -127,6 +148,8 @@ pub enum Error { pub struct Client { pool: RelayPool, signer: Arc>>, + #[cfg(feature = "nip57")] + zapper: Arc>>, opts: Options, dropped: Arc, } @@ -202,6 +225,8 @@ impl Client { Self { pool: RelayPool::with_database(builder.opts.pool, builder.database), signer: Arc::new(RwLock::new(builder.signer)), + #[cfg(feature = "nip57")] + zapper: Arc::new(RwLock::new(builder.zapper)), opts: builder.opts, dropped: Arc::new(AtomicBool::new(false)), } @@ -226,6 +251,22 @@ impl Client { *s = signer; } + /// Get current client zapper + /// + /// Rise error if it not set. + #[cfg(feature = "nip57")] + pub async fn zapper(&self) -> Result { + let zapper = self.zapper.read().await; + zapper.clone().ok_or(Error::ZapperNotConfigured) + } + + /// Set client zapper + #[cfg(feature = "nip57")] + pub async fn set_zapper(&self, zapper: Option) { + let mut s = self.zapper.write().await; + *s = zapper; + } + /// Get current [`Keys`] #[deprecated(since = "0.27.0", note = "Use `client.signer().await` instead.")] pub async fn keys(&self) -> Keys { @@ -757,6 +798,21 @@ impl Client { self.send_event_to(url, event).await } + /// Get public key metadata + /// + /// + pub async fn metadata(&self, public_key: XOnlyPublicKey) -> Result { + let filter: Filter = Filter::new() + .author(public_key) + .kind(Kind::Metadata) + .limit(1); + 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()), + } + } + /// Update metadata /// /// diff --git a/crates/nostr-sdk/src/client/zapper/mod.rs b/crates/nostr-sdk/src/client/zapper/mod.rs new file mode 100644 index 000000000..533745509 --- /dev/null +++ b/crates/nostr-sdk/src/client/zapper/mod.rs @@ -0,0 +1,123 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! Client Zapper + +use std::str::FromStr; + +use lnurl_pay::api::Lud06OrLud16; +use lnurl_pay::{LightningAddress, LnUrl}; +use nostr::nips::nip57::ZapType; +use nostr::secp256k1::XOnlyPublicKey; +use nostr::{Event, EventId, Filter, Metadata}; +#[cfg(all(feature = "webln", target_arch = "wasm32"))] +use webln::WebLN; + +use super::{Client, Error}; + +/// Zap entity +pub enum ZapEntity { + /// Zap to event + Event(EventId), + /// Zap to public key + PublicKey(XOnlyPublicKey), + /// Lightning Address + LUD16(LightningAddress), + /// LUD06 + LUD06(LnUrl), +} + +impl From for ZapEntity { + fn from(value: EventId) -> Self { + Self::Event(value) + } +} + +impl From for ZapEntity { + fn from(value: XOnlyPublicKey) -> Self { + Self::PublicKey(value) + } +} + +/// Client Zapper +#[derive(Debug, Clone)] +pub enum ClientZapper { + /// WebLN + #[cfg(all(feature = "webln", target_arch = "wasm32"))] + WebLN(WebLN), + /// NWC (TODO) + NWC, +} + +#[cfg(all(feature = "webln", target_arch = "wasm32"))] +impl From for ClientZapper { + fn from(value: WebLN) -> Self { + Self::WebLN(value) + } +} + +impl Client { + /// Send a Zap! + pub async fn zap(&self, to: T, satoshi: u64, r#_type: Option) -> Result<(), Error> + 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 zapper + let zapper: ClientZapper = self.zapper().await?; + + // Get entity metadata + let to: ZapEntity = to.into(); + let lud: Lud06OrLud16 = match to { + ZapEntity::Event(event_id) => { + // Get event + let filter: Filter = Filter::new().id(event_id); + let events: Vec = self.get_events_of(vec![filter], None).await?; + let event: &Event = events.first().ok_or(Error::EventNotFound(event_id))?; + let public_key: XOnlyPublicKey = event.author(); + let metadata: Metadata = self.metadata(public_key).await?; + + if let Some(lud16) = &metadata.lud16 { + LightningAddress::parse(lud16)?.into() + } else if let Some(lud06) = &metadata.lud06 { + LnUrl::from_str(lud06)?.into() + } else { + return Err(Error::ImpossibleToZap(String::from("LUD06/LUD16 not set"))); + } + } + ZapEntity::PublicKey(public_key) => { + let metadata: Metadata = self.metadata(public_key).await?; + + if let Some(lud16) = &metadata.lud16 { + LightningAddress::parse(lud16)?.into() + } else if let Some(lud06) = &metadata.lud06 { + LnUrl::from_str(lud06)?.into() + } else { + return Err(Error::ImpossibleToZap(String::from("LUD06/LUD16 not set"))); + } + } + ZapEntity::LUD16(lnaddr) => lnaddr.into(), + ZapEntity::LUD06(lud06) => lud06.into(), + }; + + // Get invoice + let _invoice: String = lnurl_pay::api::get_invoice(lud, satoshi * 1000, None, None).await?; + + match zapper { + #[cfg(all(feature = "webln", target_arch = "wasm32"))] + ClientZapper::WebLN(webln) => { + webln.enable().await?; + webln.send_payment(_invoice).await?; + } + ClientZapper::NWC => {} + }; + + Ok(()) + } +} diff --git a/crates/nostr-sdk/src/lib.rs b/crates/nostr-sdk/src/lib.rs index f4b136bbf..1f769c73f 100644 --- a/crates/nostr-sdk/src/lib.rs +++ b/crates/nostr-sdk/src/lib.rs @@ -28,6 +28,8 @@ pub use nostr_sqlite::{Error as SQLiteError, SQLiteDatabase}; use once_cell::sync::Lazy; #[cfg(feature = "blocking")] use tokio::runtime::Runtime; +#[cfg(all(feature = "webln", target_arch = "wasm32"))] +pub use webln; pub mod client; pub mod prelude; diff --git a/crates/nostr/src/nips/nip57.rs b/crates/nostr/src/nips/nip57.rs index c50176c26..88d88ef07 100644 --- a/crates/nostr/src/nips/nip57.rs +++ b/crates/nostr/src/nips/nip57.rs @@ -108,16 +108,16 @@ impl From for Error { } } -// /// Zap Type -// #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -// pub enum ZapType { -// Public -// Public, -// Private -// Private, -// Anonymous -// Anonymous, -// } +/// Zap Type +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ZapType { + /// Public + Public, + /// Private + Private, + /// Anonymous + Anonymous, +} /// Zap Request Data #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]