add support for external, multi-channel opening

This commit is contained in:
Alex Bosworth 2020-05-02 14:30:34 -07:00
parent 96b258a162
commit 5379e71422
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
24 changed files with 1121 additions and 158 deletions

View file

@ -1,5 +1,10 @@
# Versions
## Version 5.33.0
- `chain-receive`: Require auth payment to initiate swap
- `open`: Add method to open one or more channels funded by an external wallet
## Version 5.32.0
- `price`: Add `--from` to specify rate provider

View file

@ -97,6 +97,9 @@ bos market
# View and adjust list of saved nodes
bos nodes
# Open channesl to public keys using external funding
bos open "public_keys..."
# Outputs the sum total of local channel liquidity
bos outbound-liquidity

View file

@ -4,6 +4,8 @@ const {getNode} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const peerLiquidity = require('./peer_liquidity');
/** Get the rundown on liquidity with a specific peer
{
@ -16,7 +18,9 @@ const {returnResult} = require('asyncjs-util');
{
alias: <Alias String>
inbound: <Inbound Liquidity Tokens Number>
inbound_pending: <Pending Inbound Liquidity Tokens Number>
outbound: <Outbound Liquidity Tokens Number>
outbound_pending: <Pending Outbound Liquidity Tokens Number>
}
*/
module.exports = (args, cbk) => {
@ -49,7 +53,7 @@ module.exports = (args, cbk) => {
},
(err, res) => {
if (!!err) {
return cbk(null, {alias: '', public_key: args.public_key});
return cbk(null, {alias: String(), public_key: args.public_key});
}
return cbk(null, res);
@ -72,84 +76,24 @@ module.exports = (args, cbk) => {
return channel.partner_public_key === args.public_key;
});
const inbound = channels.reduce((sum, channel) => {
const settled = channel.pending_payments.find(n => {
return !!args.settled && n.id === args.settled;
});
if (!!settled && settled.is_outgoing) {
return sum + channel.remote_balance + settled.tokens;
}
if (!!settled && !settled.is_outgoing) {
return sum + channel.remote_balance - settled.tokens;
}
return sum + channel.remote_balance;
},
Number());
const outbound = channels.reduce((sum, channel) => {
const settled = channel.pending_payments.find(n => {
return !!args.settled && n.id === args.settled;
});
if (!!settled && settled.is_outgoing) {
return sum + channel.local_balance - settled.tokens;
}
if (!!settled && !settled.is_outgoing) {
return sum + channel.local_balance + settled.tokens;
}
return sum + channel.local_balance;
},
Number());
const pendingInHtlcs = channels.reduce((allPending, channel) => {
return allPending + channel.pending_payments.reduce((sum, n) => {
if (n.id === args.settled) {
return sum;
}
return sum + (n.is_outgoing ? n.tokens : Number());
},
Number());
},
Number());
const pendingOutHtlcs = channels.reduce((allPending, channel) => {
return allPending + channel.pending_payments.reduce((sum, n) => {
if (n.id === args.settled) {
return sum;
}
return sum + (n.is_outgoing ? Number() : n.tokens);
},
Number());
},
Number());
const pendingOpen = getPendingChannels.pending_channels.filter(n => {
return !!n.is_opening && n.partner_public_key === args.public_key;
const opening = getPendingChannels.pending_channels.filter(chan => {
return chan.is_opening && chan.partner_public_key == args.public_key;
});
const pendingInbound = pendingOpen.reduce((sum, channel) => {
return sum + channel.remote_balance;
},
Number());
const pendingOutbound = pendingOpen.reduce((sum, channel) => {
return sum + channel.local_balance;
},
Number());
const liquidity = peerLiquidity({
channels,
opening,
settled: args.settled,
});
return cbk(null, {
alias: getNode.alias,
inbound: pendingInbound + inbound,
inbound_pending: pendingInHtlcs,
outbound: pendingOutbound + outbound,
outbound_pending: pendingOutHtlcs,
inbound: liquidity.inbound,
inbound_opening: liquidity.inbound_opening,
inbound_pending: liquidity.inbound_pending,
outbound: liquidity.outbound,
outbound_opening: liquidity.outbound_opening,
outbound_pending: liquidity.outbound_pending,
});
}],
},

View file

@ -0,0 +1,85 @@
/** Calculate peer liquidity
{
channels: [{
local_balance: <Local Balance Tokens Number>
pending_payments: [{
id: <Payment Hash Hex String>
is_outgoing: <Payment is Outgoing Bool>
tokens: <Payment Tokens Number>
}]
remote_balance: <Remote Balance Tokens Number>
}]
opening: [{
local_balance: <Local Balance Tokens Number>
remote_balance: <Remote Balance Tokens Number>
}]
[settled]: <Known Settled Payment Id Hex String>
}
@returns
{
inbound: <Inbound Liquidity Tokens Number>
inbound_opening: <Opening Inbound Liquidity Tokens Number>
inbound_pending: <Pending Inbound Liquidity Tokens Number>
outbound: <Outbound Liquidity Tokens Number>
outbound_opening: <Opening Outbound Liquidity Tokens Number>
outbound_pending: <Pending Outbound Liquidity Tokens Number>
}
*/
module.exports = ({channels, opening, settled}) => {
// Inbound is the sum of remote balances
const inbound = channels.reduce((sum, channel) => {
// Settled payment is known so it can be considered part of remote balance
const settledBalance = channel.pending_payments
.filter(n => n.id === settled)
.map(n => n.is_outgoing ? n.tokens : Number())
.reduce((sum, n) => sum + n, Number());
return sum + channel.remote_balance + settledBalance;
},
Number());
// Outbound is the sum of local balances
const outbound = channels.reduce((sum, channel) => {
// Settled payment is known so it can be considered part of local balance
const settledBalance = channel.pending_payments
.filter(n => n.id === settled)
.map(n => !n.is_outgoing ? n.tokens : Number())
.reduce((sum, n) => sum + n, Number());
return sum + channel.local_balance + settledBalance;
},
Number());
// Pending inbound is potential remote balance amount assuming HTLCs succeed
const pendingInbound = channels.reduce((allPending, channel) => {
return allPending + channel.pending_payments
.filter(n => n.id !== settled && !!n.is_outgoing)
.reduce((sum, n) => sum + n.tokens, Number());
},
Number());
// Pending outbound is the potential local amount assuming HTLCs succeed
const pendingOutbound = channels.reduce((allPending, channel) => {
return allPending + channel.pending_payments
.filter(n => n.id !== settled && !n.is_outgoing)
.reduce((sum, n) => sum + n.tokens, Number());
},
Number());
// How much remote balance is in channels opening towards us?
const openIn = opening.reduce((sum, n) => sum + n.remote_balance, Number());
// How much local balance is in channels opening outwards?
const openOut = opening.reduce((sum, n) => sum + n.local_balance, Number());
return {
inbound,
outbound,
inbound_opening: openIn,
inbound_pending: pendingInbound,
outbound_opening: openOut,
outbound_pending: pendingOutbound,
};
};

