ln-sync
Version:
LN metadata persistence methods
207 lines (182 loc) • 7.47 kB
JavaScript
const conflictingBalances = require('./conflicting_balances');
const addressTypePayToTaproot = 'p2tr';
const anchorOutputAmount = 330;
const {ceil} = Math;
const flatten = arr => [].concat(...arr);
const inputsCounterVBytesLength = 3;
const inputSequenceVByteLength = 4;
const isAnchor = n => !!n && n.startsWith('anchor');
const nestedPublicKeyAddressType = 'np2wpkh';
const nestedPublicKeyVByteLength = 22;
const outputCounterVBytesLength = 1;
const outputValueVBytesLength = 8;
const outputScriptCounterVBytesLength = 1;
const outputScriptVBytesLength = 34;
const schnorrSignatureVByteLength = 16;
const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
const transactionIdVByteLength = 32;
const transactionLockTimeVBytesLength = 4;
const transactionOutputIndexVByteLength = 4;
const transactionVersionVBytesLength = 4;
const witnessElementsCounterVByteLength = 0.25;
const witnessPublicKeySizeVByteLength = 0.25;
const witnessPublicKeyVByteLength = 8.25;
const witnessSignatureVByteLength = 18;
const witnessSizeCounterVByteLength = 0.25;
/** Determine balances from components
{
channels: [{
commit_transaction_fee: <Commitment Transaction Fee Tokens Number>
is_partner_initiated: <Partner Responsible For Chain Fees Bool>
local_balance: <Local Channel Balance Tokens Number>
pending_payments: [{
is_outgoing: <Payment is Sending Out Bool>
tokens: <Payment Size Tokens Number>
}]
}]
locked: [{
tokens: <Unspent Tokens Number>
}]
pending: [{
is_opening: <Channel is Pending Bool>
is_partner_initiated: <Partner Responsible For Chain Fees Bool>
local_balance: <Channel Local Balance Tokens Number>
[pending_payments]: [{
is_outgoing: <Payment is Sending Out Bool>
tokens: <Payment Size Tokens Number>
}]
[recovered_tokens]: <Already Recovered Balance Tokens Number>
[transaction_fee]: <Commitment Transaction Fee Tokens Number>
}]
transactions: [{
is_confirmed: <Is Confirmed Bool>
is_outgoing: <Transaction Outbound Bool>
output_addresses: [<Address String>]
}]
utxos: [{
address: <Chain Address String>
address_format: <Chain Address Format String>
confirmation_count: <Confirmation Count Number>
tokens: <Unspent Tokens Number>
}]
}
@returns
{
closing_balance: <Balance of Tokens Moving Out Of Channels Tokens Number>
conflicted_pending: <Conflicting Pending Balance Tokens Number>
invalid_pending: <Invalid Pending Balance Tokens Number>
offchain_balance: <Balance of Owned Tokens In Channels Tokens Number>
offchain_pending: <Total Pending Local Balance Tokens Number>
onchain_balance: <Balance of Transaction Outputs Number>
onchain_vbytes: <Estimated Virtual Bytes to Spend On-Chain Funds Number>
}
*/
module.exports = ({channels, locked, pending, transactions, utxos}) => {
const channelBalances = channels.map(n => n.local_balance);
const confirmedUtxos = utxos.filter(n => !!n.confirmation_count);
// Unconfirmed addresses in outgoing transactions
const outgoingAddresses = flatten(transactions
.filter(n => !n.is_confirmed && n.is_outgoing)
.map(n => n.output_addresses));
// Unconfirmed change UTXOs
const changeUtxos = utxos
.filter(n => !n.confirmation_count)
.filter(n => outgoingAddresses.includes(n.address));
// Calculate the local tokens that are still in the process of opening
const opening = pending
.filter(n => n.is_partner_initiated === false && n.is_opening)
.map(n => n.local_balance);
// Calculate the balances coming back in closing
const closing = pending
.filter(n => n.is_closing)
.map(n => n.local_balance - (n.recovered_tokens || Number()));
// For in-flight payments assume refund/timeout resolutions will happen
const channelHtlcs = channels.map(chan => {
return chan.pending_payments.filter(n => n.is_outgoing).map(n => n.tokens);
});
// Some in-flight payments will be in on-chain HTLCs
const pendingHtlcs = pending
.map(chan => {
return (chan.pending_payments || [])
.filter(n => n.is_outgoing)
.map(n => n.tokens);
});
// Initiator commitment fees are deducted from channel local balances
const commitFees = channels
.filter(n => n.is_partner_initiated === false)
.map(n => n.commit_transaction_fee);
// Initiator anchor outputs are deducted from the channel local balance
const anchorOutputs = channels
.filter(n => n.is_partner_initiated === false)
.filter(n => isAnchor(n.type))
.map(n => anchorOutputAmount);
// Pending channels also have commit transaction fees
const pendingCommitFees = pending
.filter(n => n.is_opening && n.is_partner_initiated === false)
.map(n => n.transaction_fee || Number());
// Total balance to consider owned on-chain
const chainBalance = sumOf([]
.concat(confirmedUtxos)
.concat(changeUtxos)
.concat(locked)
.map(n => n.tokens)
);
// Input element virtual bytes
const inputElements = flatten([]
.concat(confirmedUtxos)
.concat(changeUtxos)
.map(utxo => {
const inputData = []
.concat(transactionIdVByteLength) // Previous outpoint tx id
.concat(transactionOutputIndexVByteLength) // Previous tx out index
.concat(inputSequenceVByteLength) // Input sequence number
.concat(witnessElementsCounterVByteLength) // Witness elements count
.concat(witnessSizeCounterVByteLength); // Witness sig size counter
// Exit early when the UTXO uses P2TR and only needs a signature push
if (utxo.address_format === addressTypePayToTaproot) {
return inputData.concat(schnorrSignatureVByteLength);
}
const inputV0Data = inputData
.concat(witnessSignatureVByteLength) // ECDSA witness signature
.concat(witnessPublicKeySizeVByteLength) // Public key size counter
.concat(witnessPublicKeyVByteLength); // Witness public key
// Exit early with nested data
if (utxo.address_format === nestedPublicKeyAddressType) {
return inputV0Data.concat(nestedPublicKeyVByteLength);
}
return inputV0Data;
}));
// Total balance to consider owned in channels
const channelBalance = sumOf(flatten([]
.concat(channelBalances)
.concat(commitFees)
.concat(anchorOutputs)
.concat(flatten(channelHtlcs))
));
// Total pending balance to consider owned
const pendingBalance = sumOf(flatten([]
.concat(opening)
.concat(pendingCommitFees)
.concat(flatten(pendingHtlcs))
));
// Estimate the virtual bytes size of a tx that spends all inputs
const vbyteComponents = !inputElements.length ? [] : flatten([]
.concat(transactionVersionVBytesLength)
.concat(inputsCounterVBytesLength)
.concat(outputCounterVBytesLength)
.concat(outputValueVBytesLength)
.concat(outputScriptCounterVBytesLength)
.concat(outputScriptVBytesLength)
.concat(transactionLockTimeVBytesLength)
.concat(inputElements));
const conflicts = conflictingBalances({transactions, utxos});
return {
closing_balance: sumOf(closing),
conflicted_pending: conflicts.conflicting_pending_balance,
invalid_pending: conflicts.invalid_pending_balance,
offchain_balance: channelBalance,
offchain_pending: pendingBalance,
onchain_balance: chainBalance,
onchain_vbytes: ceil(sumOf(vbyteComponents)),
};
};