add nip47-notifications option

This commit is contained in:
daywalker90 2025-04-18 15:12:30 +02:00
parent 3fda9e9960
commit b4ebfaaefd
No known key found for this signature in database
7 changed files with 100 additions and 10 deletions

View file

@ -4,6 +4,7 @@
### Added
- ``nip47-create`` and ``nip47-list``: add ``clientkey_public`` and ``walletkey_public`` to output. These are useful for private relay whitelists.
- ``nip47-notifications``: new option to enable/disable nip47 notifications. Usefule if you don't need them and want to use public relays that may rate limit you.
### Fixed
- ``nip47-revoke``: actually stop task if no relays were ever connected

View file

@ -42,12 +42,13 @@ Note: Release binaries are built using ``cross`` and the ``optimized`` profile.
# Documentation
## Relays
It is highly recommended to use your own relay since public relays may limit content length, amount of public keys per IP or require unsupported things like proof of work or payments. Each NWC you create is a separate public key and the ``list_transactions`` method can have quite a large content length!
It is highly recommended to use your own private relay since public relays may limit content length, amount of public keys per IP or require unsupported things like proof of work or payments. Each NWC you create is a separate public key and the ``list_transactions`` method can have quite a large content length! If you still want to use public relays, consider if you need nip47 notifications: if not, disable them with ``nip47-notifications=false``. This will reduce the amount of events send to the relay and maybe not get you rate limited as quickly.
For example you can use [nostr-rs-relay](https://github.com/scsibug/nostr-rs-relay) with ``pubkey_whitelist`` set to both ``clientkey_public`` and ``walletkey_public`` (returned from ``nip47-create``/``nip47-list``).
For a private relay you can for example use [nostr-rs-relay](https://github.com/scsibug/nostr-rs-relay) with ``pubkey_whitelist`` set to both ``clientkey_public`` and ``walletkey_public`` (returned from ``nip47-create``/``nip47-list``).
## Options
* `nip47-relays`: Specify the relays that you want to use with your NWC. Can be set multiple times to use multiple relays. You must set this atleast one time.
* ``nip47-relays``: Specify the relays that you want to use with your NWC. Can be set multiple times to use multiple relays. NWC's you create will save these and even if you add or remove relays keep the relays from the moment you created that NWC. You must set this atleast one time.
* ``nip47-notifications``: Enable/disable nip47 notifications. Default is enabled (``true``)
## Methods
* **nip47-create** *label* [*budget_msat*] [*interval*]

View file

@ -2,7 +2,7 @@ use std::{path::Path, time::Duration};
use anyhow::anyhow;
use cln_plugin::{
options::{ConfigOption, StringArrayConfigOption},
options::{ConfigOption, DefaultBooleanConfigOption, StringArrayConfigOption},
Builder, Plugin,
};
use cln_rpc::{model::requests::ListdatastoreRequest, ClnRpc};
@ -34,6 +34,11 @@ const OPT_RELAYS: StringArrayConfigOption = ConfigOption::new_str_arr_no_default
"nip47-relays",
"Nostr relays used for nwc. Can be stated multiple times.",
);
const OPT_NOTIFICATIONS: DefaultBooleanConfigOption = ConfigOption::new_bool_with_default(
"nip47-notifications",
true,
"Enable/disable nip47-notifications. Default is `true`",
);
pub const PLUGIN_NAME: &str = "cln-nip47";
#[tokio::main]
@ -48,6 +53,7 @@ async fn main() -> Result<(), anyhow::Error> {
let confplugin = match Builder::new(tokio::io::stdin(), tokio::io::stdout())
.option(OPT_RELAYS)
.option(OPT_NOTIFICATIONS)
.rpcmethod("nip47-create", "Create a new nwc", nwc_create)
.rpcmethod("nip47-revoke", "Revoke a nwc", nwc_revoke)
.rpcmethod("nip47-budget", "Set budget of a nwc", nwc_budget)

View file

@ -8,6 +8,7 @@ use crate::nwc_lookups::{list_transactions, lookup_invoice};
use crate::nwc_pay::{multi_pay_invoice, pay_invoice};
use crate::structs::{NwcStore, PluginState};
use crate::tasks::budget_task;
use crate::OPT_NOTIFICATIONS;
use cln_plugin::Plugin;
use nostr_sdk::nips::*;
use nostr_sdk::Client;
@ -66,15 +67,21 @@ pub async fn run_nwc(
time::sleep(Duration::from_secs(5)).await;
continue;
}
let info_event = match EventBuilder::new(
let mut info_event_builder = EventBuilder::new(
Kind::WalletConnectInfo,
"pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_invoice \
lookup_invoice list_transactions get_balance get_info",
)
.tag(Tag::parse(vec!["encryption", "nip44_v2 nip04"]).unwrap())
.tag(Tag::parse(vec!["notifications", "payment_received payment_sent"]).unwrap())
.sign_with_keys(&wallet_keys)
{
.tag(Tag::parse(vec!["encryption", "nip44_v2 nip04"]).unwrap());
if plugin.option(&OPT_NOTIFICATIONS).unwrap() {
info_event_builder = info_event_builder.tag(
Tag::parse(vec!["notifications", "payment_received payment_sent"]).unwrap(),
)
}
let info_event = match info_event_builder.sign_with_keys(&wallet_keys) {
Ok(o) => o,
Err(e) => {
log::warn!("Could not sign info_event! {}", e);

View file

@ -6,6 +6,7 @@ use nostr_sdk::nips::*;
use nostr_sdk::*;
use crate::structs::PluginState;
use crate::OPT_NOTIFICATIONS;
pub async fn get_info(
plugin: Plugin<PluginState>,
@ -39,6 +40,11 @@ pub async fn get_info(
"bitcoin" => "mainnet".to_owned(),
_ => get_info.network,
};
let notifications = if plugin.option(&OPT_NOTIFICATIONS).unwrap() {
vec!["payment_received".to_owned(), "payment_sent".to_owned()]
} else {
vec![]
};
Ok(nip47::GetInfoResponse {
alias: get_info.alias,
@ -58,6 +64,6 @@ pub async fn get_info(
"get_balance".to_owned(),
"get_info".to_owned(),
],
notifications: vec!["payment_received".to_owned(), "payment_sent".to_owned()],
notifications,
})
}

View file

@ -9,6 +9,7 @@ use cln_rpc::primitives::Sha256;
use cln_rpc::ClnRpc;
use crate::structs::PluginState;
use crate::OPT_NOTIFICATIONS;
use nostr_sdk::nips::*;
use nostr_sdk::*;
@ -17,6 +18,9 @@ pub async fn payment_received_handler(
plugin: Plugin<PluginState>,
args: serde_json::Value,
) -> Result<(), anyhow::Error> {
if !plugin.option(&OPT_NOTIFICATIONS).unwrap() {
return Ok(());
}
let label = args
.get("invoice_payment")
.ok_or_else(|| anyhow!("Malformed invoice_payment notification: missing invoice_payment"))?
@ -180,6 +184,9 @@ pub async fn payment_sent_handler(
plugin: Plugin<PluginState>,
args: serde_json::Value,
) -> Result<(), anyhow::Error> {
if !plugin.option(&OPT_NOTIFICATIONS).unwrap() {
return Ok(());
}
let payment_hash = args
.get("sendpay_success")
.ok_or_else(|| anyhow!("Malformed sendpay_success notification: missing sendpay_success"))?

View file

@ -120,6 +120,35 @@ async def test_get_info(node_factory, get_plugin, nostr_client): # noqa: F811
assert get_info.notifications == ["payment_received", "payment_sent"]
assert get_info.pubkey == node_get_info["id"]
l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"})
l1.rpc.call(
"plugin",
{
"subcommand": "start",
"plugin": str(get_plugin),
"nip47-notifications": False,
},
)
l1.daemon.wait_for_log("All NWC's loaded")
get_info = await nwc.get_info()
assert get_info.alias == node_get_info["alias"]
assert get_info.block_height == node_get_info["blockheight"]
assert get_info.color == node_get_info["color"]
assert get_info.methods == [
"pay_invoice",
"multi_pay_invoice",
"pay_keysend",
"multi_pay_keysend",
"make_invoice",
"lookup_invoice",
"list_transactions",
"get_balance",
"get_info",
]
assert get_info.network == "regtest"
assert get_info.notifications == []
assert get_info.pubkey == node_get_info["id"]
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
@ -734,6 +763,39 @@ async def test_notifications(node_factory, get_plugin, nostr_client): # noqa: F
assert sent_events[0]["notification"]["settled_at"] == pay1_list["completed_at"]
assert "metadata" not in sent_events[0]["notification"]
l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"})
l1.rpc.call(
"plugin",
{
"subcommand": "start",
"plugin": str(get_plugin),
"nip47-notifications": False,
},
)
l1.daemon.wait_for_log("All NWC's loaded")
invoice = l3.rpc.call(
"invoice",
{
"label": generate_random_label(),
"description": "test3",
"amount_msat": 500,
},
)
pay1 = await nwc.pay_invoice(
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
)
events = await nostr_client.fetch_events(
response_filter, timeout=timedelta(seconds=10)
)
start_time = datetime.now()
while (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
events = await nostr_client.fetch_events(
response_filter, timeout=timedelta(seconds=1)
)
assert events.len() == 2
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio