paid-services
Version:
Lightning Paid Services library
184 lines (157 loc) • 5.32 kB
JavaScript
const {randomBytes} = require('crypto');
const asyncAuto = require('async/auto');
const {createInvoice} = require('ln-service');
const {payViaRoutes} = require('ln-service');
const {probeForRoute} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {invoiceNetwork} = require('./../config');
const messagesAsResponse = require('./messages_as_response');
const messagesForRequest = require('./messages_for_request');
const waitForResponse = require('./wait_for_response');
const defaultCltvDelta = 144;
const defaultMaxFeeMtokens = '9000';
const defaultRequestMtokens = '10000';
const findResponse = messages => messages.find(n => n.type === '805805');
const makeSecret = () => randomBytes(32).toString('hex');
const maxPathfindingTimeMs = 1000 * 60 * 5;
const waitForResponseMs = 1000 * 60 * 10;
/** Make a service request
{
[arguments]: <TLV Encoded Hex String>
id: <Service Id Number String>
lnd: <Authenticated LND API Object>
network: <Network Name String>
node: <Node Public Key Hex String>
[secret]: <Push Preimage Hex String>
}
@returns via cbk or Promise
{
[invoice]: <BOLT 11 Payment Request String>
[links]: [<URL String>]
[nodes]: [<Node Public Key Hex String>]
[records]: [{
type: <Type Number String>
value: <Value Hex String>
}]
[text]: <Response Message String>
}
*/
module.exports = ({arguments, id, lnd, network, node, secret}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!id) {
return cbk([400, 'ExpectedServiceIdNumberToMakeServiceRequest']);
}
if (!lnd) {
return cbk([400, 'ExpectedAuthenticatedLndToMakeServiceRequest']);
}
if (!network) {
return cbk([400, 'ExpectedNetworkNameToMakeServiceRequest']);
}
if (!node) {
return cbk([400, 'ExpectedNodePublicKeyToMakeServiceRequest']);
}
return cbk();
},
// Create a reply invoice that the service will pay back to
createReply: ['validate', ({}, cbk) => createInvoice({lnd}, cbk)],
// Derive messages for the service request
requestMessages: ['createReply', ({createReply}, cbk) => {
try {
return cbk(null, messagesForRequest({
arguments,
reply: createReply.request,
secret: secret || makeSecret(),
service: id,
}));
} catch (err) {
return cbk([400, 'FailedToEncodeMessagesForRequest', {err}]);
}
}],
// Listen for a reply to the invoice
response: ['createReply', ({createReply}, cbk) => {
return waitForResponse({
lnd,
id: createReply.id,
ms: waitForResponseMs,
},
(error, value) => {
return cbk(null, {error, value});
});
}],
// Find a route to make a payment
findRoute: ['requestMessages', ({requestMessages}, cbk) => {
return probeForRoute({
lnd,
cltv_delta: defaultCltvDelta,
destination: node,
max_fee_mtokens: defaultMaxFeeMtokens,
messages: requestMessages.messages,
mtokens: defaultRequestMtokens,
probe_timeout_ms: maxPathfindingTimeMs,
},
cbk);
}],
// Make the push payment
push: [
'findRoute',
'requestMessages',
({findRoute, requestMessages}, cbk) =>
{
// Exit early when not using find route
if (!findRoute) {
return cbk();
}
if (!findRoute.route) {
return cbk([503, 'FailedToFindRouteToDestination']);
}
return payViaRoutes({
lnd,
id: requestMessages.id,
routes: [findRoute.route],
},
cbk);
}],
// Decode the server response
decode: ['response', 'push', ({push, response}, cbk) => {
// Exit early when there was an error with the server response
if (!!response.error) {
const [code, message] = response.error;
return cbk([code, message, {paid: push.tokens}]);
}
const {payments} = response.value;
// One of the HTLCs will have the response record encoded
const payment = payments.find(n => findResponse(n.messages));
if (!payment) {
return cbk([503, 'FailedToReceivePaidServiceResponseFromServer']);
}
const {messages} = payment;
try {
return cbk(null, messagesAsResponse({
messages,
network: invoiceNetwork[network],
}));
} catch (err) {
return cbk([503, 'UnexpectedErrorParsingServiceResponse', {err}]);
}
}],
// Final response from the server
service: ['decode', ({decode}, cbk) => {
// Exit early when the service returned an error
if (!!decode.error) {
return cbk(decode.error);
}
return cbk(null, {
invoice: decode.paywall,
links: decode.links,
nodes: decode.nodes,
records: decode.records,
text: decode.text,
});
}],
},
returnResult({reject, resolve, of: 'service'}, cbk));
});
};