Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/lnclient.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/lnclient/pay_ln_address.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ const client = new LN(nwcUrl);
console.log("Paying $1");
const response = await client.pay("[email protected]", USD(1.0));
console.info("Paid successfully", response);
client.close();
client.close(); // when done and no longer needed close the wallet connection
22 changes: 13 additions & 9 deletions examples/lnclient/paywall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
52 changes: 27 additions & 25 deletions examples/lnclient/splitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
137 changes: 104 additions & 33 deletions src/lnclient/ReceiveInvoice.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,135 @@
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;
this.invoice = new Invoice({ pr: transaction.invoice });
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;
}
Expand Down