diff --git a/docs/lnclient.md b/docs/lnclient.md index 7bb6c1f..3596988 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -18,8 +18,11 @@ Or to request a payment to be received: ```js const request = await new LN(credentials).receive(USD(1.0)); -// give request.invoice to someone... -request.onPaid(giveAccess); + +// give request.invoice to someone, then act upon it: +request + .onPaid(giveAccess) // listen for incoming payment and then fire the given method + .onTimeout(60, showTimeout); // if they didn't pay within 60 seconds, do something else ``` ## Examples diff --git a/examples/lnclient/pay_ln_address.js b/examples/lnclient/pay_ln_address.js index 8dda60e..9282969 100644 --- a/examples/lnclient/pay_ln_address.js +++ b/examples/lnclient/pay_ln_address.js @@ -16,4 +16,4 @@ const client = new LN(nwcUrl); console.log("Paying $1"); const response = await client.pay("rolznz@getalby.com", USD(1.0)); console.info("Paid successfully", response); -client.close(); +client.close(); // when done and no longer needed close the wallet connection diff --git a/examples/lnclient/paywall.js b/examples/lnclient/paywall.js index a94ec60..51bc7e3 100644 --- a/examples/lnclient/paywall.js +++ b/examples/lnclient/paywall.js @@ -14,22 +14,26 @@ const nwcUrl = rl.close(); const client = new LN(nwcUrl); -const request = await client.receive(USD(1.0)); +// request a lightning invoice that we show the user to pay +const request = await client.receive(USD(1.0), { description: "best content" }); qrcode.generate(request.invoice.paymentRequest, { small: true }); console.info(request.invoice.paymentRequest); +console.info("Please pay the above invoice within 60 seconds."); console.info("Waiting for payment..."); -const unsub = await request.onPaid(() => { - console.info("received payment!"); - client.close(); -}); +// once the invoice got paid by the user run this callback +request + .onPaid(() => { + console.info("received payment!"); + client.close(); // when done and no longer needed close the wallet connection + }) + .onTimeout(60, () => { + console.info("didn't receive payment in time."); + client.close(); // when done and no longer needed close the wallet connection + }); process.on("SIGINT", function () { console.info("Caught interrupt signal"); - - unsub(); - client.close(); - process.exit(); }); diff --git a/examples/lnclient/splitter.js b/examples/lnclient/splitter.js index 6bb3110..2c33cbf 100644 --- a/examples/lnclient/splitter.js +++ b/examples/lnclient/splitter.js @@ -25,36 +25,38 @@ const request = await client.receive(amount, { description: "prism payment" }); // prompt the user to pay the invoice qrcode.generate(request.invoice.paymentRequest, { small: true }); console.info(request.invoice.paymentRequest); +console.info("Please pay the above invoice within 60 seconds."); console.info("Waiting for payment..."); // once the invoice got paid by the user run this callback -const unsub = await request.onPaid(async () => { - // we take the sats amount from the invoice and calculate the amount we want to forward - const satsReceived = request.invoice.satoshi; - const satsToForward = Math.floor( - (satsReceived * forwardPercentage) / 100 / recipients.length, - ); - console.info( - `Received ${satsReceived} sats! Forwarding ${satsToForward} to the ${recipients.join(", ")}`, - ); - - // iterate over all recipients and pay them the amount - await Promise.all( - recipients.map(async (r) => { - const response = await client.pay(r, satsToForward, "splitter"); - console.info( - `Forwarded ${satsToForward} sats to ${r} (preimage: ${response.preimage})`, - ); - }), - ); - client.close(); -}); +request + .onPaid(async () => { + // we take the sats amount from theinvocie and calculate the amount we want to forward + const satsReceived = request.invoice.satoshi; + const satsToForward = Math.floor( + (satsReceived * forwardPercentage) / 100 / recipients.length, + ); + console.info( + `Received ${satsReceived} sats! Forwarding ${satsToForward} to the ${recipients.join(", ")}`, + ); + + // iterate over all recipients and pay them the amount + await Promise.all( + recipients.map(async (r) => { + const response = await client.pay(r, satsToForward, "splitter"); + console.info( + `Forwarded ${satsToForward} sats to ${r} (preimage: ${response.preimage})`, + ); + }), + ); + client.close(); // when done and no longer needed close the wallet connection + }) + .onTimeout(60, () => { + console.info("didn't receive payment in time."); + client.close(); // when done and no longer needed close the wallet connection + }); process.on("SIGINT", function () { console.info("Caught interrupt signal"); - - unsub(); - client.close(); - process.exit(); }); diff --git a/src/lnclient/ReceiveInvoice.ts b/src/lnclient/ReceiveInvoice.ts index 7959ad1..1b33d8b 100644 --- a/src/lnclient/ReceiveInvoice.ts +++ b/src/lnclient/ReceiveInvoice.ts @@ -1,10 +1,17 @@ import { Invoice } from "@getalby/lightning-tools"; import { Nip47Notification, Nip47Transaction, NWCClient } from "../nwc"; +/** + * A lightning invoice to be received by your wallet, along with utility functions, + * such as checking if the invoice was paid and acting upon it. + */ export class ReceiveInvoice { readonly transaction: Nip47Transaction; readonly invoice: Invoice; private _nwcClient: NWCClient; + private _unsubscribeFunc?: () => void; + private _timeoutFunc?: () => void; + private _timeoutId?: number | NodeJS.Timeout; constructor(nwcClient: NWCClient, transaction: Nip47Transaction) { this.transaction = transaction; @@ -12,53 +19,117 @@ export class ReceiveInvoice { this._nwcClient = nwcClient; } - async onPaid(callback: (receivedPayment: Nip47Transaction) => void) { - // TODO: is there a better way than calling getInfo here? - const info = await this._nwcClient.getInfo(); + /** + * Setup an action once the invoice has been paid. + * + * @param callback this method will be fired once we register the invoice was paid, with information of the received payment. + * @returns the current instance for method chaining e.g. add optional timeout + */ + onPaid( + callback: (receivedPayment: Nip47Transaction) => void, + ): ReceiveInvoice { + (async () => { + let supportsNotifications; + try { + // TODO: is there a better way than calling getInfo here? + const info = await this._nwcClient.getInfo(); + supportsNotifications = + info.notifications?.includes("payment_received"); + } catch (error) { + console.error("failed to fetch info, falling back to polling"); + } + + const callbackWrapper = (receivedPayment: Nip47Transaction) => { + this._unsubscribeFunc?.(); + callback(receivedPayment); + }; - if (!info.notifications?.includes("payment_received")) { - console.warn( - "current connection does not support notifications, falling back to polling", - ); - return this._onPaidPollingFallback(callback); - } + const unsubscribeWrapper = (unsubscribe: () => void) => { + return () => { + // cancel the timeout method and + this._timeoutFunc = undefined; + clearTimeout(this._timeoutId); + unsubscribe(); + }; + }; - let unsubscribeFunc = () => {}; - const onNotification = (notification: Nip47Notification) => { - if ( - notification.notification.payment_hash === this.transaction.payment_hash - ) { - unsubscribeFunc(); - callback(notification.notification); + if (!supportsNotifications) { + console.warn( + "current connection does not support notifications, falling back to polling", + ); + this._unsubscribeFunc = unsubscribeWrapper( + this._onPaidPollingFallback(callbackWrapper), + ); + } else { + const onNotification = (notification: Nip47Notification) => { + if ( + notification.notification.payment_hash === + this.transaction.payment_hash + ) { + callbackWrapper(notification.notification); + } + }; + + this._unsubscribeFunc = unsubscribeWrapper( + await this._nwcClient.subscribeNotifications(onNotification, [ + "payment_received", + ]), + ); } + })(); + + return this; + } + + /** + * Setup an action that happens if the invoice is not paid after a certain amount of time. + * + * @param seconds the number of seconds to wait for a payment + * @param callback this method will be called once the timeout is elapsed. + * @returns the current instance for method + */ + onTimeout(seconds: number, callback: () => void): ReceiveInvoice { + this._timeoutFunc = () => { + this._unsubscribeFunc?.(); + callback(); }; + this._timeoutId = setTimeout(() => { + this._timeoutFunc?.(); + }, seconds * 1000); - unsubscribeFunc = await this._nwcClient.subscribeNotifications( - onNotification, - ["payment_received"], - ); - return unsubscribeFunc; + return this; + } + + /** + * Manually unsubscribe if you no longer expect the user to pay. + * + * This is only needed if no payment was received and no timeout was configured. + */ + unsubscribe() { + this._unsubscribeFunc?.(); } - private async _onPaidPollingFallback( + private _onPaidPollingFallback( callback: (receivedPayment: Nip47Transaction) => void, ) { let subscribed = true; const unsubscribeFunc = () => { subscribed = false; }; - while (subscribed) { - const transaction = await this._nwcClient.lookupInvoice({ - payment_hash: this.transaction.payment_hash, - }); - if (transaction.settled_at && transaction.preimage) { - callback(transaction); - subscribed = false; - break; + (async () => { + while (subscribed) { + const transaction = await this._nwcClient.lookupInvoice({ + payment_hash: this.transaction.payment_hash, + }); + if (transaction.settled_at && transaction.preimage) { + callback(transaction); + subscribed = false; + break; + } + // sleep for 3 seconds per lookup attempt + await new Promise((resolve) => setTimeout(resolve, 3000)); } - // sleep for 3 seconds per lookup attempt - await new Promise((resolve) => setTimeout(resolve, 3000)); - } + })(); return unsubscribeFunc; }