Skip to content

Commit f28bacb

Browse files
committed
Add nwc crate
1 parent 19c4415 commit f28bacb

File tree

8 files changed

+363
-0
lines changed

8 files changed

+363
-0
lines changed

crates/nostr-sdk/examples/nip47.rs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::str::FromStr;
55

66
use nostr_sdk::prelude::*;
77

8+
// Check `nwc` crate for high level client library!
9+
810
#[tokio::main]
911
async fn main() -> Result<()> {
1012
tracing_subscriber::fmt::init();

crates/nwc/Cargo.toml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "nwc"
3+
version = "0.27.0"
4+
edition = "2021"
5+
description = "NWC client and zapper backend for Nostr apps"
6+
authors.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
license.workspace = true
10+
readme = "README.md"
11+
rust-version.workspace = true
12+
keywords = ["nostr", "zapper", "nwc"]
13+
14+
[dependencies]
15+
async-utility.workspace = true
16+
nostr = { workspace = true, features = ["std", "nip47"] }
17+
nostr-relay-pool.workspace = true
18+
nostr-zapper.workspace = true
19+
thiserror.workspace = true
20+
tracing = { workspace = true, features = ["std"] }
21+
22+
[dev-dependencies]
23+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
24+
tracing-subscriber.workspace = true

crates/nwc/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# NWC
2+
3+
NWC client and zapper backend for Nostr apps
4+
5+
## State
6+
7+
**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways.
8+
9+
## Donations
10+
11+
`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).
12+
13+
## License
14+
15+
This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details

crates/nwc/examples/nwc.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2022-2023 Yuki Kishimoto
2+
// Copyright (c) 2023-2024 Rust Nostr Developers
3+
// Distributed under the MIT software license
4+
5+
use std::str::FromStr;
6+
7+
use nwc::prelude::*;
8+
9+
#[tokio::main]
10+
async fn main() -> Result<()> {
11+
tracing_subscriber::fmt::init();
12+
13+
let mut nwc_uri_string = String::new();
14+
let mut invoice = String::new();
15+
16+
println!("Please enter a NWC string");
17+
std::io::stdin()
18+
.read_line(&mut nwc_uri_string)
19+
.expect("Failed to read line");
20+
21+
println!("Please enter a BOLT 11 invoice");
22+
std::io::stdin()
23+
.read_line(&mut invoice)
24+
.expect("Failed to read line");
25+
26+
invoice = String::from(invoice.trim());
27+
28+
// Parse URI and compose NWC client
29+
let uri = NostrWalletConnectURI::from_str(&nwc_uri_string).expect("Failed to parse NWC URI");
30+
let nwc = NWC::new(uri).await?;
31+
32+
// Pay invoice
33+
nwc.send_payment(invoice).await?;
34+
35+
Ok(())
36+
}

crates/nwc/src/error.rs

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2022-2023 Yuki Kishimoto
2+
// Copyright (c) 2023-2024 Rust Nostr Developers
3+
// Distributed under the MIT software license
4+
5+
//! NWC error
6+
7+
use nostr::nips::nip47;
8+
use nostr_zapper::ZapperError;
9+
use thiserror::Error;
10+
11+
/// NWC error
12+
#[derive(Debug, Error)]
13+
pub enum Error {
14+
/// Zapper error
15+
#[error(transparent)]
16+
Zapper(#[from] ZapperError),
17+
/// NIP47 error
18+
#[error(transparent)]
19+
NIP47(#[from] nip47::Error),
20+
/// Relay
21+
#[error(transparent)]
22+
Relay(#[from] nostr_relay_pool::relay::Error),
23+
/// Pool
24+
#[error(transparent)]
25+
Pool(#[from] nostr_relay_pool::pool::Error),
26+
/// Request timeout
27+
#[error("timeout")]
28+
Timeout,
29+
}
30+
31+
impl From<Error> for ZapperError {
32+
fn from(e: Error) -> Self {
33+
Self::backend(e)
34+
}
35+
}

crates/nwc/src/lib.rs

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright (c) 2022-2023 Yuki Kishimoto
2+
// Copyright (c) 2023-2024 Rust Nostr Developers
3+
// Distributed under the MIT software license
4+
5+
//! NWC client and zapper backend for Nostr apps
6+
7+
#![forbid(unsafe_code)]
8+
#![warn(missing_docs)]
9+
#![warn(rustdoc::bare_urls)]
10+
#![allow(unknown_lints)]
11+
#![allow(clippy::arc_with_non_send_sync)]
12+
13+
use std::time::Duration;
14+
15+
pub extern crate nostr;
16+
pub extern crate nostr_zapper as zapper;
17+
18+
use async_utility::time;
19+
use nostr::nips::nip47::{
20+
MakeInvoiceRequestParams, MakeInvoiceResponseResult, Method, NostrWalletConnectURI,
21+
PayInvoiceRequestParams, PayInvoiceResponseResult, Request, RequestParams, Response,
22+
};
23+
use nostr::{Filter, Kind, SubscriptionId};
24+
use nostr_relay_pool::{FilterOptions, RelayPool, RelayPoolNotification, RelaySendOptions};
25+
use nostr_zapper::{async_trait, NostrZapper, ZapperBackend};
26+
27+
pub mod error;
28+
pub mod options;
29+
pub mod prelude;
30+
31+
pub use self::error::Error;
32+
pub use self::options::NostrWalletConnectOptions;
33+
34+
/// Nostr Wallet Connect client
35+
#[derive(Debug, Clone)]
36+
pub struct NWC {
37+
uri: NostrWalletConnectURI,
38+
pool: RelayPool,
39+
}
40+
41+
impl NWC {
42+
/// Compose new [NWC] client
43+
pub async fn new(uri: NostrWalletConnectURI) -> Result<Self, Error> {
44+
Self::with_opts(uri, NostrWalletConnectOptions::default()).await
45+
}
46+
47+
/// Compose new [NWC] client with [NostrWalletConnectOptions]
48+
pub async fn with_opts(
49+
uri: NostrWalletConnectURI,
50+
opts: NostrWalletConnectOptions,
51+
) -> Result<Self, Error> {
52+
// Compose pool
53+
let pool = RelayPool::new(opts.pool);
54+
pool.add_relay(&uri.relay_url, opts.relay).await?;
55+
pool.connect(Some(Duration::from_secs(10))).await;
56+
57+
Ok(Self { uri, pool })
58+
}
59+
60+
/// Create invoice
61+
pub async fn make_invoice(
62+
&self,
63+
satoshi: u64,
64+
description: Option<String>,
65+
expiry: Option<u64>,
66+
) -> Result<String, Error> {
67+
// Compose NWC request event
68+
let req = Request {
69+
method: Method::MakeInvoice,
70+
params: RequestParams::MakeInvoice(MakeInvoiceRequestParams {
71+
amount: satoshi * 1000,
72+
description,
73+
description_hash: None,
74+
expiry,
75+
}),
76+
};
77+
let event = req.to_event(&self.uri)?;
78+
let event_id = event.id;
79+
80+
// Subscribe
81+
let relay = self.pool.relay(&self.uri.relay_url).await?;
82+
let id = SubscriptionId::generate();
83+
let filter = Filter::new()
84+
.author(self.uri.public_key)
85+
.kind(Kind::WalletConnectResponse)
86+
.event(event_id)
87+
.limit(1);
88+
89+
// Subscribe
90+
relay
91+
.send_req(
92+
id,
93+
vec![filter],
94+
Some(FilterOptions::WaitForEventsAfterEOSE(1)),
95+
)
96+
.await?;
97+
98+
let mut notifications = self.pool.notifications();
99+
100+
// Send request
101+
self.pool
102+
.send_event_to([&self.uri.relay_url], event, RelaySendOptions::new())
103+
.await?;
104+
105+
time::timeout(Some(Duration::from_secs(10)), async {
106+
while let Ok(notification) = notifications.recv().await {
107+
if let RelayPoolNotification::Event { event, .. } = notification {
108+
if event.kind() == Kind::WalletConnectResponse
109+
&& event.event_ids().next().copied() == Some(event_id)
110+
{
111+
let res = Response::from_event(&self.uri, &event)?;
112+
let MakeInvoiceResponseResult { invoice, .. } = res.to_make_invoice()?;
113+
return Ok(invoice);
114+
}
115+
}
116+
}
117+
118+
Err(Error::Timeout)
119+
})
120+
.await
121+
.ok_or(Error::Timeout)?
122+
}
123+
124+
/// Pay invoice
125+
pub async fn send_payment(&self, invoice: String) -> Result<(), Error> {
126+
// Compose NWC request event
127+
let req = Request {
128+
method: Method::PayInvoice,
129+
params: RequestParams::PayInvoice(PayInvoiceRequestParams {
130+
id: None,
131+
invoice,
132+
amount: None,
133+
}),
134+
};
135+
let event = req.to_event(&self.uri)?;
136+
let event_id = event.id;
137+
138+
// Subscribe
139+
let relay = self.pool.relay(&self.uri.relay_url).await?;
140+
let id = SubscriptionId::generate();
141+
let filter = Filter::new()
142+
.author(self.uri.public_key)
143+
.kind(Kind::WalletConnectResponse)
144+
.event(event_id)
145+
.limit(1);
146+
147+
// Subscribe
148+
relay
149+
.send_req(
150+
id,
151+
vec![filter],
152+
Some(FilterOptions::WaitForEventsAfterEOSE(1)),
153+
)
154+
.await?;
155+
156+
let mut notifications = self.pool.notifications();
157+
158+
// Send request
159+
self.pool
160+
.send_event_to([&self.uri.relay_url], event, RelaySendOptions::new())
161+
.await?;
162+
163+
time::timeout(Some(Duration::from_secs(10)), async {
164+
while let Ok(notification) = notifications.recv().await {
165+
if let RelayPoolNotification::Event { event, .. } = notification {
166+
if event.kind() == Kind::WalletConnectResponse
167+
&& event.event_ids().next().copied() == Some(event_id)
168+
{
169+
let res = Response::from_event(&self.uri, &event)?;
170+
let PayInvoiceResponseResult { preimage } = res.to_pay_invoice()?;
171+
tracing::info!("Invoice paid! Preimage: {preimage}");
172+
break;
173+
}
174+
}
175+
}
176+
177+
Ok::<(), Error>(())
178+
})
179+
.await
180+
.ok_or(Error::Timeout)?
181+
}
182+
}
183+
184+
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
185+
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
186+
impl NostrZapper for NWC {
187+
type Err = Error;
188+
189+
fn backend(&self) -> ZapperBackend {
190+
ZapperBackend::NWC
191+
}
192+
193+
async fn pay_invoice(&self, invoice: String) -> Result<(), Self::Err> {
194+
self.send_payment(invoice).await
195+
}
196+
}

crates/nwc/src/options.rs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2022-2023 Yuki Kishimoto
2+
// Copyright (c) 2023-2024 Rust Nostr Developers
3+
// Distributed under the MIT software license
4+
5+
//! NWC Options
6+
7+
#[cfg(not(target_arch = "wasm32"))]
8+
use std::net::SocketAddr;
9+
10+
use nostr_relay_pool::{RelayOptions, RelayPoolOptions};
11+
12+
/// NWC options
13+
#[derive(Debug, Clone, Default)]
14+
pub struct NostrWalletConnectOptions {
15+
pub(super) relay: RelayOptions,
16+
pub(super) pool: RelayPoolOptions,
17+
}
18+
19+
impl NostrWalletConnectOptions {
20+
/// New default NWC options
21+
pub fn new() -> Self {
22+
Self::default()
23+
}
24+
25+
/// Set proxy
26+
#[cfg(not(target_arch = "wasm32"))]
27+
pub fn proxy(self, proxy: Option<SocketAddr>) -> Self {
28+
Self {
29+
relay: self.relay.proxy(proxy),
30+
..self
31+
}
32+
}
33+
34+
/// Automatically shutdown relay pool on drop
35+
pub fn shutdown_on_drop(self, shutdown_on_drop: bool) -> Self {
36+
Self {
37+
pool: self.pool.shutdown_on_drop(shutdown_on_drop),
38+
..self
39+
}
40+
}
41+
}

crates/nwc/src/prelude.rs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2022-2023 Yuki Kishimoto
2+
// Copyright (c) 2023-2024 Rust Nostr Developers
3+
// Distributed under the MIT software license
4+
5+
//! Prelude
6+
7+
#![allow(unknown_lints)]
8+
#![allow(ambiguous_glob_reexports)]
9+
#![doc(hidden)]
10+
11+
pub use nostr::prelude::*;
12+
pub use nostr_zapper::prelude::*;
13+
14+
pub use crate::*;

0 commit comments

Comments
 (0)