ln-accounting
Version:
lnd accounting reports
431 lines (374 loc) • 12.5 kB
JavaScript
const asyncAuto = require('async/auto');
const asyncRetry = require('async/retry');
const {getChannels} = require('ln-service');
const {getClosedChannels} = require('ln-service');
const {getForwards} = require('ln-service');
const {getIdentity} = require('ln-service');
const {getPayments} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {categories} = require('./../harmony');
const {categorizeRecords} = require('./../harmony');
const {chainFeesAsRecords} = require('./../harmony');
const {chainReceivesAsRecords} = require('./../harmony');
const {chainSendsAsRecords} = require('./../harmony');
const {forwardsAsRecords} = require('./../harmony');
const {getAllInvoices} = require('./../records');
const {getAllPayments} = require('./../records');
const {getChainTransactions} = require('./../records');
const {getFiatValues} = require('./../fiat');
const {harmonize} = require('./../harmony');
const {invoicesAsRecords} = require('./../harmony');
const {paymentsAsRecords} = require('./../harmony');
const {recordsWithFiat} = require('./../harmony');
const {types} = require('./../harmony');
const earlyStartDate = '2017-08-24T08:57:37.000Z';
const interval = retryCount => Math.random() * 5000 * Math.pow(2, retryCount);
const largeLimit = 1e8;
const times = 10;
/** Get an accounting summary of wallet
Note: Chain fees does not include chain fees paid to close channels
{
[after]: <Records Created After ISO 8601 Date String>
[before]: <Records Created Before ISO 8601 Date String>
[category]: <Category Filter String>
currency: <Base Currency Type String>
[fiat]: <Fiat Currency Type String>
lnd: <LND gRPC Object>
[network]: <Network Name String>
[rate]: <Exchange Function> ({currency, date, fiat}, cbk) => (err, {cents})
[rate_provider]: <Fiat Rate Provider String> // coindesk || coingecko
request: <Request Function>
}
@returns via cbk or Promise
{
[chain_fees]: [{
[amount]: <Amount Number>
[asset]: <Asset Type String>
[created_at]: <ISO 8601 Date String>
[external_id]: <External Reference Id String>
[from_id]: <Source Id String>
[id]: <Record Id String>
[notes]: <Notes String>
[to_id]: <Destination Id String>
[type]: <Record Type String>
}]
[chain_fees_csv]: <CSV String>
[chain_sends]: [{
[amount]: <Amount Number>
[asset]: <Asset Type String>
[created_at]: <ISO 8601 Date String>
[external_id]: <External Reference Id String>
[from_id]: <Source Id String>
[id]: <Record Id String>
[notes]: <Notes String>
[to_id]: <Destination Id String>
[type]: <Record Type String>
}]
[chain_sends_csv]: <CSV String>
[forwards]: [{
[amount]: <Amount Number>
[asset]: <Asset Type String>
[created_at]: <ISO 8601 Date String>
[external_id]: <External Reference Id String>
[from_id]: <Source Id String>
[id]: <Record Id String>
[notes]: <Notes String>
[to_id]: <Destination Id String>
[type]: <Record Type String>
}]
[forwards_csv]: <CSV String>
[invoices]: [{
[amount]: <Amount Number>
[asset]: <Asset Type String>
[created_at]: <ISO 8601 Date String>
[external_id]: <External Reference Id String>
[from_id]: <Source Id String>
[id]: <Record Id String>
[notes]: <Notes String>
[to_id]: <Destination Id String>
[type]: <Record Type String>
}]
[invoices_csv]: <CSV String>
[payments]: [{
[amount]: <Amount Number>
[asset]: <Asset Type String>
[created_at]: <ISO 8601 Date String>
[external_id]: <External Reference Id String>
[from_id]: <Source Id String>
[id]: <Record Id String>
[notes]: <Notes String>
[to_id]: <Destination Id String>
[type]: <Record Type String>
}]
[payments_csv]: <CSV String>
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.currency) {
return cbk([400, 'ExpectedNativeCurrencyAssetType']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedLndToGetAccountingReport']);
}
if (!!args.rate && typeof args.rate !== 'function') {
return cbk([400, 'ExpectedRateFunctionForAccountingReport']);
}
if (!args.request) {
return cbk([400, 'ExpectedRequestFunctionToGetAccountingReport']);
}
return cbk();
},
// Get transactions on the blockchain
getChainTx: ['validate', ({}, cbk) => {
// Exit early when accounting for forwards, no need for chain tx info
if (args.category === categories.forwards) {
return cbk(null, {transactions: []});
}
return getChainTransactions({
after: args.after,
before: args.before,
lnd: args.lnd,
network: args.network,
request: args.request,
},
cbk);
}],
// Get channels
getChannels: ['validate', ({}, cbk) => {
if (args.category === categories.forwards) {
return cbk(null, {channels: []});
}
return getChannels({lnd: args.lnd}, cbk);
}],
// Get closed channels
getClosedChans: ['validate', ({}, cbk) => {
if (args.category === categories.forwards) {
return cbk(null, {channels: []});
}
return getClosedChannels({lnd: args.lnd}, cbk)
}],
// Get routing forwards
getForwards: ['validate', ({}, cbk) => {
if (!!args.category && args.category !== categories.forwards) {
return cbk();
}
return getForwards({
after: earlyStartDate,
before: new Date().toISOString(),
limit: largeLimit,
lnd: args.lnd,
},
cbk);
}],
// Get invoices
getInvoices: ['validate', ({}, cbk) => {
if (!!args.category && args.category !== categories.invoices) {
return cbk();
}
// Since there is no way to page by settle date, get all the invoices
return getAllInvoices({
after: args.after,
before: args.before,
lnd: args.lnd,
},
cbk);
}],
// Get payments
getPayments: ['validate', ({}, cbk) => {
if (!!args.category && args.category !== categories.payments) {
return cbk();
}
return getAllPayments({after: args.after, lnd: args.lnd}, cbk);
}],
// Get pending channels
getPending: ['validate', ({}, cbk) => {
if (args.category === categories.forwards) {
return cbk(null, {pending_channels: []});
}
return asyncRetry({interval, times}, cbk => {
return getPendingChannels({lnd: args.lnd}, cbk);
},
cbk);
}],
// Get public key
getPublicKey: ['validate', ({}, cbk) => {
return getIdentity({lnd: args.lnd}, cbk);
}],
// Forward records
forwards: ['getForwards', ({getForwards}, cbk) => {
if (!getForwards) {
return cbk(null, []);
}
const {forwards} = getForwards;
try {
return cbk(null, forwardsAsRecords({forwards}).records);
} catch (err) {
return cbk([503, 'FailedToMapForwardsToAccountingRecords', {err}]);
}
}],
// Invoice records
invoices: ['getInvoices', ({getInvoices}, cbk) => {
if (!getInvoices) {
return cbk(null, []);
}
const {invoices} = getInvoices;
try {
return cbk(null, invoicesAsRecords({invoices}).records);
} catch (err) {
return cbk([503, 'FailedToMapInvoicesToAccountingRecords', {err}]);
}
}],
// Payment records
payments: [
'getPayments',
'getPublicKey',
({getPayments, getPublicKey}, cbk) =>
{
if (!getPayments) {
return cbk(null, []);
}
const {records} = paymentsAsRecords({
payments: getPayments.payments,
public_key: getPublicKey.public_key,
});
return cbk(null, records);
}],
// Chain fees
chainFees: ['getChainTx', ({getChainTx}, cbk) => {
if (!getChainTx) {
return cbk(null, []);
}
const {transactions} = getChainTx;
const {records} = chainFeesAsRecords({transactions});
return cbk(null, records);
}],
// Channel transaction ids
channelTxIds: [
'getChannels',
'getClosedChans',
'getPending',
({getChannels, getClosedChans, getPending}, cbk) =>
{
const channels = []
.concat(getClosedChans.channels)
.concat(getPending.pending_channels)
.concat(getChannels.channels);
const closeIds = channels.map(n => n.close_transaction_id);
const transactionIds = channels.map(n => n.transaction_id);
const ids = [].concat(closeIds).concat(transactionIds).map(n => !!n);
return cbk(null, ids);
}],
// Chain receive records
chainReceives: [
'channelTxIds',
'getChainTx',
({channelTxIds, getChainTx}, cbk) =>
{
try {
const {records} = chainReceivesAsRecords({
channel_transaction_ids: channelTxIds,
transactions: getChainTx.transactions,
});
return cbk(null, records);
} catch (err) {
return cbk([503, 'FailedToMapChainReceivesToRecords', {err}]);
}
}],
// Chain send records
chainSends: [
'channelTxIds',
'getChainTx',
({channelTxIds, getChainTx}, cbk) =>
{
try {
const {records} = chainSendsAsRecords({
channel_transaction_ids: channelTxIds,
transactions: getChainTx.transactions,
});
return cbk(null, records);
} catch (err) {
return cbk([503, 'FailedToMapChainSendsToRecords', {err}]);
}
}],
// All relevant records
records: [
'chainFees',
'chainReceives',
'chainSends',
'forwards',
'invoices',
'payments',
({
chainFees,
chainReceives,
chainSends,
forwards,
invoices,
payments,
}, cbk) =>
{
const records = []
.concat(chainFees || [])
.concat(chainReceives || [])
.concat(chainSends || [])
.concat(forwards || [])
.concat(invoices || [])
.concat(payments || []);
const temporalRecords = records.filter(record => {
if (!!args.after && record.created_at < args.after) {
return false;
}
if (!!args.before && record.created_at > args.before) {
return false;
}
return true;
});
return cbk(null, temporalRecords);
}],
// Fiat values for records
getFiatValues: ['records', ({records}, cbk) => {
// Exit early when there is no fiat specified
if (!args.fiat) {
return cbk(null, {rates: []});
}
return getFiatValues({
currency: args.currency,
dates: records.map(n => n.created_at),
fiat: args.fiat,
provider: args.rate_provider,
rate: args.rate,
request: args.request,
},
cbk);
}],
// Records with fiat amounts
recordsWithFiat: [
'getFiatValues',
'records',
({getFiatValues, records}, cbk) =>
{
const {currency} = args;
const fiat = getFiatValues.rates;
try {
return cbk(null, recordsWithFiat({currency, fiat, records}).records);
} catch (err) {
return cbk([500, 'FailedToAddHistoricFiatValuesToRecords', {err}]);
}
}],
// Report
report: ['recordsWithFiat', ({recordsWithFiat}, cbk) => {
try {
return cbk(null, categorizeRecords({records: recordsWithFiat}));
} catch (err) {
return cbk([500, 'FailedToCategorizeRecords']);
}
}],
},
returnResult({reject, resolve, of: 'report'}, cbk));
});
};