From 643f765982d14e2e34c69aca7340e8dc477e2fa1 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Tue, 25 Jul 2023 18:41:37 +0530 Subject: [PATCH 1/2] feat: add getbalance to nwc provider --- examples/nostr-wallet-connect.js | 6 ++- src/webln/NostrWeblnProvider.ts | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/examples/nostr-wallet-connect.js b/examples/nostr-wallet-connect.js index eb9ac84..6d84adb 100644 --- a/examples/nostr-wallet-connect.js +++ b/examples/nostr-wallet-connect.js @@ -15,8 +15,10 @@ rl.close(); const webln = new providers.NostrWebLNProvider({ nostrWalletConnectUrl: nwcUrl }); await webln.enable(); -const response = await webln.sendPayment(invoice); +const sendPaymentResponse = await webln.sendPayment(invoice); +console.log(sendPaymentResponse); -console.log(response); +const getBalanceResponse = await webln.getBalance(); +console.log(getBalanceResponse); webln.close(); \ No newline at end of file diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index 311adf7..299ed76 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -23,6 +23,13 @@ const NWCs: Record = { } }; +// TODO: fetch this from @webbtc/webln-types +interface GetBalanceResponse { + balance: number; + max_amount?: number; + budget_renewal?: string; +} + interface NostrWebLNOptions { authorizationUrl?: string; // the URL to the NWC interface for the user to confirm the session relayUrl: string; @@ -175,6 +182,84 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { } } + // TODO: refactor code in getBalance and sendPayment + getBalance() { + this.checkConnected(); + + return new Promise(async (resolve, reject) => { + const command = { + "method": "get_balance" + }; + 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(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?.balance) { + resolve(response.result); + this.notify('getBalance', response.result); + } else { + reject({ error: response.error?.message, code: response.error?.code }); + } + }); + + 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); + }); + }); + } + sendPayment(invoice: string) { this.checkConnected(); From 708230b489620c3dd2876b0695443c38c7304f13 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 9 Aug 2023 18:39:13 +0700 Subject: [PATCH 2/2] chore: reduce duplication --- src/webln/NostrWeblnProvider.ts | 235 ++++++++++++-------------------- 1 file changed, 87 insertions(+), 148 deletions(-) diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index 299ed76..4a0a1b5 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -175,7 +175,7 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { // TODO: use NIP-47 get_info call async getInfo(): Promise { return { - methods: ["getInfo", "sendPayment"], + methods: ["getInfo", "sendPayment", "getBalance"], node: {} as WebLNNode, supports: ["lightning"], version: "NWC" @@ -186,158 +186,16 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { getBalance() { this.checkConnected(); - return new Promise(async (resolve, reject) => { - const command = { - "method": "get_balance" - }; - 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(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?.balance) { - resolve(response.result); - this.notify('getBalance', response.result); - } else { - reject({ error: response.error?.message, code: response.error?.code }); - } - }); - - 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); - }); - }); + // FIXME: add getBalance to webln-types + return this.executeNip47Request("get_balance", "getBalance" as RequestMethod, undefined, result => result.balance !== undefined, result => result); } sendPayment(invoice: string) { this.checkConnected(); - return new Promise(async (resolve, reject) => { - const command = { - "method": "pay_invoice", - "params": { - "invoice": invoice - } - }; - 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(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?.preimage) { - resolve({ preimage: response.result.preimage }); - this.notify('sendPayment', response.result); - } else { - reject({ error: response.error?.message, code: response.error?.code }); - } - }); - - 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); - }); - }); + return this.executeNip47Request("pay_invoice", 'sendPayment', { + invoice + }, result => !!result.preimage, result => ({ preimage: result.preimage })); } // not-yet implemented WebLN interface methods @@ -440,6 +298,87 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider { throw new Error("please call enable() and await the promise before calling this function") } } + + private executeNip47Request(method: string, weblnRequestMethod: RequestMethod, params: any, resultValidator: (result: any) => boolean, resultMapper: (result: any) => any) { + 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(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) { + if (resultValidator(response.result)) { + resolve(resultMapper(response.result)); + this.notify(weblnRequestMethod, response.result); + } + else { + reject({ error: "Response from NWC failed validation: " + JSON.stringify(response.result), code: "INTERNAL" }); + } + } else { + reject({ error: response.error?.message, code: response.error?.code }); + } + }); + + 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); + }); + }); + } } export const NWC = NostrWebLNProvider;