UNPKG

ln-service

Version:

Interaction helper for your Lightning Network daemon

276 lines (221 loc) 11.3 kB
const {exit} = require('node:process'); const {strictEqual} = require('node:assert').strict; const test = require('node:test'); const asyncRetry = require('async/retry'); const {setupChannel} = require('ln-docker-daemons'); const {spawnLightningCluster} = require('ln-docker-daemons'); const {closeChannel} = require('./../../'); const {createHodlInvoice} = require('./../../'); const {getClosedChannels} = require('./../../'); const {getWalletInfo} = require('./../../'); const {settleHodlInvoice} = require('./../../'); const {subscribeToInvoice} = require('./../../'); const {subscribeToPayViaRequest} = require('./../../'); const defaultFee = 1e3; const interval = 125; const maxChanTokens = Math.pow(2, 24) - 1; const size = 2; const times = 1000; // Getting closed channels should return closed channels test(`Get closed channels`, async t => { const {kill, nodes} = await spawnLightningCluster({size}); t.after(() => exit()); const [control, target] = nodes; const {generate, lnd} = control; const channelOpen = await setupChannel({ generate, lnd, capacity: maxChanTokens, partner_csv_delay: 20, to: target, }); const closing = await closeChannel({ lnd, tokens_per_vbyte: defaultFee, transaction_id: channelOpen.transaction_id, transaction_vout: channelOpen.transaction_vout, }); const {features} = await getWalletInfo({lnd}); const isAnchors = !!features.find(n => n.bit === 23); // Wait for channel to close await asyncRetry({interval, times}, async () => { await generate({}); const {channels} = await getClosedChannels({lnd}); if (!channels.length) { throw new Error('ExpectedClosedChannel'); } }); const {channels} = await getClosedChannels({lnd}); const [channel] = channels; strictEqual(channels.length, [channelOpen].length, 'Channel close listed'); const spend = maxChanTokens - channel.final_local_balance; // LND 0.11.1 and below do not use anchors if (isAnchors) { strictEqual([53345, 28473, 2810].includes(spend), true, 'Final'); } else { strictEqual(spend, 9050, 'Final'); } strictEqual(channel.capacity, maxChanTokens, 'Channel capacity reflected'); strictEqual(!!channel.close_confirm_height, true, 'Channel close height'); strictEqual(channel.close_transaction_id, closing.transaction_id, 'Close'); strictEqual(channel.final_time_locked_balance, 0, 'Final locked balance'); strictEqual(!!channel.id, true, 'Channel id'); strictEqual(channel.is_breach_close, false, 'Not breach close'); strictEqual(channel.is_cooperative_close, true, 'Is cooperative close'); strictEqual(channel.is_funding_cancel, false, 'Not funding cancel'); strictEqual(channel.is_local_force_close, false, 'Not local force close'); strictEqual(channel.is_partner_closed, false, 'Partner did not close'); strictEqual(channel.is_partner_initiated, false, 'Partner did not open'); strictEqual(channel.is_remote_force_close, false, 'Not remote force close'); strictEqual(channel.partner_public_key, target.id, 'Pubkey'); strictEqual(channel.transaction_id, channelOpen.transaction_id, 'Channel'); strictEqual(channel.transaction_vout, channelOpen.transaction_vout, 'Vout'); // Setup a force close to show force close channel output const toForceClose = await setupChannel({ generate, lnd, capacity: 7e5, give_tokens: 3e5, partner_csv_delay: 20, to: target, }); const cancelInvoice = await createHodlInvoice({ lnd, cltv_delta: 20, tokens: 1e5, }); const settleInvoice = await createHodlInvoice({ lnd, cltv_delta: 144, tokens: 1e5, }); const subCancelInvoice = subscribeToInvoice({lnd, id: cancelInvoice.id}); const subSettleInvoice = subscribeToInvoice({lnd, id: settleInvoice.id}); const cancelInvoiceUpdates = []; const settleInvoiceUpdates = []; subCancelInvoice.on('invoice_updated', n => cancelInvoiceUpdates.push(n)); subSettleInvoice.on('invoice_updated', n => settleInvoiceUpdates.push(n)); // Kick off a payment to the cancel invoice const payToCancel = subscribeToPayViaRequest({ lnd: target.lnd, request: cancelInvoice.request, }); // Kick off a payment to the settle invoice const payToSettle = subscribeToPayViaRequest({ lnd: target.lnd, request: settleInvoice.request, }); await asyncRetry({interval, times}, async () => { if (!cancelInvoiceUpdates.filter(n => !!n.is_held).length) { throw new Error('WaitingForLockToCancelInvoice'); } if (!settleInvoiceUpdates.filter(n => !!n.is_held).length) { throw new Error('WaitingForLockToSettleInvoice'); } // Push the held HTLCs to chain await closeChannel({ lnd: target.lnd, is_force_close: true, transaction_id: toForceClose.transaction_id, transaction_vout: toForceClose.transaction_vout, }); }); // Use the preimage to sweep the settle invoice on chain await settleHodlInvoice({lnd, secret: settleInvoice.secret}); // LND 0.12.0 requires a delay before sweeps start const deadChans = await asyncRetry({interval: 1000, times: 99}, async () => { const {channels} = await getClosedChannels({lnd}); if (channels.length === [toForceClose, channelOpen].length) { return channels; } await target.generate({}); await generate({}); throw new Error('WaitingForForceClose'); }); const forced = deadChans.find(n => !!n.is_remote_force_close); strictEqual(forced.capacity, 7e5, 'Got force close capacity'); strictEqual(!!forced.close_balance_spent_by, true, 'Got a spend id'); strictEqual(forced.close_balance_vout !== undefined, true, 'Got a vout'); strictEqual(!!forced.close_confirm_height !== undefined, true, 'Height'); strictEqual(forced.close_payments.length, 2, '2 pending payments'); strictEqual(!!forced.close_transaction_id, true, 'Got closed tx id'); strictEqual(!!forced.final_local_balance, true, 'Got final balance'); strictEqual(forced.final_time_locked_balance, 0, 'Got timelock balance'); strictEqual(forced.id, toForceClose.id, 'Got closed channel id'); strictEqual(forced.is_breach_close, false, 'Not a breach'); strictEqual(forced.is_cooperative_close, false, 'Not a coop close'); strictEqual(forced.is_funding_cancel, false, 'Not a cancel'); strictEqual(forced.is_local_force_close, false, 'Not a local force close'); strictEqual(forced.is_partner_closed, true, 'The remote closed'); strictEqual(forced.is_partner_initiated, false, 'Local initiated'); strictEqual(forced.is_remote_force_close, true, 'Remote force closed'); strictEqual(forced.partner_public_key, target.id, 'Got remote public key'); strictEqual(forced.transaction_id, toForceClose.transaction_id, 'Got txid'); strictEqual(forced.transaction_vout, toForceClose.transaction_vout, 'Vout'); const cancelHtlc = forced.close_payments.find(n => !n.is_paid); strictEqual(cancelHtlc.is_outgoing, false, 'HTLC is incoming'); strictEqual(cancelHtlc.is_paid, false, 'HTLC is not settled'); strictEqual(cancelHtlc.is_pending, false, 'HTLC cannot be settled'); strictEqual(cancelHtlc.is_refunded, false, 'HTLC has not been refunded'); strictEqual(cancelHtlc.spent_by, undefined, 'HTLC has no sweep tx'); strictEqual(cancelHtlc.tokens, 1e5, 'HTLC has invoice value'); strictEqual(!!cancelHtlc.transaction_id, true, 'HTLC has tx id'); strictEqual(cancelHtlc.transaction_vout !== undefined, true, 'HTLC vout'); const settleHtlc = forced.close_payments.find(n => !!n.is_paid); strictEqual(settleHtlc.is_outgoing, false, 'Settle is incoming'); strictEqual(settleHtlc.is_paid, true, 'Settle is paid'); strictEqual(settleHtlc.is_pending, false, 'Already settled'); strictEqual(settleHtlc.is_refunded, false, 'No refund available'); strictEqual(!!settleHtlc.spent_by, true, 'Swept with preimage tx'); strictEqual(settleHtlc.tokens, 1e5, 'Settled with invoice value'); strictEqual(!!settleHtlc.transaction_id, true, 'Output tx id'); strictEqual(settleHtlc.transaction_vout !== undefined, true, 'Output vout'); const alsoDead = await asyncRetry({interval: 20, times: 7000}, async () => { const {channels} = await getClosedChannels({lnd: target.lnd}); if (channels.length === [toForceClose, channelOpen].length) { return channels; } await target.generate({}); throw new Error('WaitingForTargetForceClose'); }); const forceClosed = alsoDead.find(n => !!n.is_local_force_close); strictEqual(forceClosed.capacity, 7e5, 'Target capacity reflected'); strictEqual(!!forceClosed.close_balance_spent_by, true, 'Target spent by'); strictEqual(forceClosed.close_balance_vout !== undefined, true, 'Balance'); strictEqual(!!forceClosed.close_confirm_height, true, 'Has confirm height'); strictEqual(!!forceClosed.close_payments.length, true, 'Has close payments'); strictEqual(!!forceClosed.close_transaction_id, true, 'Has close id'); strictEqual(forceClosed.final_local_balance, 1e5, 'Has local balance'); strictEqual(forceClosed.final_time_locked_balance, 3e5, 'Timelock balance'); strictEqual(forceClosed.id, toForceClose.id, 'Has channel id'); strictEqual(forceClosed.is_breach_close, false, 'Not breach close'); strictEqual(forceClosed.is_cooperative_close, false, 'Not coop close'); strictEqual(forceClosed.is_funding_cancel, false, 'Not funding cancel'); strictEqual(forceClosed.is_local_force_close, true, 'Locally forced closed'); strictEqual(forceClosed.is_partner_closed, false, 'Not remote closed'); strictEqual(forceClosed.is_partner_initiated, true, 'Remote create channel'); strictEqual(forceClosed.is_remote_force_close, false, 'Remote not force'); strictEqual(forceClosed.partner_public_key, control.id, 'Got peer key'); strictEqual(forceClosed.transaction_id, toForceClose.transaction_id, 'Tx'); strictEqual(forceClosed.transaction_vout, toForceClose.transaction_vout); const forcePay = forceClosed.close_payments.find(n => !!n.is_paid); strictEqual(forcePay.is_outgoing, true, 'Payment was outgoing'); strictEqual(forcePay.is_paid, true, 'Payment was sent'); strictEqual(forcePay.is_pending, false, 'Payment is settled'); strictEqual(forcePay.is_refunded, false, 'Payment completed successfully'); strictEqual(!!forcePay.spent_by, true, 'Payment was swept with preimage'); strictEqual(forcePay.tokens, 1e5, 'Payment tokens amount'); strictEqual(!!forcePay.transaction_id, true, 'Got payment transaction id'); strictEqual(forcePay.transaction_vout !== undefined, true, 'Got vout'); const refundedHtlc = forceClosed.close_payments.find(n => !!n.is_refunded); strictEqual(refundedHtlc.is_outgoing, true, 'Payment was outgoing'); strictEqual(refundedHtlc.is_paid, false, 'Payment was not paid'); strictEqual(refundedHtlc.is_pending, false, 'Payment is resolved back'); strictEqual(refundedHtlc.is_refunded, true, 'Payment refunded successfully'); strictEqual(!!refundedHtlc.spent_by, true, 'Payment was swept'); strictEqual(refundedHtlc.tokens, 1e5, 'Payment refund tokens amount'); strictEqual(!!refundedHtlc.transaction_id, true, 'Got refund tx id'); strictEqual(refundedHtlc.transaction_vout !== undefined, true, 'Got vout'); await kill({}); return; });