27
bos
View file

@ -44,6 +44,7 @@ const {ignoreFromAvoid} = require('./routing');
const {lndCredentials} = require('./lnd');
const marketPairs = require('./fiat').pairs;
const {openChannel} = require('./network');
const {openChannels} = require('./network');
const {priceProviders} = require('./fiat');
const {probeDestination} = require('./network');
const {rebalance} = require('./swaps');
@ -208,7 +209,6 @@ prog
.option('--api-key <api_key>', 'Pre-paid API key to use', hexMatch)
.option('--in <peer>', 'Request receiving through a specific peer')
.option('--max-fee <max_fee>', 'Max fee in tokens to pay', INT, 4000)
.option('--no-auth', 'Avoid using authenticated service')
.option('--no-color', 'Mute all colors')
.option('--node <node_name>', 'Node to receive funds on')
.option('--recovery <refund_recovery>', 'Attempt refund of swap')
@ -221,7 +221,6 @@ prog
logger,
api_key: options.apiKey || undefined,
in_through: options.in || undefined,
is_free: options.noAuth || false,
is_refund_test: options.testRefund,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
max_fee: options.maxFee,
@ -690,6 +689,30 @@ prog
});
})
// Open channels
.command('open', 'Open channels')
.help('Create channels from an external wallet. Note: do not self-broadcast')
.argument('<peer_public_keys...>', 'With nodes with public keys')
.option('--amount <channel_capacity>', 'Capacities to open', REPEATABLE)
.option('--node <node_name>', 'Node to open channels')
.action((args, options, logger) => {
return new Promise(async (resolve, reject) => {
try {
return openChannels({
logger,
request,
ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(res)),
capacities: flatten([options.amount].filter(n => !!n)),
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
public_keys: args.peerPublicKeys,
},
returnObject({logger, reject, resolve}));
} catch (err) {
return reject(err);
}
});
})
// Get outbound liquidity information: available outbound off-chain tokens
.command('outbound-liquidity', 'Get outbound liquidity size')
.option('--above <tokens>', 'Return amount above watermark', INT)

113
chain/get_address_utxo.js Normal file
View file

