ln-sync
Version:
LN metadata persistence methods
416 lines (368 loc) • 14.9 kB
JavaScript
const asyncAuto = require('async/auto');
const asyncMap = require('async/map');
const {getChainTransactions} = require('lightning/lnd_methods');
const {getChannels} = require('lightning/lnd_methods');
const {getClosedChannels} = require('lightning/lnd_methods');
const {getNode} = require('lightning/lnd_methods');
const {getPendingChannels} = require('lightning/lnd_methods');
const {returnResult} = require('asyncjs-util');
const {Transaction} = require('bitcoinjs-lib');
const transactionRecords = require('./transaction_records');
const {fromHex} = Transaction;
const uniq = arr => Array.from(new Set(arr));
/** Get LND internal record associated with a transaction id
{
[chain_transactions]: [{
[block_id]: <Block Hash String>
[confirmation_count]: <Confirmation Count Number>
[confirmation_height]: <Confirmation Block Height Number>
created_at: <Created ISO 8601 Date String>
[description]: <Transaction Label String>
[fee]: <Fees Paid Tokens Number>
id: <Transaction Id String>
is_confirmed: <Is Confirmed Bool>
is_outgoing: <Transaction Outbound Bool>
output_addresses: [<Address String>]
tokens: <Tokens Including Fee Number>
[transaction]: <Raw Transaction Hex String>
}]
[channels]: [{
capacity: <Capacity Tokens Numberr>
id: <Standard Format Short Channel Id Hex String>
partner_public_key: <Peer Public Key Hex String>
transaction_id: <Channel Transaction Id Hex String>
}]
[closed_channels]: [{
capacity: <Closed Channel Capacity Tokens Number>
[close_balance_spent_by]: <Channel Balance Output Spent By Tx Id String>
[close_balance_vout]: <Channel Balance Close Tx Output Index Number>
[close_confirm_height]: <Channel Close Confirmation Height Number>
close_payments: [{
is_outgoing: <Payment Is Outgoing Bool>
is_paid: <Payment Is Claimed With Preimage Bool>
is_pending: <Payment Resolution Is Pending Bool>
is_refunded: <Payment Timed Out And Went Back To Payer Bool>
[spent_by]: <Close Transaction Spent By Transaction Id Hex String>
tokens: <Associated Tokens Number>
transaction_id: <Transaction Id Hex String>
transaction_vout: <Transaction Output Index Number>
}]
[close_transaction_id]: <Closing Transaction Id Hex String>
final_local_balance: <Channel Close Final Local Balance Tokens Number>
final_time_locked_balance: <Closed Channel Timelocked Tokens Number>
[id]: <Closed Standard Format Channel Id String>
is_breach_close: <Is Breach Close Bool>
is_cooperative_close: <Is Cooperative Close Bool>
is_funding_cancel: <Is Funding Cancelled Close Bool>
is_local_force_close: <Is Local Force Close Bool>
[is_partner_closed]: <Channel Was Closed By Channel Peer Bool>
[is_partner_initiated]: <Channel Was Initiated By Channel Peer Bool>
is_remote_force_close: <Is Remote Force Close Bool>
partner_public_key: <Partner Public Key Hex String>
transaction_id: <Channel Funding Transaction Id Hex String>
transaction_vout: <Channel Funding Output Index Number>
}]
id: <Transaction Id Hex String>
lnd: <Authenticated LND API Object>
[pending_channels]: [{
[close_transaction_id]: <Channel Closing Transaction Id String>
is_active: <Channel Is Active Bool>
is_closing: <Channel Is Closing Bool>
is_opening: <Channel Is Opening Bool>
is_partner_initiated: <Channel Partner Initiated Channel Bool>
local_balance: <Channel Local Tokens Balance Number>
local_reserve: <Channel Local Reserved Tokens Number>
partner_public_key: <Channel Peer Public Key String>
[pending_balance]: <Tokens Pending Recovery Number>
[pending_payments]: [{
is_incoming: <Payment Is Incoming Bool>
timelock_height: <Payment Timelocked Until Height Number>
tokens: <Payment Tokens Number>
transaction_id: <Payment Transaction Id String>
transaction_vout: <Payment Transaction Vout Number>
}]
received: <Tokens Received Number>
[recovered_tokens]: <Tokens Recovered From Close Number>
remote_balance: <Remote Tokens Balance Number>
remote_reserve: <Channel Remote Reserved Tokens Number>
sent: <Send Tokens Number>
[timelock_expiration]: <Pending Tokens Block Height Timelock Number>
[transaction_fee]: <Funding Transaction Fee Tokens Number>
transaction_id: <Channel Funding Transaction Id String>
transaction_vout: <Channel Funding Transaction Vout Number>
[transaction_weight]: <Funding Transaction Weight Number>
}]
}
@returns via cbk or Promise
{
[chain_fee]: <Paid Transaction Fee Tokens Number>
[received]: <Received Tokens Number>
related_channels: [{
action: <Channel Action String>
[balance]: <Channel Balance Tokens Number>
[capacity]: <Channel Capacity Value Number>
[channel]: <Channel Standard Format Id String>
[close_tx]: <Channel Closing Transaction Id Hex String>
[open_tx]: <Channel Opening Transaction id Hex String>
[timelock]: <Channel Funds Timelocked Until Height Number>
with: <Channel Peer Public Key Hex String>
}]
[sent]: <Sent Tokens Number>
[sent_to]: [<Sent to Address String>]
[tx]: <Transaction Id Hex String>
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: async () => {
if (!args.id) {
throw [400, 'ExpectedTransactionIdToFindRecordData'];
}
if (!args.lnd) {
throw [400, 'ExpectedLndToFindChainTransactionRecordData'];
}
return;
},
// Get channels
getChannels: ['validate', ({}, cbk) => {
// Exit early when channels were provided
if (!!args.channels) {
return cbk(null, {channels: args.channels});
}
return getChannels({lnd: args.lnd}, cbk);
}],
// Get closed channels
getClosed: ['validate', ({}, cbk) => {
// Exit early when closed channels were provided
if (!!args.closed_channels) {
return cbk(null, {channels: args.closed_channels});
}
return getClosedChannels({lnd: args.lnd}, cbk);
}],
// Get pending transactions
getPending: ['validate', ({}, cbk) => {
if (!!args.pending_channels) {
return cbk(null, {pending_channels: args.pending_channels});
}
return getPendingChannels({lnd: args.lnd}, cbk);
}],
// Get transactions
getTx: ['validate', ({}, cbk) => {
if (!!args.chain_transactions) {
return cbk(null, {transactions: args.chain_transactions});
}
return getChainTransactions({lnd: args.lnd}, cbk);
}],
// Determine relationship of transaction id to records
record: [
'getChannels',
'getClosed',
'getPending',
'getTx',
async ({getChannels, getClosed, getPending, getTx}) =>
{
const records = [];
const relatedChannels = [];
const chans = getChannels.channels.filter(channel => {
return channel.transaction_id === args.id;
});
const chanClosing = getClosed.channels.find(channel => {
return channel.close_transaction_id === args.id;
});
const closingChans = getClosed.channels.filter(channel => {
return channel.transaction_id === args.id;
});
const openingChans = getPending.pending_channels.filter(channel => {
return channel.is_opening && channel.transaction_id === args.id;
});
const tx = getTx.transactions.find(transaction => {
return transaction.id === args.id;
});
getClosed.channels.forEach(channel => {
return channel.close_payments
.filter(n => n.spent_by === args.id)
.forEach(payment => {
const direction = payment.is_outgoing ? 'outgoing' : 'incoming';
if (!!payment.is_pending) {
return records.push({
action: 'payment_pending',
channel: channel.id,
with: channel.partner_public_key,
});
}
const resolution = payment.is_paid ? 'paid' : 'refunded';
return records.push({
action: `${direction}_payment_${resolution}`,
channel: channel.id,
with: channel.partner_public_key,
});
});
});
openingChans.forEach(channel => {
return records.push({
action: 'opening_channel',
balance: channel.local_balance,
open_tx: channel.transaction_id,
with: channel.partner_public_key,
});
});
if (!!closingChans.length) {
closingChans.forEach(channel => {
return records.push({
action: 'opened_channel',
capacity: channel.capacity,
channel: channel.id,
close_tx: channel.close_transaction_id,
open_tx: channel.transaction_id,
with: channel.partner_public_key,
});
});
}
if (!!chans.length) {
chans.forEach(channel => {
return records.push({
action: 'opened_channel',
capacity: channel.capacity,
channel: channel.id,
open_tx: channel.transaction_id,
with: channel.partner_public_key,
});
});
}
if (!!chanClosing) {
if (chanClosing.is_cooperative_close) {
records.push({
action: 'cooperatively_closed_channel',
balance: chanClosing.final_local_balance,
capacity: chanClosing.capacity || undefined,
channel: chanClosing.id,
close_tx: chanClosing.close_transaction_id,
open_tx: chanClosing.transaction_id,
with: chanClosing.partner_public_key,
});
}
if (!!chanClosing.is_local_force_close) {
records.push({
action: 'force_closed_channel',
balance: chanClosing.final_local_balance,
capacity: chanClosing.capacity || undefined,
channel: chanClosing.id,
close_tx: chanClosing.close_transaction_id,
open_tx: chanClosing.transaction_id,
with: chanClosing.partner_public_key,
});
}
if (!!chanClosing.is_remote_force_close) {
records.push({
action: 'peer_force_closed_channel',
balance: chanClosing.final_local_balance,
capacity: chanClosing.capacity || undefined,
channel: chanClosing.id,
close_tx: chanClosing.close_transaction_id,
open_tx: chanClosing.transaction_id,
with: chanClosing.partner_public_key,
});
}
}
if (!!closingChans.length) {
closingChans.forEach(closing => {
if (!!closing.is_remote_force_close) {
records.push({
action: 'peer_force_closed_channel',
balance: closing.final_local_balance,
capacity: closing.capacity,
channel: closing.id,
close_tx: closing.close_transaction_id,
open_tx: closing.transaction_id,
with: closing.partner_public_key,
});
}
});
}
if (!!tx && !!tx.transaction) {
fromHex(tx.transaction).ins.forEach(({hash, index}) => {
const txRecords = transactionRecords({
ended: getClosed.channels,
id: hash.reverse().toString('hex'),
original: args.id,
pending: getPending.pending_channels,
txs: getTx.transactions,
vout: index,
});
txRecords.records.forEach(record => records.push(record));
});
}
records.forEach(record => {
const {action} = record;
const {channel} = record;
const existing = relatedChannels
.filter(() => !!channel)
.find(n => n.action === action && n.channel === channel);
// Exit early when this related channel action already exists
if (!!existing) {
return;
}
return relatedChannels.push({
action,
balance: record.balance || undefined,
capacity: record.capacity || undefined,
channel: record.channel || undefined,
close_tx: record.close_tx || undefined,
open_tx: record.open_tx || undefined,
timelock: record.timelock || undefined,
with: record.with,
});
});
const hasFee = !!tx && !!tx.fee;
const isIncoming = !!tx && !tx.is_outgoing && !!tx.tokens;
return {
chain_fee: hasFee ? tx.fee : undefined,
received: isIncoming ? tx.tokens : undefined,
related_channels: relatedChannels,
sent: !records.length && !!tx ? tx.tokens : undefined,
sent_to: !records.length && !!tx ? tx.output_addresses : undefined,
tx: !!tx ? tx.id : undefined,
};
}],
// Record details
details: ['record', ({record}, cbk) => {
const keys = uniq(record.related_channels.map(n => n.with));
return asyncMap(keys, (key, cbk) => {
return getNode({
is_omitting_channels: true,
lnd: args.lnd,
public_key: key,
},
(err, res) => {
// Ignore errors
if (!!err || !res.alias) {
return cbk();
}
return cbk(null, {alias: res.alias, public_key: key});
});
},
(err, nodes) => {
if (!!err) {
return cbk(err);
}
const relatedWithAlias = record.related_channels.map(related => {
const node = nodes.find(n => !!n && n.public_key === related.with);
related.node = !!node && !!node.alias ? node.alias : undefined;
return related;
});
return cbk(null, {
chain_fee: record.chain_fee,
received: record.received,
related_channels: relatedWithAlias,
sent: record.sent,
sent_to: record.sent_to,
tx: record.tx,
});
});
}],
},
returnResult({reject, resolve, of: 'details'}, cbk));
});
};