Merge branch 'master' into nymkappa/bugfix/missing-data-node-page

This commit is contained in:
wiz 2022-08-05 07:55:11 +00:00 committed by GitHub
commit f7cbe30a16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 208 additions and 102 deletions

View File

@ -116,7 +116,7 @@ class NodesApi {
const latestDate = rows[0].maxAdded; const latestDate = rows[0].maxAdded;
const query = ` const query = `
SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels
FROM node_stats FROM node_stats
JOIN nodes ON nodes.public_key = node_stats.public_key JOIN nodes ON nodes.public_key = node_stats.public_key
WHERE added = FROM_UNIXTIME(${latestDate}) WHERE added = FROM_UNIXTIME(${latestDate})
@ -138,7 +138,7 @@ class NodesApi {
const latestDate = rows[0].maxAdded; const latestDate = rows[0].maxAdded;
const query = ` const query = `
SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels
FROM node_stats FROM node_stats
JOIN nodes ON nodes.public_key = node_stats.public_key JOIN nodes ON nodes.public_key = node_stats.public_key
WHERE added = FROM_UNIXTIME(${latestDate}) WHERE added = FROM_UNIXTIME(${latestDate})

View File

@ -1,5 +1,6 @@
import { ILightningApi } from '../lightning-api.interface'; import { ILightningApi } from '../lightning-api.interface';
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher'; import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
import logger from '../../../logger';
/** /**
* Convert a clightning "listnode" entry to a lnd node entry * Convert a clightning "listnode" entry to a lnd node entry
@ -23,12 +24,17 @@ export function convertNode(clNode: any): ILightningApi.Node {
/** /**
* Convert clightning "listchannels" response to lnd "describegraph.edges" format * Convert clightning "listchannels" response to lnd "describegraph.edges" format
*/ */
export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise<ILightningApi.Channel[]> { export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise<ILightningApi.Channel[]> {
logger.info('Converting clightning nodes and channels to lnd graph format');
let loggerTimer = new Date().getTime() / 1000;
let channelProcessed = 0;
const consolidatedChannelList: ILightningApi.Channel[] = []; const consolidatedChannelList: ILightningApi.Channel[] = [];
const clChannelsDict = {}; const clChannelsDict = {};
const clChannelsDictCount = {}; const clChannelsDictCount = {};
for (const clChannel of clChannels) { for (const clChannel of clChannels) {
if (!clChannelsDict[clChannel.short_channel_id]) { if (!clChannelsDict[clChannel.short_channel_id]) {
clChannelsDict[clChannel.short_channel_id] = clChannel; clChannelsDict[clChannel.short_channel_id] = clChannel;
clChannelsDictCount[clChannel.short_channel_id] = 1; clChannelsDictCount[clChannel.short_channel_id] = 1;
@ -39,9 +45,26 @@ export function convertNode(clNode: any): ILightningApi.Node {
delete clChannelsDict[clChannel.short_channel_id]; delete clChannelsDict[clChannel.short_channel_id];
clChannelsDictCount[clChannel.short_channel_id]++; clChannelsDictCount[clChannel.short_channel_id]++;
} }
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`);
loggerTimer = new Date().getTime() / 1000;
}
++channelProcessed;
} }
for (const short_channel_id of Object.keys(clChannelsDict)) {
channelProcessed = 0;
const keys = Object.keys(clChannelsDict);
for (const short_channel_id of keys) {
consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id])); consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id]));
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
loggerTimer = new Date().getTime() / 1000;
}
} }
return consolidatedChannelList; return consolidatedChannelList;
@ -79,7 +102,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format * Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy of only one node * In this case, clightning knows the channel policy of only one node
*/ */
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> { async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> {
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id); const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
const parts = clChannel.short_channel_id.split('x'); const parts = clChannel.short_channel_id.split('x');
const outputIdx = parts[2]; const outputIdx = parts[2];
@ -99,7 +122,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
/** /**
* Convert a clightning "listnode" response to a lnd channel policy format * Convert a clightning "listnode" response to a lnd channel policy format
*/ */
function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy { function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy {
return { return {
time_lock_delta: 0, // TODO time_lock_delta: 0, // TODO
min_htlc: clChannel.htlc_minimum_msat.slice(0, -4), min_htlc: clChannel.htlc_minimum_msat.slice(0, -4),

View File

@ -32,6 +32,7 @@ interface IConfig {
ENABLED: boolean; ENABLED: boolean;
BACKEND: 'lnd' | 'cln' | 'ldk'; BACKEND: 'lnd' | 'cln' | 'ldk';
TOPOLOGY_FOLDER: string; TOPOLOGY_FOLDER: string;
NODE_STATS_REFRESH_INTERVAL: number;
}; };
LND: { LND: {
TLS_CERT_PATH: string; TLS_CERT_PATH: string;
@ -183,6 +184,7 @@ const defaults: IConfig = {
'ENABLED': false, 'ENABLED': false,
'BACKEND': 'lnd', 'BACKEND': 'lnd',
'TOPOLOGY_FOLDER': '', 'TOPOLOGY_FOLDER': '',
'NODE_STATS_REFRESH_INTERVAL': 600,
}, },
'LND': { 'LND': {
'TLS_CERT_PATH': '', 'TLS_CERT_PATH': '',

View File

@ -137,9 +137,7 @@ class Server {
} }
if (config.LIGHTNING.ENABLED) { if (config.LIGHTNING.ENABLED) {
fundingTxFetcher.$init() this.$runLightningBackend();
.then(() => networkSyncService.$startService())
.then(() => lightningStatsUpdater.$startService());
} }
this.server.listen(config.MEMPOOL.HTTP_PORT, () => { this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
@ -185,6 +183,18 @@ class Server {
} }
} }
async $runLightningBackend() {
try {
await fundingTxFetcher.$init();
await networkSyncService.$startService();
await lightningStatsUpdater.$startService();
} catch(e) {
logger.err(`Lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
await Common.sleep$(1000 * 60);
this.$runLightningBackend();
};
}
setUpWebsocketHandling() { setUpWebsocketHandling() {
if (this.wss) { if (this.wss) {
websocketHandler.setWebsocketServer(this.wss); websocketHandler.setWebsocketServer(this.wss);

View File

@ -1,25 +1,15 @@
import DB from '../../database';
import logger from '../../logger'; import logger from '../../logger';
import lightningApi from '../../api/lightning/lightning-api-factory'; import lightningApi from '../../api/lightning/lightning-api-factory';
import LightningStatsImporter from './sync-tasks/stats-importer'; import LightningStatsImporter from './sync-tasks/stats-importer';
import config from '../../config';
class LightningStatsUpdater { class LightningStatsUpdater {
hardCodedStartTime = '2018-01-12';
public async $startService(): Promise<void> { public async $startService(): Promise<void> {
logger.info('Starting Lightning Stats service'); logger.info('Starting Lightning Stats service');
LightningStatsImporter.$run(); // LightningStatsImporter.$run();
this.$runTasks();
setTimeout(() => {
this.$runTasks();
}, this.timeUntilMidnight());
}
private timeUntilMidnight(): number {
const date = new Date();
this.setDateMidnight(date);
date.setUTCHours(24);
return date.getTime() - new Date().getTime();
} }
private setDateMidnight(date: Date): void { private setDateMidnight(date: Date): void {
@ -34,17 +24,20 @@ class LightningStatsUpdater {
setTimeout(() => { setTimeout(() => {
this.$runTasks(); this.$runTasks();
}, this.timeUntilMidnight()); }, 1000 * config.LIGHTNING.NODE_STATS_REFRESH_INTERVAL);
} }
/**
* Update the latest entry for each node every config.LIGHTNING.NODE_STATS_REFRESH_INTERVAL seconds
*/
private async $logStatsDaily(): Promise<void> { private async $logStatsDaily(): Promise<void> {
const date = new Date(); const date = new Date();
this.setDateMidnight(date); this.setDateMidnight(date);
date.setUTCHours(24); date.setUTCHours(24);
logger.info(`Running lightning daily stats log...`); logger.info(`Updating latest node stats`);
const networkGraph = await lightningApi.$getNetworkGraph(); const networkGraph = await lightningApi.$getNetworkGraph();
LightningStatsImporter.computeNetworkStats(date.getTime(), networkGraph); LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
} }
} }

View File

@ -45,7 +45,7 @@ class FundingTxFetcher {
let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 10) { if (elapsedSeconds > 10) {
elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer); elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer);
logger.debug(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` + logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` +
`(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` + `(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` +
`elapsed: ${elapsedSeconds} seconds` `elapsed: ${elapsedSeconds} seconds`
); );

View File

@ -13,19 +13,19 @@ interface Node {
features: string; features: string;
rgb_color: string; rgb_color: string;
alias: string; alias: string;
addresses: string; addresses: unknown[];
out_degree: number; out_degree: number;
in_degree: number; in_degree: number;
} }
interface Channel { interface Channel {
scid: string; channel_id: string;
source: string; node1_pub: string;
destination: string; node2_pub: string;
timestamp: number; timestamp: number;
features: string; features: string;
fee_base_msat: number; fee_base_msat: number;
fee_proportional_millionths: number; fee_rate_milli_msat: number;
htlc_minimim_msat: number; htlc_minimim_msat: number;
cltv_expiry_delta: number; cltv_expiry_delta: number;
htlc_maximum_msat: number; htlc_maximum_msat: number;
@ -41,7 +41,7 @@ class LightningStatsImporter {
const [channels]: any[] = await DB.query('SELECT short_id from channels;'); const [channels]: any[] = await DB.query('SELECT short_id from channels;');
logger.info('Caching funding txs for currently existing channels'); logger.info('Caching funding txs for currently existing channels');
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
await this.$importHistoricalLightningStats(); await this.$importHistoricalLightningStats();
} }
@ -60,10 +60,9 @@ class LightningStatsImporter {
let hasClearnet = false; let hasClearnet = false;
let isUnnanounced = true; let isUnnanounced = true;
const sockets = node.addresses.split(','); for (const socket of (node.addresses ?? [])) {
for (const socket of sockets) { hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network);
hasOnion = hasOnion || (socket.indexOf('torv3://') !== -1); hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network);
hasClearnet = hasClearnet || (socket.indexOf('ipv4://') !== -1 || socket.indexOf('ipv6://') !== -1);
} }
if (hasOnion && hasClearnet) { if (hasOnion && hasClearnet) {
clearnetTorNodes++; clearnetTorNodes++;
@ -90,8 +89,11 @@ class LightningStatsImporter {
const baseFees: number[] = []; const baseFees: number[] = [];
const alreadyCountedChannels = {}; const alreadyCountedChannels = {};
for (const channel of networkGraph.channels) { for (const channel of networkGraph.edges) {
const short_id = channel.scid.slice(0, -2); let short_id = channel.channel_id;
if (short_id.indexOf('/') !== -1) {
short_id = short_id.slice(0, -2);
}
const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id); const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id);
if (!tx) { if (!tx) {
@ -99,65 +101,93 @@ class LightningStatsImporter {
continue; continue;
} }
if (!nodeStats[channel.source]) { if (!nodeStats[channel.node1_pub]) {
nodeStats[channel.source] = { nodeStats[channel.node1_pub] = {
capacity: 0, capacity: 0,
channels: 0, channels: 0,
}; };
} }
if (!nodeStats[channel.destination]) { if (!nodeStats[channel.node2_pub]) {
nodeStats[channel.destination] = { nodeStats[channel.node2_pub] = {
capacity: 0, capacity: 0,
channels: 0, channels: 0,
}; };
} }
nodeStats[channel.source].capacity += Math.round(tx.value * 100000000);
nodeStats[channel.source].channels++;
nodeStats[channel.destination].capacity += Math.round(tx.value * 100000000);
nodeStats[channel.destination].channels++;
if (!alreadyCountedChannels[short_id]) { if (!alreadyCountedChannels[short_id]) {
capacity += Math.round(tx.value * 100000000); capacity += Math.round(tx.value * 100000000);
capacities.push(Math.round(tx.value * 100000000)); capacities.push(Math.round(tx.value * 100000000));
alreadyCountedChannels[short_id] = true; alreadyCountedChannels[short_id] = true;
nodeStats[channel.node1_pub].capacity += Math.round(tx.value * 100000000);
nodeStats[channel.node1_pub].channels++;
nodeStats[channel.node2_pub].capacity += Math.round(tx.value * 100000000);
nodeStats[channel.node2_pub].channels++;
} }
if (channel.fee_proportional_millionths < 5000) { if (channel.node1_policy !== undefined) { // Coming from the node
avgFeeRate += channel.fee_proportional_millionths; for (const policy of [channel.node1_policy, channel.node2_policy]) {
feeRates.push(channel.fee_proportional_millionths); if (policy && policy.fee_rate_milli_msat < 5000) {
} avgFeeRate += policy.fee_rate_milli_msat;
feeRates.push(policy.fee_rate_milli_msat);
if (channel.fee_base_msat < 5000) { }
avgBaseFee += channel.fee_base_msat; if (policy && policy.fee_base_msat < 5000) {
baseFees.push(channel.fee_base_msat); avgBaseFee += policy.fee_base_msat;
baseFees.push(policy.fee_base_msat);
}
}
} else { // Coming from the historical import
if (channel.fee_rate_milli_msat < 5000) {
avgFeeRate += channel.fee_rate_milli_msat;
feeRates.push(channel.fee_rate_milli_msat);
}
if (channel.fee_base_msat < 5000) {
avgBaseFee += channel.fee_base_msat;
baseFees.push(channel.fee_base_msat);
}
} }
} }
avgFeeRate /= networkGraph.channels.length; avgFeeRate /= networkGraph.edges.length;
avgBaseFee /= networkGraph.channels.length; avgBaseFee /= networkGraph.edges.length;
const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)]; const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)];
const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)]; const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)];
const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)]; const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)];
const avgCapacity = Math.round(capacity / capacities.length); const avgCapacity = Math.round(capacity / capacities.length);
let query = `INSERT INTO lightning_stats( let query = `INSERT INTO lightning_stats(
added, added,
channel_count, channel_count,
node_count, node_count,
total_capacity, total_capacity,
tor_nodes, tor_nodes,
clearnet_nodes, clearnet_nodes,
unannounced_nodes, unannounced_nodes,
clearnet_tor_nodes, clearnet_tor_nodes,
avg_capacity, avg_capacity,
avg_fee_rate, avg_fee_rate,
avg_base_fee_mtokens, avg_base_fee_mtokens,
med_capacity, med_capacity,
med_fee_rate, med_fee_rate,
med_base_fee_mtokens med_base_fee_mtokens
) )
VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
added = FROM_UNIXTIME(?),
channel_count = ?,
node_count = ?,
total_capacity = ?,
tor_nodes = ?,
clearnet_nodes = ?,
unannounced_nodes = ?,
clearnet_tor_nodes = ?,
avg_capacity = ?,
avg_fee_rate = ?,
avg_base_fee_mtokens = ?,
med_capacity = ?,
med_fee_rate = ?,
med_base_fee_mtokens = ?
`;
await DB.query(query, [ await DB.query(query, [
timestamp, timestamp,
@ -174,22 +204,44 @@ class LightningStatsImporter {
medCapacity, medCapacity,
medFeeRate, medFeeRate,
medBaseFee, medBaseFee,
timestamp,
capacities.length,
networkGraph.nodes.length,
capacity,
torNodes,
clearnetNodes,
unannouncedNodes,
clearnetTorNodes,
avgCapacity,
avgFeeRate,
avgBaseFee,
medCapacity,
medFeeRate,
medBaseFee,
]); ]);
for (const public_key of Object.keys(nodeStats)) { for (const public_key of Object.keys(nodeStats)) {
query = `INSERT INTO node_stats( query = `INSERT INTO node_stats(
public_key, public_key,
added, added,
capacity, capacity,
channels channels
) )
VALUES (?, FROM_UNIXTIME(?), ?, ?)`; VALUES (?, FROM_UNIXTIME(?), ?, ?)
ON DUPLICATE KEY UPDATE
added = FROM_UNIXTIME(?),
capacity = ?,
channels = ?
`;
await DB.query(query, [ await DB.query(query, [
public_key, public_key,
timestamp, timestamp,
nodeStats[public_key].capacity, nodeStats[public_key].capacity,
nodeStats[public_key].channels, nodeStats[public_key].channels,
timestamp,
nodeStats[public_key].capacity,
nodeStats[public_key].channels,
]); ]);
} }
@ -203,15 +255,28 @@ class LightningStatsImporter {
let latestNodeCount = 1; let latestNodeCount = 1;
const fileList = await fsPromises.readdir(this.topologiesFolder); const fileList = await fsPromises.readdir(this.topologiesFolder);
// Insert history from the most recent to the oldest
// This also put the .json cached files first
fileList.sort().reverse(); fileList.sort().reverse();
const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) as added, node_count FROM lightning_stats'); const [rows]: any[] = await DB.query(`
SELECT UNIX_TIMESTAMP(added) AS added, node_count
FROM lightning_stats
ORDER BY added DESC
`);
const existingStatsTimestamps = {}; const existingStatsTimestamps = {};
for (const row of rows) { for (const row of rows) {
existingStatsTimestamps[row.added] = rows[0]; existingStatsTimestamps[row.added] = row;
} }
// For logging purpose
let processed = 10;
let totalProcessed = -1;
for (const filename of fileList) { for (const filename of fileList) {
processed++;
totalProcessed++;
const timestamp = parseInt(filename.split('_')[1], 10); const timestamp = parseInt(filename.split('_')[1], 10);
// Stats exist already, don't calculate/insert them // Stats exist already, don't calculate/insert them
@ -220,7 +285,7 @@ class LightningStatsImporter {
continue; continue;
} }
logger.debug(`Processing ${this.topologiesFolder}/${filename}`); logger.debug(`Reading ${this.topologiesFolder}/${filename}`);
const fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8'); const fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8');
let graph; let graph;
@ -228,12 +293,13 @@ class LightningStatsImporter {
try { try {
graph = JSON.parse(fileContent); graph = JSON.parse(fileContent);
} catch (e) { } catch (e) {
logger.debug(`Invalid topology file, cannot parse the content`); logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
continue;
} }
} else { } else {
graph = this.parseFile(fileContent); graph = this.parseFile(fileContent);
if (!graph) { if (!graph) {
logger.debug(`Invalid topology file, cannot parse the content`); logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
continue; continue;
} }
await fsPromises.writeFile(`${this.topologiesFolder}/${filename}.json`, JSON.stringify(graph)); await fsPromises.writeFile(`${this.topologiesFolder}/${filename}.json`, JSON.stringify(graph));
@ -245,19 +311,22 @@ class LightningStatsImporter {
const diffRatio = graph.nodes.length / latestNodeCount; const diffRatio = graph.nodes.length / latestNodeCount;
if (diffRatio < 0.9) { if (diffRatio < 0.9) {
// Ignore drop of more than 90% of the node count as it's probably a missing data point // Ignore drop of more than 90% of the node count as it's probably a missing data point
logger.debug(`Nodes count diff ratio threshold reached, ignore the data for this day ${graph.nodes.length} nodes vs ${latestNodeCount}`);
continue; continue;
} }
} }
latestNodeCount = graph.nodes.length; latestNodeCount = graph.nodes.length;
const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`;
logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.channels.length} channels`); logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`);
// Cache funding txs if (processed > 10) {
logger.debug(`Caching funding txs for ${datestr}`); logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.channels.map(channel => channel.scid.slice(0, -2))); processed = 0;
} else {
logger.debug(`Generating LN network stats for ${datestr}`); logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
}
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
const stat = await this.computeNetworkStats(timestamp, graph); const stat = await this.computeNetworkStats(timestamp, graph);
existingStatsTimestamps[timestamp] = stat; existingStatsTimestamps[timestamp] = stat;
@ -290,13 +359,22 @@ class LightningStatsImporter {
if (!node.data) { if (!node.data) {
continue; continue;
} }
const addresses: unknown[] = [];
const sockets = node.data[5].split(',');
for (const socket of sockets) {
const parts = socket.split('://');
addresses.push({
network: parts[0],
addr: parts[1],
});
}
nodes.push({ nodes.push({
id: node.data[0], id: node.data[0],
timestamp: node.data[1], timestamp: node.data[1],
features: node.data[2], features: node.data[2],
rgb_color: node.data[3], rgb_color: node.data[3],
alias: node.data[4], alias: node.data[4],
addresses: node.data[5], addresses: addresses,
out_degree: node.data[6], out_degree: node.data[6],
in_degree: node.data[7], in_degree: node.data[7],
}); });
@ -307,13 +385,13 @@ class LightningStatsImporter {
continue; continue;
} }
channels.push({ channels.push({
scid: channel.data[0], channel_id: channel.data[0],
source: channel.data[1], node1_pub: channel.data[1],
destination: channel.data[2], node2_pub: channel.data[2],
timestamp: channel.data[3], timestamp: channel.data[3],
features: channel.data[4], features: channel.data[4],
fee_base_msat: channel.data[5], fee_base_msat: channel.data[5],
fee_proportional_millionths: channel.data[6], fee_rate_milli_msat: channel.data[6],
htlc_minimim_msat: channel.data[7], htlc_minimim_msat: channel.data[7],
cltv_expiry_delta: channel.data[8], cltv_expiry_delta: channel.data[8],
htlc_maximum_msat: channel.data[9], htlc_maximum_msat: channel.data[9],
@ -322,9 +400,9 @@ class LightningStatsImporter {
return { return {
nodes: nodes, nodes: nodes,
channels: channels, edges: channels,
}; };
} }
} }
export default new LightningStatsImporter; export default new LightningStatsImporter;