ln-service
Version:
Interaction helper for your Lightning Network daemon
283 lines (213 loc) • 8.51 kB
JavaScript
const {deepEqual} = require('node:assert').strict;
const {equal} = require('node:assert').strict;
const {exit} = require('node:process');
const {match} = require('node:assert').strict;
const test = require('node:test');
const asyncRetry = require('async/retry');
const {spawnLightningCluster} = require('ln-docker-daemons');
const {addPeer} = require('./../../');
const {broadcastChainTransaction} = require('./../../');
const {closeChannel} = require('./../../');
const {createInvoice} = require('./../../');
const {fundPendingChannels} = require('./../../');
const {fundPsbt} = require('./../../');
const {getChannels} = require('./../../');
const {getChannel} = require('./../../');
const {getClosedChannels} = require('./../../');
const {getEphemeralChannelIds} = require('./../../');
const {getPendingChannels} = require('./../../');
const {getWalletInfo} = require('./../../');
const {openChannels} = require('./../../');
const {pay} = require('./../../');
const {signPsbt} = require('./../../');
const {subscribeToChannels} = require('./../../');
const {subscribeToOpenRequests} = require('./../../');
const capacity = 1e6;
const interval = 10;
const maturity = 100;
const size = 2;
const times = 4000;
// Opening unconfirmed channels should in immediate channel opening
test(`Open unconfirmed channels`, async () => {
// Unconfirmed channels are not supported on LND 0.15.0 and below
{
const {kill, nodes} = await spawnLightningCluster({});
const [{lnd}] = nodes;
try {
await getEphemeralChannelIds({lnd});
await kill({});
} catch (err) {
deepEqual(err, [501, 'ListAliasesMethodNotSupported']);
await kill({});
return;
}
}
const {kill, nodes} = await spawnLightningCluster({
size,
lnd_configuration: [
'--maxpendingchannels=10',
'--protocol.option-scid-alias',
'--protocol.zero-conf',
],
});
const [{generate, lnd}, target] = nodes;
try {
// Make some funds to use
await generate({count: maturity});
await asyncRetry({interval, times}, async () => {
const wallet = await getWalletInfo({lnd});
await generate({});
if (!wallet.is_synced_to_chain) {
throw new Error('NotSyncedToChain');
}
});
// Connect to the peer
await addPeer({lnd, public_key: target.id, socket: target.socket});
// Wait for channel to be receptive to opens
await asyncRetry({interval, times}, async () => {
return await openChannels({
lnd,
channels: [{capacity, partner_public_key: target.id}],
});
});
// Wait for channel open requests to be able to accept the proposal
const acceptSub = subscribeToOpenRequests({lnd: target.lnd});
// Listen for channel events
const channelsSub = subscribeToChannels({lnd});
// Channel closing and opening events should be emitted
const closings = [];
const opened = [];
channelsSub.on('channel_closed', update => closings.push(update));
channelsSub.on('channel_opened', update => opened.push(update));
acceptSub.on('channel_request', request => {
equal(request.is_trusted_funding, true, 'Request is trusted funding');
return request.accept({is_trusted_funding: true});
});
// Propose the channel to the peer
const {pending} = await asyncRetry({interval, times}, async () => {
return await openChannels({
lnd,
channels: [{
capacity,
is_trusted_funding: true,
partner_public_key: target.id,
}],
});
});
// Setup funding to the 2:2 output
const fundTarget = await fundPsbt({lnd, outputs: pending});
// Sign the funding to the 2:2 output
const {psbt, transaction} = await signPsbt({lnd, psbt: fundTarget.psbt});
// Fund the channel
await fundPendingChannels({
lnd,
channels: pending.map(({id}) => id),
funding: psbt,
});
// Make an invoice to pay over the channel
const invoice = await createInvoice({lnd: target.lnd, tokens: 100});
// Wait for the trusted channel
const channel = await asyncRetry({interval, times}, async () => {
const {channels} = await getChannels({lnd, is_active: true});
if (!channels.length) {
throw new Error('ExpectedTrustedChannelToAppear');
}
const [channel] = channels;
return channel;
});
// Make sure the channel reflects that it is trusted funding
deepEqual(channel.other_ids, [], 'Got no ephemeral ids');
match(channel.id, /16000000x0/, 'Channel id is faked');
equal(channel.is_trusted_funding, true, 'Channel funding is trusted');
// Make sure the open channel event reflected that it was trusted funding
const [event] = opened;
deepEqual(event.other_ids, [], 'Got no event ephemeral ids');
match(event.id, /16000000x0/, 'Channel event id is faked');
equal(event.is_trusted_funding, true, 'Channel event funding is trusted');
// Make sure the channel can be used immediately
await asyncRetry({interval, times}, async () => {
return await pay({lnd, request: invoice.request});
});
// Generate the channel into a block
await broadcastChainTransaction({lnd, transaction});
// Make sure that the channel id gets a confirmed id
const confirmed = await asyncRetry({interval, times}, async () => {
await generate({});
const [confirmed] = (await getChannels({lnd})).channels;
if (confirmed.id === channel.id) {
throw new Error('ExpectedChangeToRealChannelId');
}
return confirmed;
});
match(confirmed.id, /1[\d][\d]x1x0/, 'Channel id is real now');
equal(confirmed.is_trusted_funding, true, 'Channel funding was trusted');
equal(confirmed.other_ids.length, 1, 'Got ephemeral id');
const [otherId] = confirmed.other_ids;
match(otherId, /16000000x0/, 'Got ephemeral id');
// Make sure the channel is really active
await asyncRetry({interval, times}, async () => {
const {channels} = await getChannels({lnd});
const [channel] = channels;
if (!channel.time_online) {
throw new Error('ExpectedChannelOnline');
}
});
// Remove the channel
await asyncRetry({interval, times}, async () => {
await closeChannel({lnd, id: confirmed.id});
});
// Propose a private channel to the peer
const openPrivate = await asyncRetry({interval, times}, async () => {
return await openChannels({
lnd,
channels: [{
capacity,
is_private: true,
is_trusted_funding: true,
partner_public_key: target.id,
}],
});
});
// Setup funding to the 2:2 output for the private channel
const fundPrivate = await fundPsbt({lnd, outputs: openPrivate.pending});
// Sign the private funding
const signedPrivate = await signPsbt({lnd, psbt: fundPrivate.psbt});
// Fund the private channel
await fundPendingChannels({
lnd,
channels: openPrivate.pending.map(({id}) => id),
funding: signedPrivate.psbt,
});
// Confirm the private channel
const privateConfirmed = await asyncRetry({interval, times}, async () => {
// Generate the channel into a block
await broadcastChainTransaction({
lnd,
transaction: signedPrivate.transaction,
});
await generate({});
const [confirmed] = (await getChannels({lnd})).channels;
const shut = await getClosedChannels({lnd});
if (!shut.channels.length) {
throw new Error('ExpectedClosedChannel');
}
return confirmed;
});
const [closedChannel] = (await getClosedChannels({lnd})).channels;
equal(closedChannel.id, confirmed.id, 'Closed channel shows confirmed id');
deepEqual(closedChannel.other_ids, confirmed.other_ids, 'Saved temp id');
const [closeEvent] = closings;
equal(closeEvent.id, confirmed.id, 'Closed event shows confirmed id');
deepEqual(closeEvent.other_ids, confirmed.other_ids, 'Got temporary id');
const ephemeralIds = await getEphemeralChannelIds({lnd});
equal(ephemeralIds.channels.length, 2, 'Got ephemeral channel ids');
const [firstChannel, secondChannel] = ephemeralIds.channels;
match(firstChannel.reference_id, /16000000x0/, 'Got first channel id');
match(secondChannel.reference_id, /16000000x0/, 'Got second channel id');
await kill({});
} catch (err) {
await kill({});
throw err;
}
return;
});