@ -0,0 +1,113 @@
const asyncAuto = require('async/auto');
const asyncRetry = require('async/retry');
const {returnResult} = require('asyncjs-util');
const {endpoints} = require('./blockstream');
const getRawTransaction = require('./get_raw_transaction');
const defaultInterval = n => 50 * Math.pow(2, n);
const {isArray} = Array;
const isHash = n => !!n && /^[0-9A-F]{64}$/i.test(n);
const isHex = n => !!n && !(n.length % 2) && /^[0-9A-F]*$/i.test(n);
/** Get raw transaction hex
{
address: <Address String>
[interval]: <Retry Interval Milliseconds Number>
network: <Network Name String>
request: <Request Function>
[retries]: <Retries Count Number>
tokens: <Tokens Number>
}
@returns via cbk or Promise
{
[transaction]: <Transaction Hex String>
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.address) {
return cbk([400, 'ExpectedAddressToGetUtxoFor']);
}
if (!args.network) {
return cbk([400, 'ExpectedNetworkNameToGetUtxoForAddress']);
}
if (!endpoints[args.network]) {
return cbk([400, 'UnsupportedNetworkToGetUtxoForAddress']);
}
if (!args.request) {
return cbk([400, 'ExpectedRequestMethodToGetUtxoForAddress']);
}
if (!args.tokens) {
return cbk([400, 'ExpectedTokensToGetUtxoForAddress']);
}
return cbk();
},
// Get the UTXO transaction id
getTransactionId: ['validate', ({}, cbk) => {
return asyncRetry({
interval: args.interval || defaultInterval,
times: args.retries,
},
cbk => {
return args.request({
json: true,
url: `${endpoints[args.network]}address/${args.address}/utxo`,
},
(err, r, utxos) => {
if (!!err) {
return cbk([503, 'FailedToGetAddressUtxos', {err}]);
}
if (!isArray(utxos)) {
return cbk([503, 'ExpectedArrayOfUtxosInAddressUtxosResponse']);
}
const [utxo] = utxos.filter(n => n.value === args.tokens);
// Exit early when there is no UTXO for the address
if (!utxo) {
return cbk();
}
if (!isHash(utxo.txid)) {
return cbk([503, 'ExpectedTransactionIdInUtxoForAddress']);
}
return cbk(null, utxo.txid);
});
},
cbk);
}],
// Get the raw transaction
getTransaction: ['getTransactionId', ({getTransactionId}, cbk) => {
// Exit early when there is no UTXO
if (!getTransactionId) {
return cbk(null, {});
}
return getRawTransaction({
id: getTransactionId,
interval: args.interval,
network: args.network,
request: args.request,
retries: args.retries,
},
cbk);
}],
},
returnResult({reject, resolve, of: 'getTransaction'}, cbk));
});
};

View file

@ -0,0 +1,84 @@
const asyncAuto = require('async/auto');
const asyncRetry = require('async/retry');
const {returnResult} = require('asyncjs-util');
const {Transaction} = require('bitcoinjs-lib');
const {endpoints} = require('./blockstream');
const defaultInterval = n => 50 * Math.pow(2, n);
const isHash = n => !!n && /^[0-9A-F]{64}$/i.test(n);
const isHex = n => !!n && !(n.length % 2) && /^[0-9A-F]*$/i.test(n);
/** Get raw transaction hex
{
id: <Transaction Id Hex String>
[interval]: <Retry Interval Milliseconds Number>
network: <Network Name String>
request: <Request Function>
[retries]: <Retries Count Number>
}
@returns via cbk or Promise
{
transaction: <Transaction Hex String>
}
*/
module.exports = ({id, interval, network, request, retries}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!isHash(id)) {
return cbk([400, 'ExpectedTransactionIdToGetRawTransaction']);
}
if (!network) {
return cbk([400, 'ExpectedNetworkNameToGetRawTransaction']);
}
if (!endpoints[network]) {
return cbk([400, 'UnsupportedNetworkToGetRawTransaction']);
}
if (!request) {
return cbk([400, 'ExpectedRequestMethodToGetRawTrasaction']);
}
return cbk();
},
// Get the raw transaction
getTransaction: ['validate', ({}, cbk) => {
return asyncRetry({
interval: interval || defaultInterval,
times: retries,
},
cbk => {
return request({
url: `${endpoints[network]}tx/${id}/hex`,
},
(err, r, transaction) => {
if (!!err) {
return cbk([503, 'FailedToGetRawTransaction', {err}]);
}
if (!isHex(transaction)) {
return cbk([503, 'ExpectedTransactionInResponse']);
}
try {
Transaction.fromHex(transaction);
} catch (err) {
return cbk([503, 'ExpectedValidTransactionInResponse']);
}
return cbk(null, {transaction});
});
},
cbk);
}],
},
returnResult({reject, resolve, of: 'getTransaction'}, cbk));
});
};

View file

@ -1,15 +1,19 @@
const getAddressUtxo = require('./get_address_utxo');
const getChainFees = require('./get_chain_fees');
const getChannelCloses = require('./get_channel_closes');
const getDepositAddress = require('./get_deposit_address');
const getMempoolSize = require('./get_mempool_size');
const getRawTransaction = require('./get_raw_transaction');
const getUtxos = require('./get_utxos');
const splitUtxos = require('./split_utxos');
module.exports = {
getAddressUtxo,
getChainFees,
getChannelCloses,
getDepositAddress,
getMempoolSize,
getRawTransaction,
getUtxos,
splitUtxos,
};

View file

@ -20,7 +20,7 @@ const {getPastForwards} = require('./../routing');
const {sortBy} = require('./../arrays');
const asEarnings = (on, tok) => !!on ? (tok / 1e8).toFixed(8) : undefined;
const asRate = n => n !== undefined ? (n / 1e4).toFixed(2) + '%' : undefined;
const asRate = n => n !== undefined ? `${(n/1e4).toFixed(2)}% (${n})` : undefined;
const defaultSort = 'first_connected';
const fromNow = epoch => !epoch ? undefined : moment(epoch * 1e3).fromNow();
const {isArray} = Array;

View file

@ -6,6 +6,7 @@ const getPeers = require('./get_peers');
const getScoredNodes = require('./get_scored_nodes');
const networks = require('./networks');
const openChannel = require('./open_channel');
const openChannels = require('./open_channels');
const probeDestination = require('./probe_destination');
const reconnect = require('./reconnect');
const removePeer = require('./remove_peer');
@ -21,6 +22,7 @@ module.exports = {
getScoredNodes,
networks,
openChannel,
openChannels,
probeDestination,
reconnect,
removePeer,

432
network/open_channels.js Normal file
View file

@ -0,0 +1,432 @@
const {randomBytes} = require('crypto');
const {addPeer} = require('ln-service');
const asyncAuto = require('async/auto');
const asyncEach = require('async/each');
const asyncDetectSeries = require('async/detectSeries');
const asyncMap = require('async/map');
const asyncRetry = require('async/retry');
const {cancelPendingChannel} = require('ln-service');
const {decodePsbt} = require('psbt');
const {extractTransaction} = require('psbt');
const {finalizePsbt} = require('psbt');
const {fundPendingChannels} = require('ln-service');
const {getNode} = require('ln-service');
const {getPeers} = require('ln-service');
const {getWalletVersion} = require('ln-service');
const {openChannels} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {Transaction} = require('bitcoinjs-lib');
const {transactionAsPsbt} = require('psbt');
const {getAddressUtxo} = require('./../chain');
const {getRawTransaction} = require('./../chain');
const getNetwork = require('./../network/get_network');
const base64AsHex = n => Buffer.from(n, 'base64').toString('hex');
const defaultChannelCapacity = 5e6;
const format = 'p2wpkh';
const {isArray} = Array;
const isHex = n => !!n && !(n.length % 2) && /^[0-9A-F]*$/i.test(n);
const makeId = () => randomBytes(32).toString('hex');
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 Number>]
lnd: <Authenticated LND API Object>
logger: <Winston Logger Object>
public_keys: [<Public Key Hex String>]
request: <Request Function>
}
@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 (!args.lnd) {
return cbk([400, 'ExpectedLndToInitiateOpenChannelRequests']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToInitiateOpenChannelRequests']);
}
if (!isArray(args.public_keys)) {
return cbk([400, 'ExpectedPublicKeysToOpenChannels']);
}
const hasCapacities = !!args.capacities.length;
const publicKeysLength = args.public_keys.length;
if (!!hasCapacities && publicKeysLength !== args.capacities.length) {
return cbk([400, 'CapacitiesMustBeSpecifiedForEveryPublicKey']);
}
if (!args.request) {
return cbk([400, 'ExpectedRequestFunctionToOpenChannels']);
}
return cbk();
},
// Get 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) => {
if (!!err) {
return cbk(null, {public_key: key, sockets: []});
}
return cbk(null, {
alias: res.alias,
public_key: key,
sockets: res.sockets,
});
});
},
cbk);
}],
// Get connected peers to see if we are already connected
getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
// Get the wallet version and check if it is compatible
getWalletVersion: ['validate', ({}, cbk) => {
return getWalletVersion({lnd: args.lnd}, err => {
if (!!err) {
return cbk([400, 'BackingLndCannotBeUsedToOpenChannels', {err}]);
}
return cbk();
});
}],
// Connect up to the peers
connect: ['getNodes', 'getPeers', ({getNodes, getPeers}, cbk) => {
return asyncEach(args.public_keys, (key, cbk) => {
// Exit early when the peer is already connected
if (getPeers.peers.map(n => n.public_key).includes(key)) {
return cbk();
}
const node = getNodes.find(n => n.public_key === key);
if (!node.sockets.length) {
return cbk([503, 'NoAddressFoundToConnectToNode', {node}]);
}
args.logger.info({
connecting_to: {alias: node.alias, public_key: node.public_key},
});
return asyncDetectSeries(node.sockets, (socket, cbk) => {
return addPeer({socket, lnd: args.lnd, public_key: key}, err => {
return cbk(null, !err);
});
},
(err, res) => {
if (!!err) {
return cbk(err);
}
if (!res) {
return cbk([503, 'FailedToConnectToPeer', ({public_key: key})]);
}
return cbk(null, true);
});
},
cbk);
}],
// Initiate open requests
openChannels: ['connect', 'getWalletVersion', ({}, cbk) => {
const channels = args.public_keys.map((key, i) => {
const capacity = args.capacities[i] || defaultChannelCapacity;
return {capacity, partner_public_key: key};
});
return openChannels({channels, lnd: args.lnd}, cbk);
}],
// Detect funding transaction
detectFunding: [
'getNetwork',
'openChannels',
({getNetwork, openChannels}, cbk) =>
{
return asyncRetry({
interval: utxoPollingIntervalMs,
times: utxoPollingTimes,
},
cbk => {
const [{address, tokens}] = openChannels.pending;
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;
const inputs = Transaction.fromHex(foundTx).ins;
const hashes = inputs.map(n => n.hash.toString('hex'));
const spendIds = hashes
.map(n => Buffer.from(n, 'hex').reverse())
.map(n => n.toString('hex'));
return asyncMap(spendIds, (id, cbk) => {
return getRawTransaction({
id,
network: getNetwork.network,
request: args.request,
},
cbk);
},
(err, res) => {
if (!!err) {
return cbk(null, {err: [400, 'FailedToGetInputs', {err}]});
}
const spending = res.map(n => n.transaction);
try {
const {psbt} = transactionAsPsbt({
spending,
transaction: foundTx,
});
finalizePsbt({psbt});
} catch (err) {
return cbk([404, 'TransactionCannotHavePsbtDerived']);
}
const signed = transactionAsPsbt({
spending,
transaction: foundTx,
});
args.logger.info({
funding_detected: Transaction.fromHex(foundTx).getId(),
});
return fundPendingChannels({
channels: openChannels.pending.map(n => n.id),
funding: finalizePsbt({psbt: signed.psbt}).psbt,
lnd: args.lnd,
},
() => {
return cbk();
});
});
});
},
() => {
// Ignore errors
return cbk();
});
}],
// Prompt for a PSBT or a signed transaction
getFunding: ['openChannels', ({openChannels}, cbk) => {
const payTo = openChannels.pending
.map(channel => {
return `${tokAsBigUnit(channel.tokens)} to ${channel.address}`;
})
.join(', ');
const funding = {
message: `Enter signed transaction or PSBT that pays ${payTo}`,
name: 'fund',
};
return args.ask(funding, ({fund}) => cbk(null, fund));
}],
// Translate funding data into hex
fundingHex: ['getFunding', ({getFunding}, cbk) => {
// Exit early when there is no funding
if (!getFunding) {
return cbk(null, {err: [400, 'ExpectedFundingTransaction']});
}
// Exit early when funding data is already hex
if (isHex(getFunding.trim())) {
return cbk(null, {hex: getFunding.trim()});
}
try {
return cbk(null, {hex: base64AsHex(getFunding.trim())});
} catch (err) {
return cbk(null, {err: [400, 'UnexpectedEncodingForFundingTx']});
}
}],
// Funding PSBT
fundingPsbt: [
'fundingHex',
'getNetwork',
'openChannels',
({fundingHex, getNetwork, openChannels}, cbk) =>
{
// Exit early when there was an error with the funding hex
if (!!fundingHex.err) {
return cbk(null, {});
}
try {
decodePsbt({psbt: fundingHex.hex});
// The PSBT is a valid funding PSBT
return cbk(null, {psbt: fundingHex.hex});
} catch (err) {}
try {
Transaction.fromHex(fundingHex.hex);
} catch (err) {
return cbk(null, {err: [400, 'ExpectedValidTxOrPsbtToFundChans']});
}
const transaction = fundingHex.hex;
const {ins} = Transaction.fromHex(transaction);
const ids = ins.map(n => n.hash.reverse().toString('hex'));
return asyncMap(ids, (id, cbk) => {
return getRawTransaction({
id,
network: getNetwork.network,
request: args.request,
},
cbk);
},
(err, res) => {
if (!!err) {
return cbk(null, {err: [400, 'FailedToGetFundingInputs', {err}]});
}
const spending = res.map(n => n.transaction);
try {
const {psbt} = transactionAsPsbt({spending, transaction});
const finalized = finalizePsbt({psbt});
return cbk(null, {psbt: finalized.psbt});
} catch (err) {
return cbk(null, {err: [400, 'FailedToConvertTxToPsbt', {err}]});
}
});
}],
// Fund the channels using the PSBT
fundChannels: [
'fundingPsbt',
'openChannels',
({fundingPsbt, openChannels}, cbk) =>
{
// Exit early when there is no funding PSBT
if (!fundingPsbt.psbt) {
return cbk(null, {});
}
args.logger.info({
funding: openChannels.pending.map(n => tokAsBigUnit(n.tokens)),
});
return fundPendingChannels({
channels: openChannels.pending.map(n => n.id),
funding: fundingPsbt.psbt,
lnd: args.lnd,
},
err => {
if (!!err) {
return cbk(null, {err});
}
return cbk(null, {});
});
}],
// Cancel pending if there is an error
cancelPending: [
'fundChannels',
'fundingHex',
'fundingPsbt',
'openChannels',
({fundChannels, fundingHex, fundingPsbt, openChannels}, cbk) =>
{
// Exit early when there were no errors
if (!fundChannels.err && !fundingHex.err && !fundingPsbt.err) {
return cbk();
}
// Cancel outstanding pending channels when there is an error
return asyncEach(openChannels.pending, (channel, cbk) => {
return cancelPendingChannel({id: channel.id, lnd: args.lnd}, () => {
return cbk();
});
},
() => {
// Return the error that canceled the finalization
return cbk(fundChannels.err || fundingHex.err || fundingPsbt.err);
});
}],
// Transaction complete
completed: [
'cancelPending',
'fundingPsbt',
({cancelPending, fundingPsbt}, cbk) =>
{
try {
const {transaction} = extractTransaction({psbt: fundingPsbt.psbt});
return cbk(null, {
transaction_id: Transaction.fromHex(transaction).getId(),
});
} catch (err) {
return cbk([503, 'UnexpectedErrorGettingTransactionId', {err}]);
}
}],
},
returnResult({reject, resolve, of: 'completed'}, cbk));
});
};

