mirror of
https://github.com/alexbosworth/balanceofsatoshis.git
synced 2026-08-13 12:33:37 +02:00
add support for external, multi-channel opening
This commit is contained in:
parent
96b258a162
commit
5379e71422
24 changed files with 1121 additions and 158 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}],
|
||||
},
|
||||
|
|
|
|||
85
balances/peer_liquidity.js
Normal file
85
balances/peer_liquidity.js
Normal 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
27
bos
|
|
@ -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
113
chain/get_address_utxo.js
Normal 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));
|
||||
});
|
||||
};
|
||||
84
chain/get_raw_transaction.js
Normal file
84
chain/get_raw_transaction.js
Normal 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));
|
||||
});
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
432
network/open_channels.js
Normal 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
105
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}) => {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
66
test/balances/test_peer_liquidity.js
Normal file
66
test/balances/test_peer_liquidity.js
Normal 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();
|
||||
});
|
||||
});
|
||||
87
test/chain/test_get_raw_transaction.js
Normal file
87
test/chain/test_get_raw_transaction.js
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue