balanceofsatoshis
Version:
Lightning balance CLI
448 lines (382 loc) • 14.3 kB
JavaScript
const asyncAuto = require('async/auto');
const {createInvoice} = require('ln-service');
const {findKey} = require('ln-sync');
const {formatTokens} = require('ln-sync');
const {getChannels} = require('ln-service');
const {getIdentity} = require('ln-service');
const {getPeerLiquidity} = require('ln-sync');
const {parseAmount} = require('ln-accounting');
const {returnResult} = require('asyncjs-util');
const probeDestination = require('./probe_destination');
const defaultDescription = 'bos transfer between saved nodes';
const feeForRate = (rate, n) => Number(BigInt(n) * BigInt(rate) / BigInt(1e6));
const {isArray} = Array;
const rateForFee = (n, fee) => Number(BigInt(fee) * BigInt(1e6) / BigInt(n));
const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
const minTokens = 1;
const tokAsBigTok = tokens => !tokens ? undefined : (tokens / 1e8).toFixed(8);
/** Transfer funds to a destination
{
amount: <Amount to Transfer Tokens String>
[description]: <Description String>
[fs]: {
getFile: <Read File Contents Function> (path, cbk) => {}
}
[in_through]: <Transfer In Through Peer String>
[is_dry_run]: <Do Not Transfer Bool>
lnd: <Authenticated LND API Object>
logger: <Winston Logger Object>
max_fee_rate: <Maximum Fee Rate Number>
[out_through]: <Transfer Out Through Peer String>
[through]: <Transfer In and Out Through Peer String>
to: <Send To Authenticated LND API Object>
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.amount) {
return cbk([400, 'ExpectedAmountToTransferFundsToSavedNode']);
}
if (!args.fs) {
return cbk([400, 'ExpectedFileSystemMethodsToTransferFundsToNode']);
}
if (!!args.in_through && !!isArray(args.in_through)) {
return cbk([400, 'MultipleInThroughPeersNotSupported']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedAuthenticatedLndToTransferFundsToNode']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerForFundsTransferToSavedNode']);
}
if (args.max_fee_rate === undefined) {
return cbk([400, 'ExpectedMaxFeeRateToTransferFundsToSavedNode']);
}
if (!!args.out_through && !!isArray(args.out_through)) {
return cbk([400, 'MultipleOutThroughPeersNotSupported']);
}
if (!!args.through && !!isArray(args.through)) {
return cbk([400, 'MultipleThroughPeersNotSupported']);
}
if (!!args.through && (!!args.in_through || !!args.out_through)) {
return cbk([400, 'EitherInAndOutOrThroughSupportedNotBoth']);
}
if (!args.to) {
return cbk([400, 'ExpectedDestinationSavedNodeToTransferFundsTo']);
}
return cbk();
},
// Get channels with the peer in order to populate liquidity
getChannels: ['validate', ({}, cbk) => {
return getChannels({lnd: args.lnd}, cbk);
}],
// Get the channels of the destination
getRemoteChannels: ['validate', ({}, cbk) => {
return getChannels({lnd: args.to}, cbk);
}],
// Get the key of the node we are sending from
getFromKey: ['validate', ({}, cbk) => getIdentity({lnd: args.lnd}, cbk)],
// Get the key of the node we are sending to
getToKey: ['validate', ({}, cbk) => getIdentity({lnd: args.to}, cbk)],
// Inbound peer to reduce inbound on
inPeer: ['validate', ({}, cbk) => {
return cbk(null, args.in_through || args.through);
}],
// Outbound peer to increase inbound on
outPeer: ['validate', ({}, cbk) => {
return cbk(null, args.out_through || args.through);
}],
// Make sure that this is a transfer and not a send-to-self
checkDestination: [
'getFromKey',
'getToKey',
({getFromKey, getToKey}, cbk) =>
{
if (getFromKey.public_key === getToKey.public_key) {
return cbk([400, 'FromNodeAndToNodeCannotBeEqual']);
}
return cbk();
}],
// Determine the inbound peer public key
getInKey: [
'getRemoteChannels',
'inPeer',
({getRemoteChannels, inPeer}, cbk) =>
{
// Exit early when there is no inbound constraint
if (!inPeer) {
return cbk(null, {});
}
return findKey({
channels: getRemoteChannels.channels,
lnd: args.to,
query: inPeer,
},
cbk);
}],
// Determine the outbound peer public key
getOutKey: ['getChannels', 'outPeer', ({getChannels, outPeer}, cbk) => {
// Exit early when there is no outbound constraint
if (!outPeer) {
return cbk(null, {});
}
return findKey({
channels: getChannels.channels,
lnd: args.lnd,
query: outPeer,
},
cbk);
}],
// Parse the amount specified
parseAmount: [
'getChannels',
'getInKey',
'getOutKey',
'getRemoteChannels',
'getToKey',
'outPeer',
({
getChannels,
getInKey,
getOutKey,
getRemoteChannels,
getToKey,
outPeer,
},
cbk) =>
{
// Calculate the inbound peer inbound liquidity
const inInbound = getRemoteChannels.channels
.filter(n => n.partner_public_key === getInKey.public_key)
.reduce((sum, chan) => {
// Treat incoming payment as if they were still remote balance
const inbound = chan.pending_payments.filter(n => !n.is_outgoing);
const pending = sumOf(inbound.map(({tokens}) => tokens));
return sum + chan.remote_balance + pending;
},
Number());
// Calculate the inbound peer outbound liquidity
const inOutbound = getRemoteChannels.channels
.filter(n => n.partner_public_key === getInKey.public_key)
.reduce((sum, chan) => {
// Treat outgoing payment as if they were still local balance
const outbound = chan.pending_payments
.filter(n => !!n.is_outgoing);
const pending = sumOf(outbound.map(({tokens}) => tokens));
return sum + chan.local_balance + pending;
},
Number());
// Calculate the outbound peer inbound liquidity
const outInbound = getChannels.channels
.filter(n => n.partner_public_key === getOutKey.public_key)
.reduce((sum, chan) => {
// Treat incoming payment as if they were still remote balance
const inbound = chan.pending_payments.filter(n => !n.is_outgoing);
const pending = sumOf(inbound.map(({tokens}) => tokens));
return sum + chan.remote_balance + pending;
},
Number());
// Calculate the outbound peer outbound liquidity
const outOutbound = getChannels.channels
.filter(n => n.partner_public_key === getOutKey.public_key)
.reduce((sum, chan) => {
// Treat outgoing payment as if they were still local balance
const outbound = chan.pending_payments
.filter(n => !!n.is_outgoing);
const pending = sumOf(outbound.map(({tokens}) => tokens));
return sum + chan.local_balance + pending;
},
Number());
// Variables to use in amount
const variables = {
in_inbound: inInbound,
in_outbound: inOutbound,
out_inbound: outInbound,
out_liquidity: sumOf(
getChannels.channels
.filter(n => n.partner_public_key === getOutKey.public_key)
.map(n => n.capacity)
),
out_outbound: outOutbound,
};
if (!!outPeer) {
args.logger.info(variables);
}
try {
return cbk(null, parseAmount({variables, amount: args.amount}));
} catch (err) {
return cbk([400, 'FailedToParseTransferAmount', err]);
}
}],
// Check if the amount can route to the destination
probe: [
'getInKey',
'getOutKey',
'getToKey',
'parseAmount',
({getInKey, getOutKey, getToKey, parseAmount}, cbk) =>
{
if (parseAmount.tokens < minTokens) {
return cbk([400, 'ExpectedNonZeroAmountToTransferFundsToSavedNode']);
}
return probeDestination({
destination: getToKey.public_key,
fs: args.fs,
lnd: args.lnd,
logger: args.logger,
in_through: getInKey.public_key,
out_through: getOutKey.public_key,
timeout_minutes: args.timeout_minutes,
tokens: parseAmount.tokens,
},
cbk);
}],
// Create the invoice on the receiving side
createInvoice: [
'checkDestination',
'parseAmount',
'probe',
({parseAmount, probe}, cbk) =>
{
if (!probe.success) {
return cbk([400, 'FailedToFindPathToDestination']);
}
// Exit early when this is a dry run
if (!!args.is_dry_run) {
return cbk(null, {tokens: parseAmount.tokens});
}
const maxFee = feeForRate(args.max_fee_rate, parseAmount.tokens);
const minFeeRate = rateForFee(parseAmount.tokens, probe.fee);
if (probe.fee > maxFee) {
return cbk([400, 'InsufficientMaxFeeRate', {needed: minFeeRate}]);
}
return createInvoice({
description: args.description || defaultDescription,
lnd: args.to,
tokens: parseAmount.tokens,
},
cbk);
}],
// Transfer the amount to the destination
transfer: [
'createInvoice',
'getInKey',
'getOutKey',
'getToKey',
({createInvoice, getInKey, getOutKey, getToKey}, cbk) =>
{
const maxFee = feeForRate(args.max_fee_rate, createInvoice.tokens);
args.logger.info({
max_fee: maxFee,
paying: formatTokens({tokens: createInvoice.tokens}).display,
to: getToKey.public_key,
});
if (!!args.is_dry_run) {
return cbk([400, 'TransferFundsDryRun']);
}
return probeDestination({
lnd: args.lnd,
logger: args.logger,
in_through: getInKey.public_key,
is_real_payment: true,
max_fee: maxFee,
out_through: getOutKey.public_key,
request: createInvoice.request,
timeout_minutes: args.timeout_minutes,
},
cbk);
}],
// Get adjusted iinbound liquidity after transfer
getAdjustedInbound: [
'outPeer',
'transfer',
({outPeer, transfer}, cbk) =>
{
// Exit early when the payment failed
if (!transfer.preimage) {
return cbk([503, 'UnexpectedSendPaymentFailure']);
}
// Exit early when there is no outbound constraint
if (!outPeer) {
return cbk();
}
const [, inbound] = transfer.relays.slice().reverse();
return getPeerLiquidity({
lnd: args.to,
public_key: inbound,
settled: transfer.id,
},
cbk);
}],
// Get adjusted outbound liquidity after transfer
getAdjustedOutbound: [
'outPeer',
'transfer',
({outPeer, transfer}, cbk) =>
{
// Exit early when the payment failed
if (!transfer.preimage) {
return cbk([503, 'UnexpectedSendPaymentFailure']);
}
// Exit early when there is no outbound constraint
if (!outPeer) {
return cbk();
}
const [out] = transfer.relays;
return getPeerLiquidity({
lnd: args.lnd,
public_key: out,
settled: transfer.id,
},
cbk);
}],
// Final liquidity outcome
liquidity: [
'getAdjustedInbound',
'getAdjustedOutbound',
'transfer',
({getAdjustedInbound, getAdjustedOutbound, transfer}, cbk) =>
{
if (!getAdjustedOutbound) {
return cbk();
}
const [out] = transfer.relays;
const outOpeningIn = getAdjustedOutbound.inbound_opening;
const outOpeningOut = getAdjustedOutbound.outbound_opening;
const outPendingIn = getAdjustedOutbound.inbound_pending;
const outPendingOut = getAdjustedOutbound.outbound_pending;
const [, inbound] = transfer.relays.slice().reverse();
const inboundAlias = getAdjustedInbound.alias;
const inOpeningIn = getAdjustedInbound.inbound_opening;
const inOpeningOut = getAdjustedInbound.outbound_opening;
const inPendingIn = getAdjustedInbound.inbound_pending;
const inPendingOut = getAdjustedInbound.outbound_pending;
args.logger.info({
local_liquidity_change: {
increased_inbound_on: `${getAdjustedOutbound.alias} ${out}`.trim(),
liquidity_inbound: tokAsBigTok(getAdjustedOutbound.inbound),
liquidity_inbound_opening: tokAsBigTok(outOpeningIn),
liquidity_inbound_pending: tokAsBigTok(outPendingIn),
liquidity_outbound: tokAsBigTok(getAdjustedOutbound.outbound),
liquidity_outbound_opening: tokAsBigTok(outOpeningOut),
liquidity_outbound_pending: tokAsBigTok(outPendingOut),
},
remote_liquidity_change: {
decreased_inbound_on: `${inboundAlias} ${inbound}`.trim(),
liquidity_inbound: tokAsBigTok(getAdjustedInbound.inbound),
liquidity_inbound_opening: tokAsBigTok(inOpeningIn),
liquidity_inbound_pending: tokAsBigTok(inPendingIn),
liquidity_outbound: tokAsBigTok(getAdjustedInbound.outbound),
liquidity_outbound_opening: tokAsBigTok(inOpeningOut),
liquidity_outbound_pending: tokAsBigTok(inPendingOut),
},
});
return cbk();
}],
},
returnResult({reject, resolve, of: 'transfer'}, cbk));
});
};