105
package-lock.json generated
View file

@ -2015,6 +2015,28 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
"integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA=="
},
"ln-service": {
"version": "48.0.1",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-48.0.1.tgz",
"integrity": "sha512-NPSeQ2YARuIa53Qm+EUMheebwZTm+49mJgy4YegSlsBGNk2CzISSEUy13zT6ScgwbtmXEZH22xHvcnJhxCyamw==",
"requires": {
"@datastructures-js/priority-queue": "4.1.0",
"async": "3.2.0",
"asyncjs-util": "1.2.2",
"bitcoinjs-lib": "5.1.7",
"bn.js": "5.1.1",
"bolt07": "1.5.0",
"bolt09": "0.0.2",
"cors": "2.8.5",
"express": "4.17.1",
"invoices": "1.0.2",
"is-base64": "1.1.0",
"lightning": "2.0.0",
"macaroon": "3.0.4",
"morgan": "1.10.0",
"ws": "7.2.5"
}
}
}
},
@ -3303,12 +3325,41 @@
"goldengate": "5.3.3",
"json2csv": "5.0.0",
"ln-service": "48.0.1"
},
"dependencies": {
"bn.js": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
"integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA=="
},
"ln-service": {
"version": "48.0.1",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-48.0.1.tgz",
"integrity": "sha512-NPSeQ2YARuIa53Qm+EUMheebwZTm+49mJgy4YegSlsBGNk2CzISSEUy13zT6ScgwbtmXEZH22xHvcnJhxCyamw==",
"requires": {
"@datastructures-js/priority-queue": "4.1.0",
"async": "3.2.0",
"asyncjs-util": "1.2.2",
"bitcoinjs-lib": "5.1.7",
"bn.js": "5.1.1",
"bolt07": "1.5.0",
"bolt09": "0.0.2",
"cors": "2.8.5",
"express": "4.17.1",
"invoices": "1.0.2",
"is-base64": "1.1.0",
"lightning": "2.0.0",
"macaroon": "3.0.4",
"morgan": "1.10.0",
"ws": "7.2.5"
}
}
}
},
"ln-service": {
"version": "48.0.1",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-48.0.1.tgz",
"integrity": "sha512-NPSeQ2YARuIa53Qm+EUMheebwZTm+49mJgy4YegSlsBGNk2CzISSEUy13zT6ScgwbtmXEZH22xHvcnJhxCyamw==",
"version": "48.0.3",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-48.0.3.tgz",
"integrity": "sha512-fwdxNwtij3msUIlsUhQjQczMEcpbSWkts4nkYqCl2CqwBx8iYwe73PPIx/bOawp6tEG2OA/T96dVSwhS+4R4bg==",
"requires": {
"@datastructures-js/priority-queue": "4.1.0",
"async": "3.2.0",
@ -3321,7 +3372,7 @@
"express": "4.17.1",
"invoices": "1.0.2",
"is-base64": "1.1.0",
"lightning": "2.0.0",
"lightning": "2.0.1",
"macaroon": "3.0.4",
"morgan": "1.10.0",
"ws": "7.2.5"
@ -3331,6 +3382,26 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
"integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA=="
},
"lightning": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/lightning/-/lightning-2.0.1.tgz",
"integrity": "sha512-FOulJuckUuFrDaDPXoGSKl8Or7vvbAg9iYhl+vQnnvMMwNSsT2tUT4QU7fOh7ge5E1pDFsDuCo2EKPpfkHcRKw==",
"requires": {
"@grpc/proto-loader": "0.5.4",
"async": "3.2.0",
"asyncjs-util": "1.2.2",
"bitcoinjs-lib": "5.1.7",
"bn.js": "5.1.1",
"body-parser": "1.19.0",
"bolt07": "1.5.0",
"bolt09": "0.0.2",
"cbor": "5.0.2",
"express": "4.17.1",
"grpc": "1.24.2",
"invoices": "1.0.2",
"is-base64": "1.1.0"
}
}
}
},
@ -3611,9 +3682,9 @@
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q=="
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
"version": "2.25.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.25.1.tgz",
"integrity": "sha512-nRKMf9wDS4Fkyd0C9LXh2FFXinD+iwbJ5p/lh3CHitW9kZbRbJ8hCruiadiIXZVbeAqKZzqcTvHnK3mRhFjb6w=="
},
"morgan": {
"version": "1.10.0",
@ -4204,6 +4275,26 @@
"ipaddr.js": "1.9.1"
}
},
"psbt": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/psbt/-/psbt-1.1.2.tgz",
"integrity": "sha512-f/3Axbk3tD6+HFqbIf3SjpDF+4Wta8HchBeFVrejEalXpAeZa5sdIytAJUAhvbgc4rwj4dhXgYQkOn/TJoFyCA==",
"requires": {
"bip66": "1.1.5",
"bitcoin-ops": "1.4.1",
"bitcoinjs-lib": "5.1.7",
"bn.js": "5.1.1",
"pushdata-bitcoin": "1.0.1",
"varuint-bitcoin": "1.1.2"
},
"dependencies": {
"bn.js": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
"integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA=="
}
}
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",

