balanceofsatoshis
Version:
Lightning balance CLI
969 lines (826 loc) • 31.7 kB
JavaScript
const {randomBytes} = require('crypto');
const {acceptsChannelOpen} = require('ln-sync');
const {addPeer} = require('ln-service');
const {address} = require('bitcoinjs-lib');
const {askForFeeRate} = require('ln-sync');
const asyncAuto = require('async/auto');
const asyncEach = require('async/each');
const asyncEachSeries = require('async/eachSeries');
const asyncDetectSeries = require('async/detectSeries');
const asyncMap = require('async/map');
const asyncMapSeries = require('async/mapSeries');
const asyncReflect = require('async/reflect');
const asyncRetry = require('async/retry');
const {broadcastChainTransaction} = require('ln-service');
const {cancelPendingChannel} = require('ln-service');
const {fundPendingChannels} = require('ln-service');
const {getChainBalance} = require('ln-service');
const {getChannels} = require('ln-service');
const {getFundedTransaction} = require('ln-sync');
const {getNetwork} = require('ln-sync');
const {getNode} = require('ln-service');
const {getPeers} = require('ln-service');
const {getPsbtFromTransaction} = require('goldengate');
const {getWalletVersion} = require('ln-service');
const {openChannels} = require('ln-service');
const {maintainUtxoLocks} = require('ln-sync');
const moment = require('moment');
const {parseAmount} = require('ln-accounting');
const {returnResult} = require('asyncjs-util');
const {Transaction} = require('bitcoinjs-lib');
const {unlockUtxo} = require('ln-service');
const adjustFees = require('./../routing/adjust_fees');
const {authenticatedLnd} = require('./../lnd');
const channelsFromArguments = require('./channels_from_arguments');
const {getAddressUtxo} = require('./../chain');
const getChannelOutpoints = require('./get_channel_outpoints');
const getPeersForNodes = require('./get_peers_for_nodes');
const bech32AsData = bech32 => address.fromBech32(bech32).data;
const description = 'bos open';
const detectNetworks = ['btc', 'btctestnet'];
const featureAnchors = 'anchor_zero_fee_htlc_tx';
const flatten = arr => [].concat(...arr);
const format = 'p2wpkh';
const {fromHex} = Transaction;
const interval = 1000;
const {isArray} = Array;
const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
const isUnknown = (a1, a2) => a1.findIndex(n => !a2.includes(n)) !== -1;
const knownCommits = ['default', 'simplified_taproot'];
const knownTypes = ['private', 'private-trusted', 'public', 'public-trusted'];
const lineBreak = '\n';
const noInternalFundingVersions = ['0.11.0-beta', '0.11.1-beta'];
const notFound = -1;
const peerAddedDelayMs = 1000 * 5;
const pendingCheckTimes = 60 * 10;
const per = (a, b) => (a / b).toFixed(2);
const relockIntervalMs = 1000 * 20;
const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
const times = 10;
const tokAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
const uniq = arr => Array.from(new Set(arr));
const utxoPollingIntervalMs = 1000 * 30;
const utxoPollingTimes = 20;
/** Open channels with peers
{
ask: <Ask For Input Function>
capacities: [<New Channel Capacity Tokens String>]
commitments: [<Channel Commitment Types String>]
cooperative_close_addresses: [<Cooperative Close Address>]
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
}
gives: [<New Channel Give Tokens Number>]
[is_allowing_minimal_reserve]: <Allow Peers to Have Minimal Reserve Bool>
[is_avoiding_broadcast]: <Avoid Funding Transaction Broadcast Bool>
[is_external]: <Use External Funds to Open Channels Bool>
lnd: <Authenticated LND API Object>
logger: <Winston Logger Object>
opening_nodes: [<Open New Channel With Saved Node Name String>]
public_keys: [<Public Key Hex String>]
request: <Request Function>
set_fee_rates: [<Fee Rate Number>]
[skip_anchors_check]: <Skip Check For Anchor Channel Support Bool>
types: [<Channel Type String>]
}
@returns via cbk or Promise
{
transaction_id: <Open Channels Transaction Id Hex String>
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.ask) {
return cbk([400, 'ExpectedAskMethodToOpenChannels']);
}
if (!isArray(args.capacities)) {
return cbk([400, 'ExpectedChannelCapacitiesToOpenChannels']);
}
if (!isArray(args.commitments)) {
return cbk([400, 'ExpectedArrayOfChannelOutputTypesToOpenChannels']);
}
if (!isArray(args.cooperative_close_addresses)) {
return cbk([400, 'ExpectedCooperativeCloseAddressesArray']);
}
if (!isArray(args.gives)) {
return cbk([400, 'ExpectedArrayOfGivesToOpenChannels']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedLndToInitiateOpenChannelRequests']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToInitiateOpenChannelRequests']);
}
if (!isArray(args.opening_nodes)) {
return cbk([400, 'ExpectedOpeningNodesArrayToInitiateOpenChannels']);
}
if (!isArray(args.public_keys)) {
return cbk([400, 'ExpectedPublicKeysToOpenChannels']);
}
if (!!args.public_keys.filter(n => !isPublicKey(n)).length) {
return cbk([400, 'NodesToOpenWithMustBeSpecifiedWithPublicKeyOnly']);
}
const closeAddrCount = args.cooperative_close_addresses.length;
const hasCapacities = !!args.capacities.length;
const hasCommitments = !!args.commitments.length;
const hasGives = !!args.gives.length;
const hasFeeRates = !!args.set_fee_rates.length;
const hasNodes = !!args.opening_nodes.length;
const publicKeysLength = args.public_keys.length;
if (!!hasCapacities && publicKeysLength !== args.capacities.length) {
return cbk([400, 'CapacitiesMustBeSpecifiedForEveryPublicKey']);
}
if (!!hasCommitments && publicKeysLength !== args.commitments.length) {
return cbk([400, 'CommitmentTypesMustBeSpecifiedForEveryPublicKey']);
}
if (!!closeAddrCount && publicKeysLength !== closeAddrCount) {
return cbk([400, 'MustSetCoopClosingAddressForEveryPublicKey']);
}
if (!!hasGives && publicKeysLength !== args.gives.length) {
return cbk([400, 'GivesMustBeSpecifiedForEveryPublicKey']);
}
if (!!hasFeeRates && publicKeysLength !== args.set_fee_rates.length) {
return cbk([400, 'MustSetFeeRateForEveryPublicKey']);
}
if (!!hasNodes && publicKeysLength !== args.opening_nodes.length) {
return cbk([400, 'MustSetOpeningNodeForEveryPublicKey']);
}
if (!!args.is_external && !!args.internal_fund_fee_rate) {
return cbk([400, 'CannotUseBothInternalAndExternalFundsForOpen']);
}
if (!args.request) {
return cbk([400, 'ExpectedRequestFunctionToOpenChannels']);
}
if (!isArray(args.types)) {
return cbk([400, 'ExpectedArrayOfTypesToOpenChannels']);
}
if (args.types.findIndex(n => !knownTypes.includes(n)) !== notFound) {
return cbk([400, 'UnknownChannelType', {channel_types: knownTypes}]);
}
if (isUnknown(args.commitments, knownCommits)) {
return cbk([400, 'UnknownCommitment', {commitments: knownCommits}]);
}
if (!!args.types.length && args.types.length !== publicKeysLength) {
return cbk([400, 'ChannelTypesMustBeSpecifiedForEveryPublicKey']);
}
return cbk();
},
// Parse capacities
capacities: ['validate', ({}, cbk) => {
const capacities = args.capacities.map(amount => {
try {
return parseAmount({amount}).tokens;
} catch (err) {
return cbk([400, err.message]);
}
});
return cbk(null, capacities);
}],
// Get LNDs associated with nodes specified for opening
getLnds: ['validate', ({}, cbk) => {
// Exit early when there are no opening nodes specified
if (!args.opening_nodes.length) {
return cbk(null, [{lnd: args.lnd}]);
}
return asyncMapSeries(uniq(args.opening_nodes), (node, cbk) => {
return authenticatedLnd({node, logger: args.logger}, (err, res) => {
if (!!err) {
return cbk(err);
}
return cbk(null, {node, lnd: res.lnd});
});
},
cbk);
}],
// Get the default network name
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
// Get sockets in case we need to connect
getNodes: ['validate', ({}, cbk) => {
return asyncMap(uniq(args.public_keys), (key, cbk) => {
return getNode({lnd: args.lnd, public_key: key}, (err, res) => {
// Ignore errors when a node is unknown in the graph
if (!!err) {
return cbk(null, {public_key: key, sockets: []});
}
const peers = res.channels.map(({policies}) => {
return policies.find(n => n.public_key !== key).public_key;
});
const isBig = res.features.find(n => n.type === 'large_channels');
return cbk(null, {
alias: res.alias,
channels_count: res.channels.length,
is_accepting_large_channels: !!isBig || undefined,
peers_count: uniq(peers).length,
public_key: key,
sockets: res.sockets,
});
});
},
cbk);
}],
// Get the wallet version to make sure the node supports internal funding
getWalletVersion: ['validate', ({}, cbk) => {
return getWalletVersion({lnd: args.lnd}, cbk);
}],
// Get the networks of the opening nodes
getOpeningNetworks: ['getLnds', ({getLnds}, cbk) => {
if (!getLnds) {
return cbk();
}
return asyncMap(getLnds, ({lnd}, cbk) => getNetwork({lnd}, cbk), cbk);
}],
// Get the opening parameters to use to open the new channels
opens: ['capacities', ({capacities}, cbk) => {
const {opens} = channelsFromArguments({
capacities,
addresses: args.cooperative_close_addresses,
commitments: args.commitments,
gives: args.gives,
nodes: args.public_keys,
rates: args.set_fee_rates,
saved: args.opening_nodes,
types: args.types,
});
return cbk(null, opens);
}],
// Check if all networks are the same
checkNetworks: [
'getNetwork',
'getOpeningNetworks',
({getNetwork, getOpeningNetworks}, cbk) =>
{
// Exit early when there are no networks to check
if (!getOpeningNetworks) {
return cbk();
}
if (!!getOpeningNetworks.find(n => n.network !== getNetwork.network)) {
return cbk([400, 'AllOpeningNodesMustBeOnSameChain']);
}
return cbk();
}],
// Get connected peers to see if we are already connected
getPeers: ['getLnds', ({getLnds}, cbk) => {
return getPeersForNodes({lnd: args.lnd, nodes: getLnds}, cbk);
}],
// Connect up to the peers
connect: [
'getLnds',
'getNodes',
'getPeers',
'opens',
({getLnds, getNodes, getPeers, opens}, cbk) =>
{
// Collect some details about nodes being connected to
const nodes = getNodes.filter(n => !!n.channels_count).map(node => {
return {
node: `${node.alias || node.public_key}`,
channels_per_peer: `${per(node.channels_count, node.peers_count)}`,
is_accepting_large_channels: node.is_accepting_large_channels,
};
});
args.logger.info(nodes);
// Connect up as peers
return asyncEach(opens, ({node, channels}, cbk) => {
// Summarize who is being opened to
const openingTo = getNodes
.filter(remote => {
return !!channels.find(channel => {
return channel.partner_public_key === remote.public_key;
});
})
.map(remote => {
const {capacity} = channels.find(channel => {
return channel.partner_public_key === remote.public_key;
});
const remoteNamed = remote.alias || remote.public_key;
return `${remoteNamed}: ${tokAsBigUnit(capacity)}`;
});
args.logger.info({node, opening_to: openingTo});
const connectToKeys = channels.map(n => n.partner_public_key);
const {lnd} = getLnds.find(n => n.node === node);
const {peers} = getPeers.find(n => n.node === node);
return asyncEach(connectToKeys, (key, cbk) => {
// Exit early when the peer is already connected
if (peers.map(n => n.public_key).includes(key)) {
return cbk();
}
const to = getNodes.find(n => n.public_key === key);
if (!to.sockets.length) {
return cbk([503, 'NoAddressFoundToConnectToNode', {to}]);
}
args.logger.info({
connecting_to: {alias: to.alias, public_key: to.public_key},
from: node,
});
return asyncRetry({times}, cbk => {
return asyncDetectSeries(to.sockets, ({socket}, cbk) => {
return addPeer({lnd, socket, public_key: key}, err => {
return cbk(null, !err);
});
},
(err, res) => {
if (!!err) {
return cbk(err);
}
if (!res) {
return cbk([503, 'FailedToConnectToPeer', ({peer: key})]);
}
return setTimeout(() => cbk(null, true), peerAddedDelayMs);
});
},
cbk);
},
cbk);
},
cbk);
}],
// Get connected peers again to look for features
getConnected: ['connect', 'getLnds', ({getLnds}, cbk) => {
// Exit early when allowing non-anchor support
if (!!args.skip_anchors_check) {
return cbk();
}
return getPeersForNodes({lnd: args.lnd, nodes: getLnds}, cbk);
}],
// Check for channels that do not support anchor feature bit
checkAnchorSupport: [
'getConnected',
'opens',
({getConnected, opens}, cbk) =>
{
// Exit early when allowing non-anchor support
if (!!args.skip_anchors_check) {
return cbk();
}
const channelOpens = flatten(opens.map(n => n.channels));
// Ids of nodes that are being opened to
const ids = channelOpens.map(n => n.partner_public_key);
// All the now connected peers
const peers = flatten(getConnected.map(n => n.peers));
// Only consider peers that are actually being opened to
const openingTo = peers.filter(n => ids.includes(n.public_key));
// Every peer's features set should include the anchors feature
const noAnchorsSupport = openingTo.filter(peer => {
return !peer.features.map(n => n.type).includes(featureAnchors);
});
// Node ids of the peers that dont' support anchors
const noAnchors = noAnchorsSupport.map(n => n.public_key);
// One of the peers fails to signal support for anchor channels
if (!!noAnchors.length) {
return cbk([400, 'AnchorChannelFeatureNotFound', {noAnchors}]);
}
return cbk();
}],
// Check all nodes that they will allow an inbound channel
checkAcceptance: [
'checkAnchorSupport',
'connect',
'getLnds',
'opens',
({connect, getLnds, opens}, cbk) =>
{
// Flatten out the opens so that they can be tried serially
const tests = opens.map(({channels, node}) => {
return channels.map(channel => ({
capacity: channel.capacity,
cooperative_close_address: channel.cooperative_close_address,
give_tokens: channel.give_tokens,
is_private: channel.is_private,
is_simplified_taproot: channel.is_simplified_taproot,
is_trusted_funding: channel.is_trusted_funding,
lnd: getLnds.find(n => n.node === node).lnd,
partner_public_key: channel.partner_public_key,
}));
});
return asyncEachSeries(flatten(tests), (test, cbk) => {
return acceptsChannelOpen({
capacity: test.capacity,
cooperative_close_address: test.cooperative_close_address,
give_tokens: test.give_tokens,
is_private: test.is_private,
is_simplified_taproot: test.is_simplified_taproot,
is_trusted_funding: test.is_trusted_funding,
lnd: test.lnd,
partner_public_key: test.partner_public_key,
},
cbk);
},
cbk);
}],
// Determine if internal funding should be used
isExternal: [
'capacities',
'checkAcceptance',
'connect',
'getWalletVersion',
({getWalletVersion}, cbk) =>
{
// Exit early when using internal funding
if (!!args.internal_fund_fee_rate) {
return cbk(null, false);
}
// Exit early when external directive is supplied
if (!!args.is_external) {
return cbk(null, args.is_external);
}
// Early versions of LND do not support internal PSBT funding
if (noInternalFundingVersions.includes(getWalletVersion.version)) {
return cbk(null, true);
}
// Peers are connected - what type of funding will be used?
args.logger.info(lineBreak);
// Prompt to make sure that internal funding should be used
return args.ask({
default: true,
message: 'Use internal wallet funds?',
name: 'internal',
type: 'confirm',
},
({internal}) => cbk(null, !internal));
}],
// Get the chain balance to see if there is enough available
getBalance: ['isExternal', ({isExternal}, cbk) => {
// Exit early when not using internal balance to fund
if (!!isExternal) {
return cbk();
}
return getChainBalance({lnd: args.lnd}, cbk);
}],
// Make sure there is enough coins to fund the opens
checkBalance: ['getBalance', 'opens', ({getBalance, opens}, cbk) => {
// Exit early when externally funding
if (!getBalance) {
return cbk();
}
const capacities = opens.map(n => n.channels.map(n => n.capacity));
// Make sure that the chain balance is sufficient
if (getBalance.chain_balance < sumOf(flatten(capacities))) {
return cbk([400, 'ExpectedChainBalanceAboveCapacityBeingOpened']);
}
return cbk();
}],
// Ask for the fee rate to use for internally funded opens
askForFeeRate: ['checkBalance', 'isExternal', ({isExternal}, cbk) => {
// Exit early when there are no internal funds being spent or internal fee rate is specified
if (!!isExternal || !!args.internal_fund_fee_rate) {
return cbk(null, {});
}
return askForFeeRate({ask: args.ask, lnd: args.lnd}, cbk);
}],
// Initiate open requests
openChannels: [
'askForFeeRate',
'capacities',
'checkBalance',
'connect',
'getLnds',
'getNodes',
'isExternal',
'opens',
({getLnds, getNodes, opens}, cbk) =>
{
// When there are multiple batches, broadcasting must be stopped
const [, hasMultipleBatches] = opens;
// Go through each batch and open channels
return asyncMapSeries(opens, asyncReflect(({channels, node}, cbk) => {
const {lnd} = getLnds.find(n => n.node === node);
return openChannels({
channels,
lnd,
is_avoiding_broadcast: true,
},
(err, res) => {
if (!!err) {
return cbk(err);
}
return cbk(null, {lnd, node, pending: res.pending});
});
}),
(err, res) => {
const openError = res.find(n => !!n.error);
const opening = res.map(n => n.value).filter(n => !!n);
if (!!openError) {
// Cancel past successful batch channel open proposals
return asyncEach(opening, ({lnd, pending}, cbk) => {
return asyncEach(pending, ({id}, cbk) => {
return cancelPendingChannel({id, lnd}, err => {
// Suppress errors
return cbk();
});
},
cbk);
},
() => {
// Return the original error
return cbk(openError.error);
});
}
return cbk(null, res.map(n => n.value));
});
}],
// Pending channel outputs
outputs: ['openChannels', ({openChannels}, cbk) => {
// All batches will be paid out together in a single tx
const pending = flatten(openChannels.map(({pending}) => {
return pending.map(n => ({address: n.address, tokens: n.tokens}));
}));
// Sort all the outputs using BIP 69
try {
pending.sort((a, b) => {
// Sort by tokens ascending when no tie breaker needed
if (a.tokens !== b.tokens) {
return a.tokens - b.tokens;
}
return bech32AsData(a.address).compare(bech32AsData(b.address));
});
} catch (err) {}
return cbk(null, pending);
}],
// Detect funding transaction
detectFunding: [
'getNetwork',
'isExternal',
'openChannels',
({getNetwork, isExternal, openChannels}, cbk) =>
{
// Exit early when the funding is coming from the internal wallet
if (!isExternal) {
return cbk();
}
if (!detectNetworks.includes(getNetwork.network)) {
return cbk();
}
const [{pending}] = openChannels;
const [{address, tokens}] = pending;
return asyncRetry({
interval: utxoPollingIntervalMs,
times: utxoPollingTimes,
},
cbk => {
return getAddressUtxo({
address,
tokens,
network: getNetwork.network,
request: args.request,
},
(err, res) => {
if (!!err) {
return cbk(err);
}
if (!res.transaction) {
return cbk([404, 'FailedToFindFundingUtxo']);
}
const foundTx = res.transaction;
return getPsbtFromTransaction({
network: getNetwork.network,
request: args.request,
transaction: foundTx,
},
(err, res) => {
if (!!err) {
return cbk();
}
args.logger.info({
funding_detected: Transaction.fromHex(foundTx).getId(),
});
return asyncEach(openChannels, ({lnd, node, pending}, cbk) => {
return fundPendingChannels({
lnd,
channels: pending.map(n => n.id),
funding: res.psbt,
},
() => cbk());
},
cbk);
});
});
},
() => {
// Ignore errors
return cbk();
});
}],
// Prompt for a PSBT or a signed transaction
getFunding: [
'askForFeeRate',
'isExternal',
'outputs',
asyncReflect(({askForFeeRate, isExternal, outputs}, cbk) =>
{
// Warn external funding that funds are expected within 10 minutes
if (!!isExternal) {
args.logger.info({
funding_deadline: moment().add(10, 'minutes').calendar(),
});
}
const fee = args.internal_fund_fee_rate;
return getFundedTransaction({
outputs,
ask: args.ask,
chain_fee_tokens_per_vbyte: fee || askForFeeRate.tokens_per_vbyte,
is_external: isExternal,
lnd: args.lnd,
logger: args.logger,
},
cbk);
})],
// Derive the funding PSBT which is needed for the funding flow
fundingPsbt: [
'getFunding',
'getNetwork',
'openChannels',
asyncReflect(({getFunding, getNetwork, openChannels}, cbk) =>
{
// Exit early when there was an error with the funding
if (!!getFunding.error) {
return cbk(null, {});
}
// Maintain a lock on any UTXO inputs until the tx confirms
if (isArray(getFunding.value.inputs)) {
maintainUtxoLocks({
id: getFunding.value.id,
inputs: getFunding.value.inputs,
interval: relockIntervalMs,
lnd: args.lnd,
},
() => {});
}
// Exit early when there was a PSBT entered and no need to convert a tx
if (!!getFunding.value.psbt) {
return cbk(null, {psbt: getFunding.value.psbt});
}
return getPsbtFromTransaction({
network: getNetwork.network,
request: args.request,
transaction: getFunding.value.transaction,
},
cbk);
})],
// Fund the channels using the PSBT
fundChannels: [
'fundingPsbt',
'openChannels',
'outputs',
asyncReflect(({fundingPsbt, openChannels, outputs}, cbk) =>
{
// Exit early when there is no funding PSBT
if (!fundingPsbt.value || !fundingPsbt.value.psbt) {
return cbk(null, {});
}
args.logger.info({funding: outputs.map(n => tokAsBigUnit(n.tokens))});
return asyncMap(openChannels, ({lnd, node, pending}, cbk) => {
return fundPendingChannels({
lnd,
channels: pending.map(n => n.id),
funding: fundingPsbt.value.psbt,
},
cbk);
},
cbk);
})],
// Broadcast the funding transaction when opening on multiple nodes
broadcastChainTransaction: [
'fundChannels',
'fundingPsbt',
'getFunding',
'openChannels',
({fundChannels, fundingPsbt, getFunding, openChannels}, cbk) =>
{
const fundingError = getFunding.error || fundingPsbt.error;
const error = fundChannels.error || fundingError;
// Exit early when the opening had an error and broadcasting isn't safe
if (!!error || !!fundingError) {
return cbk();
}
const toOpen = flatten(openChannels.map(n => n.pending));
const txId = fromHex(getFunding.value.transaction).getId();
args.logger.info({confirming_pending_open: true});
// Make sure that pending channels are showing up: got commitment tx
return asyncRetry({interval, times: pendingCheckTimes}, cbk => {
return asyncMap(openChannels, ({lnd, node, pending}, cbk) => {
return getChannelOutpoints({lnd}, cbk);
},
(err, res) => {
if (!!err) {
return cbk(err);
}
// Consolidate all channels from all nodes
const pending = flatten(res.map(n => n.channels));
// Only consider channels related to this funding tx
const opening = pending.filter(n => n.transaction_id === txId);
// Every channel to open should be reflected in a channel
if (opening.length !== toOpen.length) {
return cbk([503, 'FailedToFindPendingChannelOpen']);
}
args.logger.info({
raw_transaction_to_broadcast: getFunding.value.transaction,
});
// Exit early when avoiding broadcast
if (!!args.is_avoiding_broadcast) {
args.logger.info({is_avoiding_broadcast: true});
return cbk();
}
args.logger.info({broadcasting: getFunding.value.id});
return broadcastChainTransaction({
description,
lnd: args.lnd,
transaction: getFunding.value.transaction,
},
(err, res) => {
if (!!err) {
return cbk(err);
}
args.logger.info({broadcast: res.id});
return cbk();
});
});
},
cbk);
}],
// Cancel pending if there is an error
cancelPending: [
'fundChannels',
'fundingPsbt',
'getFunding',
'openChannels',
({fundChannels, fundingPsbt, getFunding, openChannels}, cbk) =>
{
const fundingError = getFunding.error || fundingPsbt.error;
const error = fundChannels.error || fundingError;
// Exit early when there were no errors at any step
if (!error) {
return cbk();
}
args.logger.info({
canceling_pending_channels: openChannels.map(({node, pending}) => ({
node,
ids: pending.map(n => n.id),
})),
});
return asyncEach(openChannels, ({lnd, pending}, cbk) => {
return asyncEach(pending => ({id}, cbk) => {
return cancelPendingChannel({id, lnd}, err => {
// Ignore errors when trying to cancel a pending channel
return cbk();
});
},
cbk);
},
() => {
// Return the original error that canceled the finalization
return cbk(null, error);
});
}],
// Cancel UTXO locks if they are present
cancelLocks: [
'cancelPending',
'getFunding',
({cancelPending, getFunding}, cbk) =>
{
// Exit early when there is no error that caused a cancel
if (!cancelPending) {
return cbk();
}
// Exit early when there are no UTXOs to unlock, like external funding
if (!isArray(getFunding.inputs)) {
return cbk(cancelPending);
}
// Unlock UTXOs locked from internal funding
return asyncEach(getFunding.inputs, (input, cbk) => {
// Potentially the UTXO will be relocked with a new id, but attempt
return unlockUtxo({
id: input.lock_id,
lnd: args.lnd,
transaction_id: input.transaction_id,
transaction_vout: input.transaction_vout,
},
() => {
//Ignore errors when trying to cancel a locked UTXO, it'll timeout
return cbk();
});
},
() => {
// Return the original error that caused the cancel
return cbk(cancelPending);
});
}],
// Transaction complete
completed: [
'broadcastChainTransaction',
'cancelPending',
'fundingPsbt',
'getFunding',
({getFunding, fundingPsbt}, cbk) =>
{
return cbk(null, {
transaction: getFunding.value.transaction,
transaction_id: getFunding.value.id,
});
}],
},
returnResult({reject, resolve, of: 'completed'}, cbk));
});
};