2022-07-06 13:20:37 +02:00
import DB from '../../database' ;
import logger from '../../logger' ;
import lightningApi from '../../api/lightning/lightning-api-factory' ;
2022-07-06 14:56:10 +02:00
import channelsApi from '../../api/explorer/channels.api' ;
2022-07-06 13:20:37 +02:00
import * as net from 'net' ;
2022-04-19 17:37:06 +04:00
class LightningStatsUpdater {
constructor ( ) { }
2022-07-04 12:00:16 +02:00
public async $startService() {
2022-07-06 21:43:47 +02:00
logger . info ( 'Starting Lightning Stats service' ) ;
let isInSync = false ;
let error : any ;
try {
error = null ;
isInSync = await this . $lightningIsSynced ( ) ;
} catch ( e ) {
error = e ;
}
if ( ! isInSync ) {
if ( error ) {
logger . warn ( 'Was not able to fetch Lightning Node status: ' + ( error instanceof Error ? error.message : error ) + '. Retrying in 1 minute...' ) ;
} else {
logger . notice ( 'The Lightning graph is not yet in sync. Retrying in 1 minute...' ) ;
}
setTimeout ( ( ) = > this . $startService ( ) , 60 * 1000 ) ;
return ;
}
2022-04-19 17:37:06 +04:00
const now = new Date ( ) ;
const nextHourInterval = new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) , Math . floor ( now . getHours ( ) / 1 ) + 1 , 0 , 0 , 0 ) ;
const difference = nextHourInterval . getTime ( ) - now . getTime ( ) ;
2022-07-06 21:43:47 +02:00
setTimeout ( ( ) = > {
2022-07-04 12:00:16 +02:00
setInterval ( async ( ) = > {
await this . $runTasks ( ) ;
2022-04-19 17:37:06 +04:00
} , 1000 * 60 * 60 ) ;
2022-07-06 21:43:47 +02:00
} , difference ) ;
2022-05-01 03:01:27 +04:00
2022-07-04 12:00:16 +02:00
await this . $runTasks ( ) ;
}
2022-07-06 21:43:47 +02:00
private async $lightningIsSynced ( ) : Promise < boolean > {
const nodeInfo = await lightningApi . $getInfo ( ) ;
return nodeInfo . is_synced_to_chain && nodeInfo . is_synced_to_graph ;
}
2022-07-04 12:00:16 +02:00
private async $runTasks() {
await this . $populateHistoricalData ( ) ;
await this . $logLightningStatsDaily ( ) ;
await this . $logNodeStatsDaily ( ) ;
2022-04-19 17:37:06 +04:00
}
2022-04-27 02:52:23 +04:00
private async $logNodeStatsDaily() {
2022-04-29 03:57:27 +04:00
const currentDate = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
try {
const [ state ] : any = await DB . query ( ` SELECT string FROM state WHERE name = 'last_node_stats' ` ) ;
// Only store once per day
2022-05-01 03:01:27 +04:00
if ( state [ 0 ] . string === currentDate ) {
2022-04-29 03:57:27 +04:00
return ;
}
2022-04-19 17:37:06 +04:00
2022-07-06 21:43:47 +02:00
logger . info ( ` Running daily node stats update... ` ) ;
2022-05-16 00:01:53 +04:00
const query = ` SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels WHERE channels.status < 2 GROUP BY node1_public_key) c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN (SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right FROM channels WHERE channels.status < 2 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key ` ;
2022-04-29 03:57:27 +04:00
const [ nodes ] : any = await DB . query ( query ) ;
2022-05-08 16:16:54 +04:00
// First run we won't have any nodes yet
if ( nodes . length < 10 ) {
return ;
}
2022-04-29 03:57:27 +04:00
for ( const node of nodes ) {
await DB . query (
2022-05-08 16:16:54 +04:00
` INSERT INTO node_stats(public_key, added, capacity, channels) VALUES (?, NOW(), ?, ?) ` ,
2022-05-05 23:19:24 +04:00
[ node . public_key , ( parseInt ( node . channels_capacity_left || 0 , 10 ) ) + ( parseInt ( node . channels_capacity_right || 0 , 10 ) ) ,
node . channels_count_left + node . channels_count_right ] ) ;
2022-04-29 03:57:27 +04:00
}
await DB . query ( ` UPDATE state SET string = ? WHERE name = 'last_node_stats' ` , [ currentDate ] ) ;
2022-07-06 11:58:06 +02:00
logger . info ( 'Daily node stats has updated.' ) ;
2022-04-29 03:57:27 +04:00
} catch ( e ) {
logger . err ( '$logNodeStatsDaily() error: ' + ( e instanceof Error ? e.message : e ) ) ;
2022-04-27 02:52:23 +04:00
}
}
2022-07-04 12:00:16 +02:00
// We only run this on first launch
private async $populateHistoricalData() {
const startTime = '2018-01-13' ;
2022-04-19 17:37:06 +04:00
try {
2022-07-06 11:58:06 +02:00
const [ rows ] : any = await DB . query ( ` SELECT COUNT(*) FROM lightning_stats ` ) ;
2022-07-04 12:00:16 +02:00
// Only store once per day
if ( rows [ 0 ] [ 'COUNT(*)' ] > 0 ) {
return ;
}
2022-07-06 21:43:47 +02:00
logger . info ( ` Running historical stats population... ` ) ;
2022-07-04 12:00:16 +02:00
const [ channels ] : any = await DB . query ( ` SELECT capacity, created, closing_date FROM channels ORDER BY created ASC ` ) ;
let date : Date = new Date ( startTime ) ;
const currentDate = new Date ( ) ;
while ( date < currentDate ) {
let totalCapacity = 0 ;
let channelsCount = 0 ;
for ( const channel of channels ) {
if ( new Date ( channel . created ) > date ) {
break ;
}
if ( channel . closing_date !== null && new Date ( channel . closing_date ) < date ) {
continue ;
}
totalCapacity += channel . capacity ;
channelsCount ++ ;
}
2022-07-06 11:58:06 +02:00
const query = ` INSERT INTO lightning_stats(
2022-07-04 12:00:16 +02:00
added ,
channel_count ,
node_count ,
2022-07-06 13:20:37 +02:00
total_capacity ,
tor_nodes ,
clearnet_nodes ,
unannounced_nodes
2022-07-04 12:00:16 +02:00
)
2022-07-06 13:20:37 +02:00
VALUES ( FROM_UNIXTIME ( ? ) , ? , ? , ? , ? , ? , ? ) ` ;
2022-07-04 12:00:16 +02:00
2022-07-06 14:56:10 +02:00
await DB . query ( query , [
date . getTime ( ) / 1000 ,
channelsCount ,
0 ,
totalCapacity ,
0 ,
0 ,
0
] ) ;
2022-07-04 12:00:16 +02:00
// Add one day and continue
date . setDate ( date . getDate ( ) + 1 ) ;
}
2022-07-06 13:20:37 +02:00
const [ nodes ] : any = await DB . query ( ` SELECT first_seen, sockets FROM nodes ORDER BY first_seen ASC ` ) ;
2022-07-04 12:00:16 +02:00
date = new Date ( startTime ) ;
while ( date < currentDate ) {
let nodeCount = 0 ;
2022-07-06 13:20:37 +02:00
let clearnetNodes = 0 ;
let torNodes = 0 ;
let unannouncedNodes = 0 ;
2022-07-04 12:00:16 +02:00
for ( const node of nodes ) {
if ( new Date ( node . first_seen ) > date ) {
break ;
}
nodeCount ++ ;
2022-07-06 13:20:37 +02:00
const sockets = node . sockets . split ( ',' ) ;
let isUnnanounced = true ;
for ( const socket of sockets ) {
const hasOnion = socket . indexOf ( '.onion' ) !== - 1 ;
if ( hasOnion ) {
torNodes ++ ;
isUnnanounced = false ;
}
const hasClearnet = [ 4 , 6 ] . includes ( net . isIP ( socket . split ( ':' ) [ 0 ] ) ) ;
if ( hasClearnet ) {
clearnetNodes ++ ;
isUnnanounced = false ;
}
}
if ( isUnnanounced ) {
unannouncedNodes ++ ;
}
2022-07-04 12:00:16 +02:00
}
2022-07-06 13:20:37 +02:00
const query = ` UPDATE lightning_stats SET node_count = ?, tor_nodes = ?, clearnet_nodes = ?, unannounced_nodes = ? WHERE added = FROM_UNIXTIME(?) ` ;
2022-07-04 12:00:16 +02:00
await DB . query ( query , [
nodeCount ,
2022-07-06 13:20:37 +02:00
torNodes ,
clearnetNodes ,
unannouncedNodes ,
2022-07-04 12:00:16 +02:00
date . getTime ( ) / 1000 ,
] ) ;
// Add one day and continue
date . setDate ( date . getDate ( ) + 1 ) ;
}
2022-07-06 11:58:06 +02:00
logger . info ( 'Historical stats populated.' ) ;
2022-07-04 12:00:16 +02:00
} catch ( e ) {
logger . err ( '$populateHistoricalData() error: ' + ( e instanceof Error ? e.message : e ) ) ;
}
}
private async $logLightningStatsDaily() {
const currentDate = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
try {
const [ state ] : any = await DB . query ( ` SELECT string FROM state WHERE name = 'last_node_stats' ` ) ;
// Only store once per day
if ( state [ 0 ] . string === currentDate ) {
return ;
}
2022-07-06 21:43:47 +02:00
logger . info ( ` Running lightning daily stats log... ` ) ;
2022-05-01 03:01:27 +04:00
const networkGraph = await lightningApi . $getNetworkGraph ( ) ;
let total_capacity = 0 ;
for ( const channel of networkGraph . channels ) {
if ( channel . capacity ) {
total_capacity += channel . capacity ;
}
}
2022-04-27 02:52:23 +04:00
2022-07-06 13:20:37 +02:00
let clearnetNodes = 0 ;
let torNodes = 0 ;
let unannouncedNodes = 0 ;
for ( const node of networkGraph . nodes ) {
let isUnnanounced = true ;
for ( const socket of node . sockets ) {
const hasOnion = socket . indexOf ( '.onion' ) !== - 1 ;
if ( hasOnion ) {
torNodes ++ ;
isUnnanounced = false ;
}
const hasClearnet = [ 4 , 6 ] . includes ( net . isIP ( socket . split ( ':' ) [ 0 ] ) ) ;
if ( hasClearnet ) {
clearnetNodes ++ ;
isUnnanounced = false ;
}
}
if ( isUnnanounced ) {
unannouncedNodes ++ ;
}
}
2022-07-06 14:56:10 +02:00
const channelStats = await channelsApi . $getChannelsStats ( ) ;
2022-07-06 11:58:06 +02:00
const query = ` INSERT INTO lightning_stats(
2022-04-19 17:37:06 +04:00
added ,
channel_count ,
node_count ,
2022-07-06 13:20:37 +02:00
total_capacity ,
tor_nodes ,
clearnet_nodes ,
2022-07-06 14:56:10 +02:00
unannounced_nodes ,
avg_capacity ,
avg_fee_rate ,
avg_base_fee_mtokens ,
med_capacity ,
med_fee_rate ,
med_base_fee_mtokens
2022-04-19 17:37:06 +04:00
)
2022-07-10 17:24:43 +02:00
VALUES ( NOW ( ) , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) ` ;
2022-04-19 17:37:06 +04:00
await DB . query ( query , [
2022-05-01 03:01:27 +04:00
networkGraph . channels . length ,
networkGraph . nodes . length ,
total_capacity ,
2022-07-06 13:20:37 +02:00
torNodes ,
clearnetNodes ,
2022-07-06 14:56:10 +02:00
unannouncedNodes ,
channelStats . avgCapacity ,
channelStats . avgFeeRate ,
channelStats . avgBaseFee ,
channelStats . medianCapacity ,
channelStats . medianFeeRate ,
channelStats . medianBaseFee ,
2022-04-19 17:37:06 +04:00
] ) ;
2022-07-06 11:58:06 +02:00
logger . info ( ` Lightning daily stats done. ` ) ;
2022-04-19 17:37:06 +04:00
} catch ( e ) {
2022-07-06 11:58:06 +02:00
logger . err ( '$logLightningStatsDaily() error: ' + ( e instanceof Error ? e.message : e ) ) ;
2022-04-19 17:37:06 +04:00
}
}
}
export default new LightningStatsUpdater ( ) ;