View file

@ -28,8 +28,9 @@
"ini": "1.3.5",
"inquirer": "7.1.0",
"ln-accounting": "4.1.3",
"ln-service": "48.0.1",
"moment": "2.24.0",
"ln-service": "48.0.3",
"moment": "2.25.1",
"psbt": "1.1.2",
"qrcode-terminal": "0.12.0",
"sanitize-filename": "1.6.3",
"saxophone": "0.6.1",

View file

@ -107,7 +107,7 @@ module.exports = ({cltv, hops, lnd, logger, max}, cbk) => {
return cbk(err);
}
isPayable = isPayable || true;
isPayable = tokens;
return setTimeout(() => {
return cbk(null, res.is_payable);

View file

@ -3,16 +3,12 @@ const {getSwapInQuote} = require('goldengate');
const {getSwapInTerms} = require('goldengate');
const {getSwapOutQuote} = require('goldengate');
const {getSwapOutTerms} = require('goldengate');
const {lightningLabsSwapService} = require('goldengate');
const moment = require('moment');
const {returnResult} = require('asyncjs-util');
const {balanceFromTokens} = require('./../balances');
const {fastDelayMinutes} = require('./constants');
const {feeRateDenominator} = require('./constants');
const {getNetwork} = require('./../network');
const {slowDelayMinutes} = require('./constants');
const {swapTypes} = require('./constants');
/** Get the cost of liquidity via swap

View file

@ -1,6 +1,7 @@
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const asyncMapSeries = require('async/mapSeries');
const asyncRetry = require('async/retry');
const {createInvoice} = require('ln-service');
const {getChannel} = require('ln-service');
const {getChannels} = require('ln-service');
@ -403,7 +404,12 @@ module.exports = (args, cbk) => {
}],
// Calculate maximum amount to rebalance
max: ['getInbound', 'getOutbound', 'tokens', ({getInbound, getOutbound, tokens}, cbk) => {
max: [
'getInbound',
'getOutbound',
'tokens',
({getInbound, getOutbound, tokens}, cbk) =>
{
if (!!args.max_rebalance && !!args.in_outbound) {
return cbk([400, 'CannotSpecifyBothDiscreteAmountAndTargetAmounts']);
}
@ -602,13 +608,20 @@ module.exports = (args, cbk) => {
// Execute the rebalance
pay: ['invoice', 'lnd', 'routes', ({invoice, lnd, routes}, cbk) => {
return payViaRoutes({lnd, routes, id: invoice.id}, (err, res) => {
if (!!err) {
return cbk([503, 'UnexpectedErrExecutingRebalance', {err}]);
}
return asyncRetry({}, cbk => {
return payViaRoutes({lnd, routes, id: invoice.id}, (err, res) => {
if (!!err) {
return cbk([503, 'UnexpectedErrExecutingRebalance', {err}]);
}
return cbk(null, {fee: res.fee, id: invoice.id, tokens: res.tokens});
});
return cbk(null, {
fee: res.fee,
id: invoice.id,
tokens: res.tokens,
});
});
},
cbk);
}],
// Get adjusted inbound liquidity after rebalance
@ -638,8 +651,12 @@ module.exports = (args, cbk) => {
'pay',
({getAdjustedInbound, getAdjustedOutbound, pay}, cbk) =>
{
const inOpeningIn = getAdjustedInbound.inbound_opening;
const inOpeningOut = getAdjustedInbound.outbound_opening;
const inPendingIn = getAdjustedInbound.inbound_pending;
const inPendingOut = getAdjustedInbound.outbound_pending;
const outOpeningIn = getAdjustedOutbound.inbound_opening;
const outOpeningOut = getAdjustedOutbound.outbound_opening;
const outPendingIn = getAdjustedOutbound.inbound_pending;
const outPendingOut = getAdjustedOutbound.outbound_pending;
@ -648,15 +665,19 @@ module.exports = (args, cbk) => {
{
increased_inbound_on: getAdjustedOutbound.alias,
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),
},
{
decreased_inbound_on: getAdjustedInbound.alias,
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),
},
{

View file

@ -40,7 +40,6 @@ const waitForDepositMs = 1000 * 60 * 60 * 24;
{
[api_key]: <API Key CBOR String>
[in_through]: <Request Inbound Payment Public Key Hex String>
[is_free]: <Use Free Service Bool>
[is_refund_test]: <Alter Swap Timeout To Have Short Refund Bool>
lnd: <Authenticated LND gRPC API Object>
logger: <Logger Object>
@ -104,7 +103,7 @@ module.exports = (args, cbk) => {
try {
const {service} = lightningLabsSwapService({
network,
is_free: true,
is_free: false,
});
return cbk(null, service);
@ -184,11 +183,6 @@ module.exports = (args, cbk) => {
return cbk();
}
// Exit early when unpaid service is requested
if (!!args.is_free) {
return cbk(null, {service});
}
return getPaidService({
lnd: args.lnd,
logger: args.logger,

View file

@ -2,6 +2,50 @@ const {test} = require('tap');
const {getBalance} = require('./../../balances');
const makeLnd = ({unconfirmedBalance}) => {
return {
default: {
channelBalance: ({}, cbk) => cbk(null, {
balance: '1',
pending_open_balance: '1',
}),
listChannels: ({}, cbk) => cbk(null, {
channels: [{
active: true,
capacity: '1',
chan_id: 1,
channel_point: '1:1',
commit_fee: 1,
commit_weight: 1,
fee_per_kw: 1,
initiator: true,
local_balance: 1,
local_chan_reserve_sat: '1',
num_updates: 1,
pending_htlcs: [],
private: true,
remote_balance: 1,
remote_chan_reserve_sat: 1,
remote_pubkey: 'b',
total_satoshis_received: 1,
total_satoshis_sent: 1,
unsettled_balance: 1,
}],
}),
pendingChannels: ({}, cbk) => cbk(null, {
pending_closing_channels: [],
pending_force_closing_channels: [],
pending_open_channels: [],
total_limbo_balance: '1',
}),
walletBalance: ({}, cbk) => cbk(null, {
confirmed_balance: '1',
unconfirmed_balance: unconfirmedBalance || '1',
}),
},
};
};
const tests = [
{
args: {},
@ -9,52 +53,20 @@ const tests = [
error: [400, 'ExpectedLndToGetBalance'],
},
{
args: {
lnd: {
default: {
channelBalance: ({}, cbk) => cbk(null, {
balance: '1',
pending_open_balance: '1',
}),
listChannels: ({}, cbk) => cbk(null, {
channels: [{
active: true,
capacity: '1',
chan_id: 1,
channel_point: '1:1',
commit_fee: 1,
commit_weight: 1,
fee_per_kw: 1,
initiator: true,
local_balance: 1,
local_chan_reserve_sat: '1',
num_updates: 1,
pending_htlcs: [],
private: true,
remote_balance: 1,
remote_chan_reserve_sat: 1,
remote_pubkey: 'b',
total_satoshis_received: 1,
total_satoshis_sent: 1,
unsettled_balance: 1,
}],
}),
pendingChannels: ({}, cbk) => cbk(null, {
pending_closing_channels: [],
pending_force_closing_channels: [],
pending_open_channels: [],
total_limbo_balance: '1',
}),
walletBalance: ({}, cbk) => cbk(null, {
confirmed_balance: '1',
unconfirmed_balance: '1',
}),
},
},
},
args: {lnd: makeLnd({})},
description: 'Get balances',
expected: {balance: 3, channel_balance: 0},
},
{
args: {is_offchain_only: true, lnd: makeLnd({})},
description: 'Get balances offchain',
expected: {balance: 1, channel_balance: 0},
},
{
args: {lnd: makeLnd({unconfirmedBalance: '0'})},
description: 'Get balances confirmed',
expected: {balance: 2, channel_balance: 0},
},
];
tests.forEach(({args, description, error, expected}) => {

View file

@ -30,7 +30,7 @@ const tests = [
public_key: Buffer.alloc(33).toString('hex'),
},
description: 'Get peer liquidity',
expected: {alias: 'alias', inbound: 2, outbound: 2},
expected: {alias: 'alias', inbound: 1, outbound: 1},
},
{
args: {
@ -45,7 +45,7 @@ const tests = [
public_key: Buffer.alloc(33).toString('hex'),
},
description: 'Get peer liquidity when node info returns an error',
expected: {alias: '', inbound: 2, outbound: 2},
expected: {alias: '', inbound: 1, outbound: 1},
},
];

View file

@ -0,0 +1,66 @@
const {test} = require('tap');
const peerLiquidity = require('./../../balances/peer_liquidity');
const tests = [
{
args: {
channels: [{
local_balance: 1,
pending_payments: [
{
id: 'id',
is_outgoing: true,
tokens: 1,
},
{
id: 'id',
is_outgoing: false,
tokens: 1,
},
{
id: 'id3',
is_outgoing: true,
tokens: 1,
},
{
id: 'id2',
is_outgoing: false,
tokens: 2,
},
],
remote_balance: 1,
}],
opening: [{
local_balance: 1,
remote_balance: 1,
}],
settled: 'id',
},
description: 'Channels are mapped to liquidity balances',
expected: {
liquidity: {
inbound: 2,
inbound_opening: 1,
inbound_pending: 1,
outbound: 2,
outbound_opening: 1,
outbound_pending: 2,
},
},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, ({deepIs, end, equal, throws}) => {
if (!!error) {
throws(() => peerLiquidity(args), new Error(error));
} else {
const liquidity = peerLiquidity(args);
deepIs(liquidity, expected.liquidity, 'Got expected liquidity');
}
return end();
});
});

View file

@ -0,0 +1,87 @@
const {test} = require('tap');
const {Transaction} = require('bitcoinjs-lib');
const {getRawTransaction} = require('./../../chain');
const makeRequest = ({err, tx}) => {
return ({}, cbk) => cbk(err, null, tx || new Transaction().toHex());
};
const makeArgs = overrides => {
const args = {
id: Buffer.alloc(32).toString('hex'),
interval: 1,
network: 'btc',
request: makeRequest({}),
};
Object.keys(overrides).forEach(k => args[k] = overrides[k]);
return args;
};
const tests = [
{
args: makeArgs({id: undefined}),
description: 'An id is expected',
error: [400, 'ExpectedTransactionIdToGetRawTransaction'],
},
{
args: makeArgs({network: undefined}),
description: 'A network is expected',
error: [400, 'ExpectedNetworkNameToGetRawTransaction'],
},
{
args: makeArgs({network: 'network'}),
description: 'A known network is expected',
error: [400, 'UnsupportedNetworkToGetRawTransaction'],
},
{
args: makeArgs({request: undefined}),
description: 'A request method is expected',
error: [400, 'ExpectedRequestMethodToGetRawTrasaction'],
},
{
args: makeArgs({request: makeRequest({err: 'err'})}),
description: 'Errors are passed back',
error: [503, 'FailedToGetRawTransaction', {err: 'err'}],
},
{
args: makeArgs({request: makeRequest({tx: 'invalid_tx'})}),
description: 'A hex transaction is expected',
error: [503, 'ExpectedTransactionInResponse'],
},
{
args: makeArgs({
interval: undefined,
request: makeRequest({tx: '00'}),
retries: 2,
}),
description: 'A valid transaction is expected',
error: [503, 'ExpectedValidTransactionInResponse'],
},
{
args: makeArgs({}),
description: 'Got raw transaction',
expected: {transaction: '01000000000000000000'},
},
{
args: makeArgs({interval: undefined}),
description: 'Got raw transaction with no retry interval',
expected: {transaction: '01000000000000000000'},
},
];
tests.forEach(({args, description, error, expected}) => {
return test(description, async ({end, equal, rejects}) => {
if (!!error) {
rejects(getRawTransaction(args), error, 'Got expected error');
} else {
const {transaction} = await getRawTransaction(args);
equal(transaction, expected.transaction, 'Got expected transaction');
}
return end();
});
});

View file

@ -108,7 +108,7 @@ const tests = [
alias: 'alias',
fee_earnings: undefined,
last_activity: undefined,
inbound_fee_rate: '0.00%',
inbound_fee_rate: '0.00% (2)',
inbound_liquidity: '0.00000001',
is_offline: true,
outbound_liquidity: '0.00000001',

View file

@ -97,7 +97,7 @@ const tests = [
expected: {
text: [
'🥀 node1',
`0.00000001 channel closed with alias ${pubKey}. Inbound liquidity now: 0.00000002. Outbound liquidity now: 0.00000002.`,
`0.00000001 channel closed with alias ${pubKey}. Inbound liquidity now: 0.00000001. Outbound liquidity now: 0.00000001.`,
],
},
},
@ -107,7 +107,7 @@ const tests = [
expected: {
text: [
'🥀 node1',
`Breach countered on 0.00000001 channel with alias ${pubKey}. Inbound liquidity now: 0.00000002. Outbound liquidity now: 0.00000002.`,
`Breach countered on 0.00000001 channel with alias ${pubKey}. Inbound liquidity now: 0.00000001. Outbound liquidity now: 0.00000001.`,
],
},
},
@ -117,7 +117,7 @@ const tests = [
expected: {
text: [
'🥀 node1',
`Cooperatively closed 0.00000001 channel with alias ${pubKey}. Inbound liquidity now: 0.00000002. Outbound liquidity now: 0.00000002.`,
`Cooperatively closed 0.00000001 channel with alias ${pubKey}. Inbound liquidity now: 0.00000001. Outbound liquidity now: 0.00000001.`,
],
},
},
@ -127,7 +127,7 @@ const tests = [
expected: {
text: [
'🥀 node1',
`Force-closed 0.00000001 channel with alias ${pubKey}. Inbound liquidity now: 0.00000002. Outbound liquidity now: 0.00000002.`,
`Force-closed 0.00000001 channel with alias ${pubKey}. Inbound liquidity now: 0.00000001. Outbound liquidity now: 0.00000001.`,
],
},
},
@ -137,7 +137,7 @@ const tests = [
expected: {
text: [
'🥀 node1',
`0.00000001 channel was force closed by alias ${pubKey}. Inbound liquidity now: 0.00000002. Outbound liquidity now: 0.00000002.`,
`0.00000001 channel was force closed by alias ${pubKey}. Inbound liquidity now: 0.00000001. Outbound liquidity now: 0.00000001.`,
],
},
},

View file

@ -80,7 +80,7 @@ const tests = [
expected: {
text: [
'🌹 node1',
`Accepted new 0.00000001 private channel from alias ${pubKey}. Inbound liquidity now: 0.00000002. Outbound liquidity now: 0.00000002.`,
`Accepted new 0.00000001 private channel from alias ${pubKey}. Inbound liquidity now: 0.00000001. Outbound liquidity now: 0.00000001.`,
],
},
},
@ -90,7 +90,7 @@ const tests = [
expected: {
text: [
'🌹 node1',
`Opened new 0.00000001 channel to alias ${pubKey}. Inbound liquidity now: 0.00000002. Outbound liquidity now: 0.00000002.`,
`Opened new 0.00000001 channel to alias ${pubKey}. Inbound liquidity now: 0.00000001. Outbound liquidity now: 0.00000001.`,
],
},
},