balanceofsatoshis
Version:
Lightning balance CLI
284 lines (230 loc) • 8.72 kB
JavaScript
const {deepEqual} = require('node:assert').strict;
const {equal} = require('node:assert').strict;
const {exit} = require('node:process');
const test = require('node:test');
const asyncAuto = require('async/auto');
const asyncEach = require('async/each');
const asyncRetry = require('async/retry');
const {addPeer} = require('ln-service');
const {createChainAddress} = require('ln-service');
const {fundPsbt} = require('ln-service');
const {getChainBalance} = require('ln-service');
const {getChainTransactions} = require('ln-service');
const {getChannels} = require('ln-service');
const {getHeight} = require('ln-service');
const {getInvoices} = require('ln-service');
const {getNetworkGraph} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {getUtxos} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {openChannel} = require('ln-service');
const {removePeer} = require('ln-service');
const {signPsbt} = require('ln-service');
const {spawnLightningCluster} = require('ln-docker-daemons');
const {stopDaemon} = require('ln-service');
const {openBalancedChannel} = require('./../../services');
const capacity = 1e6;
const findAddress = n => n.match(/to\ (.*)\ or/)[1];
const giveTokens = 1e6 / 2;
const info = () => {};
const interval = 10;
const many = 20000;
const maturityBlocks = 100;
const rate = 1;
const size = 2;
const slow = 100;
const times = 2000;
const tokens = 500095;
// Opening a balanced channel with a peer should open a balanced channel
test(`Open balanced channel`, async t => {
t.after(() => exit());
const {kill, nodes} = await spawnLightningCluster({size});
const [{generate, lnd}, target] = nodes;
// Do the open balanced channel dance
try {
// Generate some coins for each side
await asyncEach([generate, target.generate], async generate => {
return await generate({count: maturityBlocks});
});
const {address} = await createChainAddress({lnd});
// Create a channel between the two nodes so they can communicate over LN
await openChannel({
lnd,
give_tokens: giveTokens,
local_tokens: capacity,
partner_public_key: target.id,
partner_socket: target.socket,
});
// Make sure the channel is open and the wallet is sync'ed to chain
await asyncEach([lnd, target.lnd], async lnd => {
await asyncRetry(({interval, times}), async () => {
await generate({});
await target.generate({});
const {channels} = await getChannels({lnd});
if (!channels.length) {
throw new Error('ExpectedChannel');
}
const [channel] = channels;
if (!channel.is_active) {
throw new Error('ExpectedActiveChannel');
}
const wallet = await getWalletInfo({lnd});
if (!wallet.is_synced_to_chain) {
throw new Error('ExpectedWalletSyncToChain');
}
});
});
// Make sure that the nodes see each other in the graph to see TLV support
await asyncRetry(({interval, times}), async () => {
const graph = await getNetworkGraph({lnd});
// Force graph resync in case it gets stuck
try {
await removePeer({lnd, public_key: target.id});
} catch (err) {}
await addPeer({lnd, public_key: target.id, socket: target.socket});
if (graph.nodes.length < [lnd, target].length) {
throw new Error('ExpectedGraphNodes');
}
});
// Make sure nodes are on the same chain hash and the channels are active
await asyncRetry(({interval, times}), async () => {
const controlChain = await getHeight({lnd});
const targetChain = await getHeight({lnd: target.lnd});
if (controlChain.current_block_hash !== targetChain.current_block_hash) {
throw new Error('ExpectedSyncChain');
}
await addPeer({lnd, public_key: target.id, socket: target.socket});
const [channel] = (await getChannels({lnd: target.lnd})).channels;
if (!channel.is_active) {
throw new Error('ExpectedActiveChannelForSetup');
}
});
// Get UTXOs to be spent into the balanced channel
const controlUtxos = (await getUtxos({lnd})).utxos;
const {utxos} = await getUtxos({lnd: target.lnd});
await asyncAuto({
// Start proposing the channel
initiate: async () => {
return await openBalancedChannel({
lnd,
address,
ask: (args, cbk) => {
// Propose the capacity
if (args.name === 'capacity') {
return cbk({capacity});
}
// Provide funding
if (args.name === 'fund') {
// Scrape the address out of the query
const address = findAddress(args.message);
const [utxo] = controlUtxos;
// Use an old UTXO to fund the PSBT
return fundPsbt({
lnd,
inputs: [utxo],
outputs: [{address, tokens}],
},
(err, res) => {
if (!!err) {
throw err;
}
const {psbt} = res;
return signPsbt({lnd, psbt}, (err, res) => {
if (!!err) {
throw err;
}
return cbk({fund: res.transaction});
});
});
}
// Use external funding
if (args.name === 'internal') {
return cbk({internal: false});
}
// Make channel to target
if (args.name === 'key') {
return cbk({key: target.id});
}
// Use standard fee rate
if (args.name === 'rate') {
return cbk({rate});
}
throw new Error('UnknownAskQuery');
},
logger: {info, error: err => { throw err; }},
});
},
// Wait for an incoming request on target
waitForRequest: async () => {
// The request will appear as a push invoice
return await asyncRetry(({interval, times}), async () => {
const {invoices} = await getInvoices({lnd: target.lnd});
if (!invoices.length) {
throw new Error('WaitingForProposal');
}
return;
});
},
// Accept the balanced channel request
acceptRequest: ['waitForRequest', async () => {
return await openBalancedChannel({
lnd: target.lnd,
ask: (args, cbk) => {
// Accept the proposal
if (args.name === 'accept') {
return cbk({accept: true});
}
// Provide for funding
if (args.name === 'fund') {
const address = findAddress(args.message);
const [utxo] = utxos;
return fundPsbt({
inputs: [utxo],
lnd: target.lnd,
outputs: [{address, tokens}],
},
(err, res) => {
const {psbt} = res;
return signPsbt({psbt, lnd: target.lnd}, (err, res) => {
return cbk({fund: res.transaction});
});
});
}
// Use external funding
if (args.name === 'internal') {
return cbk({internal: false});
}
throw new Error('UnknownAskQuery');
},
logger: {info, error: err => { throw err; }},
});
}],
// Drive the chain forward so that the channel confirms
waitForChannel: ['waitForRequest', async () => {
return await asyncRetry(({interval: slow, times: many}), async () => {
const {channels} = await getChannels({lnd});
const opening = (await getPendingChannels({lnd})).pending_channels;
if (!!opening.length || channels.length === [lnd, target.length]) {
await generate({});
}
// Make sure the new channel is active
if (channels.filter(n => n.is_active).length < [lnd, lnd].length) {
throw new Error('WaitingForMoreChannels');
}
const addresses = channels.map(n => n.cooperative_close_address);
const given = channels.map(n => n.local_given);
addresses.sort();
given.sort();
deepEqual(addresses, [address, undefined], 'Got coop close addrs');
deepEqual(given, [500000, 500000], 'Got coins given');
return;
});
}],
});
} catch (err) {
equal(err, null, 'Expected no error opening a balanced channel');
} finally {
await kill({});
}
return;
});