From bf3d9fb692cb3bfe3f3b8fd889971ccadfd9b208 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 21 Aug 2023 19:21:57 +0700 Subject: [PATCH 1/4] feat: add lookup invoice to nwc webln provider --- examples/nwc/lookup-invoice.js | 29 +++++++++++++++++++++++++++++ src/webln/NostrWeblnProvider.ts | 26 +++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 examples/nwc/lookup-invoice.js diff --git a/examples/nwc/lookup-invoice.js b/examples/nwc/lookup-invoice.js new file mode 100644 index 0000000..fe84967 --- /dev/null +++ b/examples/nwc/lookup-invoice.js @@ -0,0 +1,29 @@ +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 { webln as providers } from "../../dist/index.module.js"; + +const rl = readline.createInterface({ input, output }); + +const nwcUrl = await rl.question( + "Nostr Wallet Connect URL (nostrwalletconnect://...): ", +); +rl.close(); + +const webln = new providers.NostrWebLNProvider({ + nostrWalletConnectUrl: nwcUrl, +}); +await webln.enable(); +const response = await webln.lookupInvoice({ + // provide one of the below + //invoice: 'lnbc10n1pjwxschpp5hg0pw234n9ww9q4uy25pnvu8y4jzpznysasyf7m9fka36t7fahysdqufet5xgzhv43ycn3qv4uxzmtsd3jscqzzsxqyz5vqsp5uw023qhxuxqfj69rvj9yns5gufczad5gqw4uer5cgqhw90slkavs9qyyssqvv2tw6c30ssgtpejc3zk7ns0svuj8547d8wxj0e36hltljx5a8x4qj59mk2y7qlt6qazf2j38fzc8uag3887nslxz6fe3vnyvg0f2xqqnlvcu2', + payment_hash: 'ba1e172a35995ce282bc22a819b3872564208a64876044fb654dbb1d2fc9edc9' +}); + +console.log(response); + +webln.close(); diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index df2cd09..7b3712a 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -40,6 +40,16 @@ interface GetBalanceResponse { budget_renewal?: string; } +interface LookupInvoiceArgs { + invoice?: string; + payment_hash?: string; +} + +interface LookupInvoiceResponse { + invoice: string; + paid: boolean; +} + interface NostrWebLNOptions { authorizationUrl?: string; // the URL to the NWC interface for the user to confirm the session relayUrl: string; @@ -214,7 +224,7 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { // TODO: use NIP-47 get_info call async getInfo(): Promise { return { - methods: ["getInfo", "sendPayment", "addinvoice", "getBalance"], + methods: ["getInfo", "sendPayment", "addinvoice", "getBalance", "lookupinvoice"], node: {} as WebLNNode, supports: ["lightning"], version: "NWC", @@ -258,6 +268,7 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { ): Promise<{ status: "OK" } | { status: "ERROR"; reason: string }> { throw new Error("Method not implemented."); } + makeInvoice(args: string | number | RequestInvoiceArgs) { this.checkConnected(); @@ -283,6 +294,19 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { (result) => ({ paymentRequest: result.invoice }), ); } + + lookupInvoice(args: LookupInvoiceArgs) { + this.checkConnected(); + + return this.executeNip47Request( + "lookup_invoice", + "lookupinvoice", + args, + (result) => result.invoice !== undefined && result.paid !== undefined, + (result) => ({ paymentRequest: result.invoice, paid: result.paid }), + ); + } + request(method: RequestMethod, args?: unknown): Promise { throw new Error("Method not implemented."); } From 99407d8c3fc57688e98d066de7fc3018f30e25cc Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 22 Aug 2023 13:30:13 +0700 Subject: [PATCH 2/4] chore: upgrade webln-types, fix webLN method names, fix linting errors --- package.json | 2 +- src/webln/NostrWeblnProvider.ts | 250 ++++++++++++++++---------------- yarn.lock | 8 +- 3 files changed, 131 insertions(+), 129 deletions(-) diff --git a/package.json b/package.json index eddec5b..853e969 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@types/node": "^18.11.0", "@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/parser": "^6.3.0", - "@webbtc/webln-types": "^1.0.11", + "@webbtc/webln-types": "^2.0.0", "browserify": "^17.0.0", "eslint": "^8.46.0", "eslint-config-prettier": "^9.0.0", diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index 7b3712a..62fa62a 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -12,14 +12,18 @@ import { Kind, } from "nostr-tools"; import { + GetBalanceResponse, KeysendArgs, RequestInvoiceArgs, - RequestInvoiceResponse, - RequestMethod, + MakeInvoiceResponse, SendPaymentResponse, SignMessageResponse, WebLNNode, WebLNProvider, + WebLNMethod, + WebLNRequestMethod, + LookupInvoiceArgs, + LookupInvoiceResponse, } from "@webbtc/webln-types"; import { GetInfoResponse } from "@webbtc/webln-types"; import { GetNWCAuthorizationUrlOptions } from "../types"; @@ -33,23 +37,6 @@ const NWCs: Record = { }, }; -// TODO: fetch this from @webbtc/webln-types -interface GetBalanceResponse { - balance: number; - max_amount?: number; - budget_renewal?: string; -} - -interface LookupInvoiceArgs { - invoice?: string; - payment_hash?: string; -} - -interface LookupInvoiceResponse { - invoice: string; - paid: boolean; -} - interface NostrWebLNOptions { authorizationUrl?: string; // the URL to the NWC interface for the user to confirm the session relayUrl: string; @@ -68,7 +55,7 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { secret: string | undefined; walletPubkey: string; options: NostrWebLNOptions; - subscribers: Record void>; + subscribers: Record void>; static parseWalletConnectUrl(walletConnectUrl: string) { walletConnectUrl = walletConnectUrl @@ -135,7 +122,6 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { ) as string; this.subscribers = {}; - // @ts-ignore if (globalThis.WebSocket === undefined) { console.error( "WebSocket is undefined. Make sure to `import websocket-polyfill` for nodejs environments", @@ -147,7 +133,7 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { this.subscribers[name] = callback; } - notify(name: string, payload?: any) { + notify(name: string, payload?: unknown) { const callback = this.subscribers[name]; if (callback) { callback(payload); @@ -224,21 +210,25 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { // TODO: use NIP-47 get_info call async getInfo(): Promise { return { - methods: ["getInfo", "sendPayment", "addinvoice", "getBalance", "lookupinvoice"], + methods: [ + "getInfo", + "sendPayment", + "makeInvoice", + "getBalance", + "lookupInvoice", + ], node: {} as WebLNNode, supports: ["lightning"], version: "NWC", }; } - // TODO: refactor code in getBalance and sendPayment - getBalance(): Promise { + getBalance() { this.checkConnected(); - // FIXME: add getBalance to webln-types - return this.executeNip47Request( + return this.executeNip47Request( "get_balance", - "getBalance" as RequestMethod, + "getBalance", undefined, (result) => result.balance !== undefined, (result) => result, @@ -248,7 +238,7 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { sendPayment(invoice: string) { this.checkConnected(); - return this.executeNip47Request( + return this.executeNip47Request( "pay_invoice", "sendPayment", { @@ -268,7 +258,7 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { ): Promise<{ status: "OK" } | { status: "ERROR"; reason: string }> { throw new Error("Method not implemented."); } - + makeInvoice(args: string | number | RequestInvoiceArgs) { this.checkConnected(); @@ -282,9 +272,9 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { throw new Error("No amount specified"); } - return this.executeNip47Request( + return this.executeNip47Request( "make_invoice", - "addinvoice", + "makeInvoice", { amount, description: requestInvoiceArgs?.defaultMemo, @@ -298,16 +288,19 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { lookupInvoice(args: LookupInvoiceArgs) { this.checkConnected(); - return this.executeNip47Request( + return this.executeNip47Request< + LookupInvoiceResponse, + { invoice: string; paid: boolean } + >( "lookup_invoice", - "lookupinvoice", + "lookupInvoice", args, (result) => result.invoice !== undefined && result.paid !== undefined, (result) => ({ paymentRequest: result.invoice, paid: result.paid }), ); } - request(method: RequestMethod, args?: unknown): Promise { + request(method: WebLNRequestMethod, args?: unknown): Promise { throw new Error("Method not implemented."); } signMessage(message: string): Promise { @@ -380,7 +373,10 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { } }; - const onMessage = (message: { data: any; origin: string }) => { + const onMessage = (message: { + data?: { type: "nwc:success" | unknown }; + origin: string; + }) => { const data = message.data; if ( data && @@ -408,103 +404,109 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { } } - private executeNip47Request( - method: string, - weblnRequestMethod: RequestMethod, - params: any, - resultValidator: (result: any) => boolean, - resultMapper: (result: any) => any, + private executeNip47Request( + nip47Method: string, + weblnMethod: WebLNMethod, + params: unknown, + resultValidator: (result: R) => boolean, + resultMapper: (result: R) => T, ) { - return new Promise(async (resolve, reject) => { - const command = { - method, - params, - }; - const encryptedCommand = await this.encrypt( - this.walletPubkey, - JSON.stringify(command), - ); - const unsignedEvent: UnsignedEvent = { - kind: 23194 as Kind, - created_at: Math.floor(Date.now() / 1000), - tags: [["p", this.walletPubkey]], - content: encryptedCommand, - pubkey: this.publicKey, - }; - - const event = await this.signEvent(unsignedEvent); - // subscribe to NIP_47_SUCCESS_RESPONSE_KIND and NIP_47_ERROR_RESPONSE_KIND - // that reference the request event (NIP_47_REQUEST_KIND) - let sub = this.relay.sub([ - { - kinds: [23195], - authors: [this.walletPubkey], - "#e": [event.id], - }, - ]); - - function replyTimeout() { - sub.unsub(); - //console.error(`Reply timeout: event ${event.id} `); - reject({ error: `reply timeout: event ${event.id}`, code: "INTERNAL" }); - } - - let replyTimeoutCheck = setTimeout(replyTimeout, 60000); - - sub.on("event", async (event) => { - //console.log(`Received reply event: `, event); - clearTimeout(replyTimeoutCheck); - sub.unsub(); - const decryptedContent = await this.decrypt( + return new Promise((resolve, reject) => { + (async () => { + const command = { + method: nip47Method, + params, + }; + const encryptedCommand = await this.encrypt( this.walletPubkey, - event.content, + JSON.stringify(command), ); - let response; - try { - response = JSON.parse(decryptedContent); - } catch (e) { - reject({ error: "invalid response", code: "INTERNAL" }); - return; + const unsignedEvent: UnsignedEvent = { + kind: 23194 as Kind, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", this.walletPubkey]], + content: encryptedCommand, + pubkey: this.publicKey, + }; + + const event = await this.signEvent(unsignedEvent); + // subscribe to NIP_47_SUCCESS_RESPONSE_KIND and NIP_47_ERROR_RESPONSE_KIND + // that reference the request event (NIP_47_REQUEST_KIND) + const sub = this.relay.sub([ + { + kinds: [23195], + authors: [this.walletPubkey], + "#e": [event.id], + }, + ]); + + function replyTimeout() { + sub.unsub(); + //console.error(`Reply timeout: event ${event.id} `); + reject({ + error: `reply timeout: event ${event.id}`, + code: "INTERNAL", + }); } - // @ts-ignore // event is still unknown in nostr-tools - if (event.kind == 23195 && response.result) { - if (resultValidator(response.result)) { - resolve(resultMapper(response.result)); - this.notify(weblnRequestMethod, response.result); + + const replyTimeoutCheck = setTimeout(replyTimeout, 60000); + + sub.on("event", async (event) => { + //console.log(`Received reply event: `, event); + clearTimeout(replyTimeoutCheck); + sub.unsub(); + const decryptedContent = await this.decrypt( + this.walletPubkey, + event.content, + ); + let response; + try { + response = JSON.parse(decryptedContent); + } catch (e) { + reject({ error: "invalid response", code: "INTERNAL" }); + return; + } + // @ts-ignore // event is still unknown in nostr-tools + if (event.kind == 23195 && response.result) { + //console.log("NIP-47 result", response.result); + if (resultValidator(response.result)) { + resolve(resultMapper(response.result)); + this.notify(weblnMethod, response.result); + } else { + reject({ + error: + "Response from NWC failed validation: " + + JSON.stringify(response.result), + code: "INTERNAL", + }); + } } else { reject({ - error: - "Response from NWC failed validation: " + - JSON.stringify(response.result), - code: "INTERNAL", + error: response.error?.message, + code: response.error?.code, }); } - } else { - reject({ - error: response.error?.message, - code: response.error?.code, - }); + }); + + const pub = this.relay.publish(event); + + function publishTimeout() { + //console.error(`Publish timeout: event ${event.id}`); + reject({ error: `Publish timeout: event ${event.id}` }); } - }); - - let pub = this.relay.publish(event); - - function publishTimeout() { - //console.error(`Publish timeout: event ${event.id}`); - reject({ error: `Publish timeout: event ${event.id}` }); - } - let publishTimeoutCheck = setTimeout(publishTimeout, 5000); - - pub.on("failed", (reason: unknown) => { - //console.debug(`failed to publish to ${this.relay.url}: ${reason}`) - clearTimeout(publishTimeoutCheck); - reject({ error: `Failed to publish request: ${reason}` }); - }); - - pub.on("ok", () => { - //console.debug(`Event ${event.id} for ${invoice} published`); - clearTimeout(publishTimeoutCheck); - }); + const publishTimeoutCheck = setTimeout(publishTimeout, 5000); + + pub.on("failed", (reason: unknown) => { + //console.debug(`failed to publish to ${this.relay.url}: ${reason}`) + clearTimeout(publishTimeoutCheck); + reject({ error: `Failed to publish request: ${reason}` }); + }); + + pub.on("ok", () => { + //console.debug(`Event ${event.id} for ${invoice} published`); + clearTimeout(publishTimeoutCheck); + }); + })(); }); } } diff --git a/yarn.lock b/yarn.lock index 53eb6fe..2a333a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2119,10 +2119,10 @@ "@typescript-eslint/types" "6.4.0" eslint-visitor-keys "^3.4.1" -"@webbtc/webln-types@^1.0.11": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@webbtc/webln-types/-/webln-types-1.0.11.tgz#9b925725bfc6cc5ac7dce6dd6741652feb60dd76" - integrity sha512-IxIV3XVVS/888+ipnQ6XvVW4W5RFD1Dw3cQe8PWlEn9BT7U6TTIFXxS6ZidRc2i0vQlixJLe1jDD/BARbjywyQ== +"@webbtc/webln-types@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@webbtc/webln-types/-/webln-types-2.0.0.tgz#d0fae5d2bf253a77dccbd37b2c9752292b798686" + integrity sha512-tLYbKHDHUugArcw1nyAOelpMhno4FrCAcHiNm6Fzt4c9EqVbGpP9HSE9v4tSiWP+5oExecZ9cdWCAarDL/qoLw== JSONStream@^1.0.3, JSONStream@^1.3.5: version "1.3.5" From fc60a84e68486b88f071e1120a2a0fbcc9ea6455 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 22 Aug 2023 14:34:04 +0700 Subject: [PATCH 3/4] chore: add nip-47 to webln request map --- src/webln/NostrWeblnProvider.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index 62fa62a..ef07445 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -20,7 +20,6 @@ import { SignMessageResponse, WebLNNode, WebLNProvider, - WebLNMethod, WebLNRequestMethod, LookupInvoiceArgs, LookupInvoiceResponse, @@ -49,6 +48,13 @@ type Nip07Provider = { signEvent(event: UnsignedEvent): Promise; }; +const nip47ToWeblnRequestMap = { + get_balance: "getBalance", + make_invoice: "makeInvoice", + pay_invoice: "sendPayment", + lookup_invoice: "lookupInvoice", +}; + export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { relay: Relay; relayUrl: string; @@ -228,7 +234,6 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { return this.executeNip47Request( "get_balance", - "getBalance", undefined, (result) => result.balance !== undefined, (result) => result, @@ -240,7 +245,6 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { return this.executeNip47Request( "pay_invoice", - "sendPayment", { invoice, }, @@ -274,7 +278,6 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { return this.executeNip47Request( "make_invoice", - "makeInvoice", { amount, description: requestInvoiceArgs?.defaultMemo, @@ -293,7 +296,6 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { { invoice: string; paid: boolean } >( "lookup_invoice", - "lookupInvoice", args, (result) => result.invoice !== undefined && result.paid !== undefined, (result) => ({ paymentRequest: result.invoice, paid: result.paid }), @@ -405,12 +407,12 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { } private executeNip47Request( - nip47Method: string, - weblnMethod: WebLNMethod, + nip47Method: keyof typeof nip47ToWeblnRequestMap, params: unknown, resultValidator: (result: R) => boolean, resultMapper: (result: R) => T, ) { + const weblnMethod = nip47ToWeblnRequestMap[nip47Method]; return new Promise((resolve, reject) => { (async () => { const command = { From e54254f0fe71d6e787c841b48a3fb65270cc46f6 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 29 Aug 2023 14:38:25 +0700 Subject: [PATCH 4/4] chore: minor logging changes --- examples/nwc/lookup-invoice.js | 5 +++-- examples/nwc/make-invoice.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/nwc/lookup-invoice.js b/examples/nwc/lookup-invoice.js index fe84967..75962b9 100644 --- a/examples/nwc/lookup-invoice.js +++ b/examples/nwc/lookup-invoice.js @@ -21,9 +21,10 @@ await webln.enable(); const response = await webln.lookupInvoice({ // provide one of the below //invoice: 'lnbc10n1pjwxschpp5hg0pw234n9ww9q4uy25pnvu8y4jzpznysasyf7m9fka36t7fahysdqufet5xgzhv43ycn3qv4uxzmtsd3jscqzzsxqyz5vqsp5uw023qhxuxqfj69rvj9yns5gufczad5gqw4uer5cgqhw90slkavs9qyyssqvv2tw6c30ssgtpejc3zk7ns0svuj8547d8wxj0e36hltljx5a8x4qj59mk2y7qlt6qazf2j38fzc8uag3887nslxz6fe3vnyvg0f2xqqnlvcu2', - payment_hash: 'ba1e172a35995ce282bc22a819b3872564208a64876044fb654dbb1d2fc9edc9' + payment_hash: + "ba1e172a35995ce282bc22a819b3872564208a64876044fb654dbb1d2fc9edc9", }); -console.log(response); +console.info(response); webln.close(); diff --git a/examples/nwc/make-invoice.js b/examples/nwc/make-invoice.js index 9d58949..61f9017 100644 --- a/examples/nwc/make-invoice.js +++ b/examples/nwc/make-invoice.js @@ -19,10 +19,10 @@ const webln = new providers.NostrWebLNProvider({ }); await webln.enable(); const response = await webln.makeInvoice({ - amount: 100, + amount: 1, // in sats defaultMemo: "NWC WebLN example", }); -console.log(response); +console.info(response); webln.close();