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..4a0a1b5 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; @@ -168,91 +175,27 @@ 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" } } - sendPayment(invoice: string) { + // TODO: refactor code in getBalance and sendPayment + getBalance() { 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); + // FIXME: add getBalance to webln-types + return this.executeNip47Request("get_balance", "getBalance" as RequestMethod, undefined, result => result.balance !== undefined, result => result); + } - pub.on('failed', (reason: unknown) => { - //console.debug(`failed to publish to ${this.relay.url}: ${reason}`) - clearTimeout(publishTimeoutCheck) - reject({ error: `Failed to publish request: ${reason}` }); - }); + sendPayment(invoice: string) { + this.checkConnected(); - 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 @@ -355,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;