paid-services
Version:
Lightning Paid Services library
304 lines (265 loc) • 9.32 kB
JavaScript
const {address} = require('bitcoinjs-lib');
const asyncAuto = require('async/auto');
const {createChainAddress} = require('ln-service');
const {getIdentity} = require('ln-service');
const {acceptsChannelOpen} = require('ln-sync');
const {getNetwork} = require('ln-sync');
const {networks} = require('bitcoinjs-lib');
const {returnResult} = require('asyncjs-util');
const proposeChannelOpen = require('./propose_channel_open');
const askForChanSize = max => `Capacity of the new channel? (max: ${max})`;
const askForSendSize = max => `Amount you want to send out? (max: ${max})`;
const bufferAsHex = buffer => buffer.toString('hex');
const dust = 550;
const {isInteger} = Number;
const isNumber = n => !isNaN(n);
const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
const minChannelCapacity = 20000;
const minSpendTokens = 0;
const openChannelAction = 'open_channel';
const spendToExternalAddressAction = 'external_spend_funds';
const spendToInternalAddressAction = 'internal_spend_funds';
const {toOutputScript} = address;
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
/** Ask for details about a decrease
{
ask: <Ask Function>
lnd: <Authenticated LND API Object>
max: <Maximum Possible Decrease Number>
}
@returns via cbk or Promise
{
[address]: <Send Funds to Address String>
is_final: <Decrease is Final Decrease Bool>
[node]: <Create Channel with Node With Public Key Hex String>
[output]: <Output Script Hex String>
tokens: <Withdraw Tokens Number>
}
*/
module.exports = ({ask, lnd, max}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!ask) {
return cbk([400, 'ExpectedAskFunctionToAskForDecrease']);
}
if (!lnd) {
return cbk([400, 'ExpectedAuthenticatedLndToAskForDecrease']);
}
if (!max) {
return cbk([400, 'ExpectedMaximumAvaialbleTok'])
}
return cbk();
},
// Select the type of decrease
askForDecreaseType: ['validate', ({}, cbk) => {
return ask({
choices: [
{
name: `Send decreased funds to the internal chain wallet`,
value: spendToInternalAddressAction,
},
{
name: `Send decreased funds to an external chain address`,
value: spendToExternalAddressAction,
},
{
disabled: max < minChannelCapacity,
name: `Spend decreased funds into new channel with another peer`,
value: openChannelAction,
},
],
message: 'How do you want to change the channel capacity?',
name: 'decrease',
type: 'list',
},
({decrease}) => cbk(null, decrease));
}],
// Get the node identity key to make sure a channel open isn't with self
getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
// Get network name to validate addresses against
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
// Ask for the public key of the node to trade with
askForNodeId: [
'askForDecreaseType',
'getIdentity',
({askForDecreaseType, getIdentity}, cbk) =>
{
// Exit early when not opening a new channel
if (askForDecreaseType !== openChannelAction) {
return cbk();
}
return ask({
name: 'key',
message: 'Public key of node to open channel with?',
type: 'input',
validate: input => {
if (!input) {
return false;
}
if (!isPublicKey(input)) {
return 'Expected public key of node to open channel with';
}
if (input === getIdentity.public_key) {
return 'Expected public key of other node';
}
return true;
},
},
({key}) => cbk(null, key));
}],
// Create a decrease address to withdraw funds out to
createAddress: ['askForDecreaseType', ({askForDecreaseType}, cbk) => {
// Exit early when there is no need to create an internal address
if (askForDecreaseType !== spendToInternalAddressAction) {
return cbk();
}
return createChainAddress({lnd}, cbk);
}],
// Ask for the amount to decrease
askForAmount: ['askForNodeId', ({askForNodeId}, cbk) => {
return ask({
default: !askForNodeId ? minSpendTokens : minChannelCapacity,
message: !askForNodeId ? askForSendSize(max) : askForChanSize(max),
name: 'amount',
validate: input => {
// A numeric input is required
if (!isNumber(input) || !isInteger(Number(input))) {
return false;
}
// Zero value is acceptable for a decrease when not opening channel
if (!askForNodeId && !Number(input)) {
return true;
}
// On-chain values must always be above dust
if (!!Number(input) && Number(input) < dust) {
return false;
}
// Channel opens must always be above the minimum channel size
if (!!askForNodeId && Number(input) < minChannelCapacity) {
return `The minimum channel capacity is ${minChannelCapacity}`;
}
// A decrease cannot spend more funds than are available
if (!!Number(input) && Number(input) > max) {
return `The maximum possible to decrease is ${max}`;
}
return true;
},
},
({amount}) => cbk(null, Number(amount)));
}],
// Ask for an external address
askForAddress: [
'askForAmount',
'askForDecreaseType',
'getNetwork',
({askForAmount, askForDecreaseType, getNetwork}, cbk) =>
{
// Exit early when the withdraw address is not external
if (askForDecreaseType !== spendToExternalAddressAction) {
return cbk();
}
return ask({
name: 'address',
message: `Address to spend ${tokensAsBigUnit(askForAmount)} to?`,
type: 'input',
validate: input => {
if (!input) {
return false;
}
// Make sure that the entered address can be derived to an output
try {
toOutputScript(input, networks[getNetwork.bitcoinjs]);
} catch (err) {
return 'Failed to parse address. Try a standard one?';
}
return true;
},
},
({address}) => cbk(null, {address}));
}],
// Ask if this is the last output
askForAddition: [
'askForAddress',
'askForAmount',
'askForDecreaseType',
({askForAmount, askForDecreaseType}, cbk) =>
{
// Exit early and only ask for addition when decrease is a "spend" type
if (askForDecreaseType === spendToInternalAddressAction) {
return cbk();
}
// Exit early when there are no more funds to spend
if (max - askForAmount < dust) {
return cbk();
}
return ask({
default: false,
name: 'add',
message: 'Spend additional funds from the channel capacity?',
type: 'confirm',
},
({add}) => cbk(null, add));
}],
// Propose a channel open to confirm the peer will accept the channel
checkChannelAcceptance: [
'askForAmount',
'askForNodeId',
({askForAmount, askForNodeId}, cbk) =>
{
// Exit early if pubkey was not entered
if (!askForNodeId) {
return cbk();
}
return proposeChannelOpen({
lnd,
capacity: askForAmount,
id: askForNodeId,
},
cbk);
}],
// Final decrease output details
decrease: [
'askForAddition',
'askForAddress',
'askForAmount',
'checkChannelAcceptance',
'createAddress',
'getNetwork',
({
askForAddition,
askForAddress,
askForAmount,
askForNodeId,
checkChannelAcceptance,
createAddress,
getNetwork,
},
cbk) =>
{
// Make sure that a channel proposal didn't fail
if (!!checkChannelAcceptance && !checkChannelAcceptance.is_accepted) {
return cbk([503, 'RemoteNodeRejectedNewChannelProposal']);
}
// Exit early when decreasing to a new channel
if (!!askForNodeId) {
return cbk(null, {
is_final: !askForAddition,
node: askForNodeId,
tokens: askForAmount,
});
}
const {address} = (createAddress || askForAddress);
const output = toOutputScript(address, networks[getNetwork.bitcoinjs]);
return cbk(null, {
address,
is_final: !askForAddition,
output: bufferAsHex(output),
tokens: askForAmount,
});
}],
},
returnResult({reject, resolve, of: 'decrease'}, cbk));
});
};