From 6f9eaa8b27c7a2f2f1175bf77b9da888432ec622 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 1 Apr 2024 00:01:57 +0700 Subject: [PATCH 1/5] feat: subscribe to NIP-47 notifications --- examples/nwc/client/subscribe.js | 34 ++++++++ src/NWCClient.ts | 137 +++++++++++++++++-------------- 2 files changed, 108 insertions(+), 63 deletions(-) create mode 100644 examples/nwc/client/subscribe.js diff --git a/examples/nwc/client/subscribe.js b/examples/nwc/client/subscribe.js new file mode 100644 index 0000000..588962f --- /dev/null +++ b/examples/nwc/client/subscribe.js @@ -0,0 +1,34 @@ +import * as crypto from "node:crypto"; // required in node.js +global.crypto = crypto; // required in node.js +import "websocket-polyfill"; // required in node.js + +import * as readline from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; + +import { nwc } from "../../../dist/index.module.js"; + +const rl = readline.createInterface({ input, output }); + +const nwcUrl = + process.env.NWC_URL || + (await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): ")); +rl.close(); + +const client = new nwc.NWCClient({ + nostrWalletConnectUrl: nwcUrl, +}); + +const onNotification = (notification) => + console.info("Got notification", notification); + +const unsub = await client.subscribeNotifications(onNotification); + +console.info("Waiting for notifications..."); +process.on("SIGINT", function () { + console.info("Caught interrupt signal"); + + unsub(); + client.close(); + + process.exit(); +}); diff --git a/src/NWCClient.ts b/src/NWCClient.ts index db5ef37..1537343 100644 --- a/src/NWCClient.ts +++ b/src/NWCClient.ts @@ -578,6 +578,40 @@ export class NWCClient { } } + // TODO: add typings + async subscribeNotifications( + onNotification: (notification: unknown) => void, + ): Promise<() => void> { + const sub = this.relay.sub([ + { + kinds: [23196], + authors: [this.walletPubkey], + }, + ]); + await this._checkConnected(); + + sub.on("event", async (event) => { + const decryptedContent = await this.decrypt( + this.walletPubkey, + event.content, + ); + let notification; + try { + notification = JSON.parse(decryptedContent); + } catch (e) { + console.error("Failed to parse decrypted event content", e); + return; + } + if (notification.result) { + onNotification(notification); + } else { + console.error("No result in response", notification); + } + }); + + return () => sub.unsub(); + } + private async executeNip47Request( nip47Method: Nip47SingleMethod, params: unknown, @@ -649,38 +683,27 @@ export class NWCClient { ); return; } - if (event.kind == 23195) { - if (response.result) { - // console.info("NIP-47 result", response.result); - if (resultValidator(response.result)) { - resolve(response.result); - } else { - clearTimeout(replyTimeoutCheck); - sub.unsub(); - reject( - new Nip47ResponseValidationError( - "response from NWC failed validation: " + - JSON.stringify(response.result), - "INTERNAL", - ), - ); - } + if (response.result) { + // console.info("NIP-47 result", response.result); + if (resultValidator(response.result)) { + resolve(response.result); } else { clearTimeout(replyTimeoutCheck); sub.unsub(); - // console.error("Wallet error", response.error); reject( - new Nip47WalletError( - response.error?.message || "unknown Error", - response.error?.code || "INTERNAL", + new Nip47ResponseValidationError( + "response from NWC failed validation: " + + JSON.stringify(response.result), + "INTERNAL", ), ); } } else { clearTimeout(replyTimeoutCheck); sub.unsub(); + // console.error("Wallet error", response.error); reject( - new Nip47UnexpectedResponseError( + new Nip47WalletError( response.error?.message || "unknown Error", response.error?.code || "INTERNAL", ), @@ -790,53 +813,41 @@ export class NWCClient { ), ); } - if (event.kind == 23195) { - if (response.result) { - // console.info("NIP-47 result", response.result); - if (!resultValidator(response.result)) { - clearTimeout(replyTimeoutCheck); - sub.unsub(); - reject( - new Nip47ResponseValidationError( - "Response from NWC failed validation: " + - JSON.stringify(response.result), - "INTERNAL", - ), - ); - return; - } - const dTag = event.tags.find((tag) => tag[0] === "d")?.[1]; - if (dTag === undefined) { - clearTimeout(replyTimeoutCheck); - sub.unsub(); - reject( - new Nip47ResponseValidationError( - "No d tag found in response event", - "INTERNAL", - ), - ); - return; - } - results.push({ - ...response.result, - dTag, - }); - if (results.length === numPayments) { - clearTimeout(replyTimeoutCheck); - sub.unsub(); - //console.log("Received results", results); - resolve(results); - } - } else { + if (response.result) { + // console.info("NIP-47 result", response.result); + if (!resultValidator(response.result)) { clearTimeout(replyTimeoutCheck); sub.unsub(); - // console.error("Wallet error", response.error); reject( - new Nip47WalletError( - response.error?.message, - response.error?.code, + new Nip47ResponseValidationError( + "Response from NWC failed validation: " + + JSON.stringify(response.result), + "INTERNAL", ), ); + return; + } + const dTag = event.tags.find((tag) => tag[0] === "d")?.[1]; + if (dTag === undefined) { + clearTimeout(replyTimeoutCheck); + sub.unsub(); + reject( + new Nip47ResponseValidationError( + "No d tag found in response event", + "INTERNAL", + ), + ); + return; + } + results.push({ + ...response.result, + dTag, + }); + if (results.length === numPayments) { + clearTimeout(replyTimeoutCheck); + sub.unsub(); + //console.log("Received results", results); + resolve(results); } } else { clearTimeout(replyTimeoutCheck); From bdf48742c21eea08ea68a2e3a6e93c10e6c4931f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 9 Apr 2024 11:03:52 +0700 Subject: [PATCH 2/5] fix: only subscribe to notifications tagged for the app pubkey --- src/NWCClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NWCClient.ts b/src/NWCClient.ts index 1537343..30f16a7 100644 --- a/src/NWCClient.ts +++ b/src/NWCClient.ts @@ -586,6 +586,7 @@ export class NWCClient { { kinds: [23196], authors: [this.walletPubkey], + "#p": [this.publicKey], }, ]); await this._checkConnected(); From f3a7bb8be5b949b8d633e56d2969f86b2e40c9f1 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 9 Apr 2024 12:52:12 +0700 Subject: [PATCH 3/5] fix: re-connect to relay while subscribing --- src/NWCClient.ts | 94 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/src/NWCClient.ts b/src/NWCClient.ts index 30f16a7..0348f3c 100644 --- a/src/NWCClient.ts +++ b/src/NWCClient.ts @@ -9,6 +9,7 @@ import { Event, UnsignedEvent, finishEvent, + Sub, } from "nostr-tools"; import { NWCAuthorizationUrlOptions } from "./types"; @@ -582,35 +583,74 @@ export class NWCClient { async subscribeNotifications( onNotification: (notification: unknown) => void, ): Promise<() => void> { - const sub = this.relay.sub([ - { - kinds: [23196], - authors: [this.walletPubkey], - "#p": [this.publicKey], - }, - ]); - await this._checkConnected(); - - sub.on("event", async (event) => { - const decryptedContent = await this.decrypt( - this.walletPubkey, - event.content, - ); - let notification; - try { - notification = JSON.parse(decryptedContent); - } catch (e) { - console.error("Failed to parse decrypted event content", e); - return; - } - if (notification.result) { - onNotification(notification); - } else { - console.error("No result in response", notification); + let subscribed = true; + let endPromise: (() => void) | undefined; + let onRelayDisconnect: (() => void) | undefined; + let sub: Sub<23196> | undefined; + (async () => { + while (subscribed) { + try { + await this._checkConnected(); + sub = this.relay.sub([ + { + kinds: [23196], + authors: [this.walletPubkey], + "#p": [this.publicKey], + }, + ]); + console.info("subscribed to relay", sub); + + sub.on("event", async (event) => { + const decryptedContent = await this.decrypt( + this.walletPubkey, + event.content, + ); + let notification; + try { + notification = JSON.parse(decryptedContent); + } catch (e) { + console.error("Failed to parse decrypted event content", e); + return; + } + if (notification.result) { + onNotification(notification); + } else { + console.error("No result in response", notification); + } + }); + + await new Promise((resolve) => { + endPromise = () => { + resolve(); + }; + onRelayDisconnect = () => { + console.info("relay disconnected"); + endPromise?.(); + }; + this.relay.on("disconnect", onRelayDisconnect); + }); + if (onRelayDisconnect !== undefined) { + this.relay.off("disconnect", onRelayDisconnect); + } + } catch (error) { + console.error( + "error subscribing to notifications", + error || "unknown relay error", + ); + } + if (subscribed) { + // wait a second and try re-connecting + // any notifications during this period will be lost + await new Promise((resolve) => setTimeout(resolve, 1000)); + } } - }); + })(); - return () => sub.unsub(); + return () => { + subscribed = false; + endPromise?.(); + sub?.unsub(); + }; } private async executeNip47Request( From 35a25769c730a3354096f0c0f716832c97d23fca Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 10 Apr 2024 00:18:16 +0700 Subject: [PATCH 4/5] fix: notification check --- src/NWCClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NWCClient.ts b/src/NWCClient.ts index 0348f3c..20dfb5f 100644 --- a/src/NWCClient.ts +++ b/src/NWCClient.ts @@ -598,7 +598,7 @@ export class NWCClient { "#p": [this.publicKey], }, ]); - console.info("subscribed to relay", sub); + console.info("subscribed to relay"); sub.on("event", async (event) => { const decryptedContent = await this.decrypt( @@ -612,10 +612,10 @@ export class NWCClient { console.error("Failed to parse decrypted event content", e); return; } - if (notification.result) { + if (notification.notification) { onNotification(notification); } else { - console.error("No result in response", notification); + console.error("No notification in response", notification); } }); From f90c62a20b14224d2aa73503aa2e9fddb52a8d0c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 10 Apr 2024 00:20:38 +0700 Subject: [PATCH 5/5] chore: add nip47 notification typings --- src/NWCClient.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/NWCClient.ts b/src/NWCClient.ts index 20dfb5f..06d918f 100644 --- a/src/NWCClient.ts +++ b/src/NWCClient.ts @@ -99,6 +99,11 @@ export type Nip47Transaction = { metadata?: Record; }; +export type Nip47Notification = { + notification_type: "payment_received"; + notification: Nip47Transaction; +}; /* | { notification_type: "other_type", notification: OtherTypeHere } */ + export type Nip47PayInvoiceRequest = { invoice: string; amount?: number; // msats @@ -579,9 +584,8 @@ export class NWCClient { } } - // TODO: add typings async subscribeNotifications( - onNotification: (notification: unknown) => void, + onNotification: (notification: Nip47Notification) => void, ): Promise<() => void> { let subscribed = true; let endPromise: (() => void) | undefined; @@ -607,7 +611,7 @@ export class NWCClient { ); let notification; try { - notification = JSON.parse(decryptedContent); + notification = JSON.parse(decryptedContent) as Nip47Notification; } catch (e) { console.error("Failed to parse decrypted event content", e); return;