2022-04-27 02:52:23 +04:00
import logger from '../../logger' ;
import DB from '../../database' ;
2022-08-08 09:00:11 +02:00
import { ResultSetHeader } from 'mysql2' ;
import { ILightningApi } from '../lightning/lightning-api.interface' ;
2022-08-17 16:19:01 +02:00
import { ITopNodesPerCapacity , ITopNodesPerChannels } from '../../mempool.interfaces' ;
2022-04-27 02:52:23 +04:00
class NodesApi {
2022-04-29 03:57:27 +04:00
public async $getNode ( public_key : string ) : Promise < any > {
try {
2022-08-04 11:30:32 +02:00
// General info
let query = `
SELECT public_key , alias , UNIX_TIMESTAMP ( first_seen ) AS first_seen ,
2022-08-17 16:00:30 +02:00
UNIX_TIMESTAMP ( updated_at ) AS updated_at , color , sockets as sockets ,
as_number , city_id , country_id , subdivision_id , longitude , latitude ,
geo_names_iso . names as iso_code , geo_names_as . names as as_organization , geo_names_city . names as city ,
geo_names_country . names as country , geo_names_subdivision . names as subdivision
2022-07-12 20:18:41 +02:00
FROM nodes
2022-07-12 21:07:38 +02:00
LEFT JOIN geo_names geo_names_as on geo_names_as . id = as_number
LEFT JOIN geo_names geo_names_city on geo_names_city . id = city_id
2022-07-12 21:28:02 +02:00
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision . id = subdivision_id
2022-07-12 21:07:38 +02:00
LEFT JOIN geo_names geo_names_country on geo_names_country . id = country_id
2022-07-26 12:26:44 +02:00
LEFT JOIN geo_names geo_names_iso ON geo_names_iso . id = nodes . country_id AND geo_names_iso . type = 'country_iso_code'
2022-07-12 20:18:41 +02:00
WHERE public_key = ?
` ;
2022-08-04 11:30:32 +02:00
let [ rows ] : any [ ] = await DB . query ( query , [ public_key ] ) ;
if ( rows . length === 0 ) {
throw new Error ( ` This node does not exist, or our node is not seeing it yet ` ) ;
}
const node = rows [ 0 ] ;
node . as_organization = JSON . parse ( node . as_organization ) ;
node . subdivision = JSON . parse ( node . subdivision ) ;
node . city = JSON . parse ( node . city ) ;
node . country = JSON . parse ( node . country ) ;
// Active channels and capacity
const activeChannelsStats : any = await this . $getActiveChannelsStats ( public_key ) ;
node . active_channel_count = activeChannelsStats . active_channel_count ? ? 0 ;
node . capacity = activeChannelsStats . capacity ? ? 0 ;
// Opened channels count
query = `
SELECT count ( short_id ) as opened_channel_count
FROM channels
WHERE status != 2 AND ( channels . node1_public_key = ? OR channels . node2_public_key = ? )
` ;
[ rows ] = await DB . query ( query , [ public_key , public_key ] ) ;
node . opened_channel_count = 0 ;
2022-07-12 21:07:38 +02:00
if ( rows . length > 0 ) {
2022-08-04 11:30:32 +02:00
node . opened_channel_count = rows [ 0 ] . opened_channel_count ;
2022-07-12 21:07:38 +02:00
}
2022-08-04 11:30:32 +02:00
// Closed channels count
query = `
SELECT count ( short_id ) as closed_channel_count
FROM channels
WHERE status = 2 AND ( channels . node1_public_key = ? OR channels . node2_public_key = ? )
` ;
[ rows ] = await DB . query ( query , [ public_key , public_key ] ) ;
node . closed_channel_count = 0 ;
if ( rows . length > 0 ) {
node . closed_channel_count = rows [ 0 ] . closed_channel_count ;
}
return node ;
2022-04-29 03:57:27 +04:00
} catch ( e ) {
2022-08-04 11:30:32 +02:00
logger . err ( ` Cannot get node information for ${ public_key } . Reason: ${ ( e instanceof Error ? e.message : e ) } ` ) ;
2022-04-29 03:57:27 +04:00
throw e ;
}
}
2022-08-04 11:30:32 +02:00
public async $getActiveChannelsStats ( node_public_key : string ) : Promise < unknown > {
const query = `
SELECT count ( short_id ) as active_channel_count , sum ( capacity ) as capacity
FROM channels
WHERE status = 1 AND ( channels . node1_public_key = ? OR channels . node2_public_key = ? )
` ;
const [ rows ] : any [ ] = await DB . query ( query , [ node_public_key , node_public_key ] ) ;
if ( rows . length > 0 ) {
return {
active_channel_count : rows [ 0 ] . active_channel_count ,
capacity : rows [ 0 ] . capacity
} ;
} else {
return null ;
}
}
2022-07-11 17:52:38 +02:00
public async $getAllNodes ( ) : Promise < any > {
try {
const query = ` SELECT * FROM nodes ` ;
const [ rows ] : any = await DB . query ( query ) ;
return rows ;
} catch ( e ) {
logger . err ( '$getAllNodes error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-05 23:19:24 +04:00
public async $getNodeStats ( public_key : string ) : Promise < any > {
try {
2022-08-04 11:30:32 +02:00
const query = `
SELECT UNIX_TIMESTAMP ( added ) AS added , capacity , channels
FROM node_stats
WHERE public_key = ?
ORDER BY added DESC
` ;
2022-05-05 23:19:24 +04:00
const [ rows ] : any = await DB . query ( query , [ public_key ] ) ;
return rows ;
} catch ( e ) {
logger . err ( '$getNodeStats error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-08-17 16:19:01 +02:00
public async $getTopCapacityNodes ( full : boolean ) : Promise < ITopNodesPerCapacity [ ] > {
2022-04-27 02:52:23 +04:00
try {
2022-08-02 18:26:07 +02:00
let [ rows ] : any [ ] = await DB . query ( 'SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats' ) ;
const latestDate = rows [ 0 ] . maxAdded ;
2022-08-17 16:00:30 +02:00
let query : string ;
if ( full === false ) {
query = `
SELECT nodes . public_key AS publicKey , IF ( nodes . alias = '' , SUBSTRING ( nodes . public_key , 1 , 20 ) , alias ) as alias ,
node_stats . capacity
FROM node_stats
JOIN nodes ON nodes . public_key = node_stats . public_key
WHERE added = FROM_UNIXTIME ( $ { latestDate } )
ORDER BY capacity DESC
LIMIT 100
` ;
[ rows ] = await DB . query ( query ) ;
} else {
query = `
SELECT node_stats . public_key AS publicKey , IF ( nodes . alias = '' , SUBSTRING ( node_stats . public_key , 1 , 20 ) , alias ) as alias ,
CAST ( COALESCE ( node_stats . capacity , 0 ) as INT ) as capacity ,
CAST ( COALESCE ( node_stats . channels , 0 ) as INT ) as channels ,
UNIX_TIMESTAMP ( nodes . first_seen ) as firstSeen , UNIX_TIMESTAMP ( nodes . updated_at ) as updatedAt ,
geo_names_city . names as city , geo_names_country . names as country
FROM node_stats
RIGHT JOIN nodes ON nodes . public_key = node_stats . public_key
LEFT JOIN geo_names geo_names_country ON geo_names_country . id = nodes . country_id AND geo_names_country . type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city . id = nodes . city_id AND geo_names_city . type = 'city'
WHERE added = FROM_UNIXTIME ( $ { latestDate } )
ORDER BY capacity DESC
LIMIT 100
` ;
[ rows ] = await DB . query ( query ) ;
for ( let i = 0 ; i < rows . length ; ++ i ) {
rows [ i ] . country = JSON . parse ( rows [ i ] . country ) ;
rows [ i ] . city = JSON . parse ( rows [ i ] . city ) ;
}
}
2022-08-02 18:26:07 +02:00
2022-04-27 02:52:23 +04:00
return rows ;
} catch ( e ) {
logger . err ( '$getTopCapacityNodes error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-08-17 16:19:01 +02:00
public async $getTopChannelsNodes ( full : boolean ) : Promise < ITopNodesPerChannels [ ] > {
2022-04-27 02:52:23 +04:00
try {
2022-08-02 18:26:07 +02:00
let [ rows ] : any [ ] = await DB . query ( 'SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats' ) ;
const latestDate = rows [ 0 ] . maxAdded ;
2022-08-17 16:19:01 +02:00
let query : string ;
if ( full === false ) {
query = `
SELECT nodes . public_key , IF ( nodes . alias = '' , SUBSTRING ( nodes . public_key , 1 , 20 ) , alias ) as alias ,
node_stats . channels
FROM node_stats
JOIN nodes ON nodes . public_key = node_stats . public_key
WHERE added = FROM_UNIXTIME ( $ { latestDate } )
ORDER BY channels DESC
LIMIT 100 ;
` ;
[ rows ] = await DB . query ( query ) ;
} else {
query = `
SELECT node_stats . public_key AS publicKey , IF ( nodes . alias = '' , SUBSTRING ( node_stats . public_key , 1 , 20 ) , alias ) as alias ,
CAST ( COALESCE ( node_stats . channels , 0 ) as INT ) as channels ,
CAST ( COALESCE ( node_stats . capacity , 0 ) as INT ) as capacity ,
UNIX_TIMESTAMP ( nodes . first_seen ) as firstSeen , UNIX_TIMESTAMP ( nodes . updated_at ) as updatedAt ,
geo_names_city . names as city , geo_names_country . names as country
FROM node_stats
RIGHT JOIN nodes ON nodes . public_key = node_stats . public_key
LEFT JOIN geo_names geo_names_country ON geo_names_country . id = nodes . country_id AND geo_names_country . type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city . id = nodes . city_id AND geo_names_city . type = 'city'
WHERE added = FROM_UNIXTIME ( $ { latestDate } )
ORDER BY channels DESC
LIMIT 100
` ;
[ rows ] = await DB . query ( query ) ;
for ( let i = 0 ; i < rows . length ; ++ i ) {
rows [ i ] . country = JSON . parse ( rows [ i ] . country ) ;
rows [ i ] . city = JSON . parse ( rows [ i ] . city ) ;
}
}
2022-08-02 18:26:07 +02:00
2022-04-27 02:52:23 +04:00
return rows ;
} catch ( e ) {
logger . err ( '$getTopChannelsNodes error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-09 18:21:42 +04:00
public async $searchNodeByPublicKeyOrAlias ( search : string ) {
try {
const searchStripped = search . replace ( '%' , '' ) + '%' ;
2022-05-15 19:22:14 +04:00
const query = ` SELECT nodes.public_key, nodes.alias, node_stats.capacity FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key WHERE nodes.public_key LIKE ? OR nodes.alias LIKE ? GROUP BY nodes.public_key ORDER BY node_stats.capacity DESC LIMIT 10 ` ;
2022-05-09 18:21:42 +04:00
const [ rows ] : any = await DB . query ( query , [ searchStripped , searchStripped ] ) ;
return rows ;
} catch ( e ) {
logger . err ( '$searchNodeByPublicKeyOrAlias error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-07-12 22:32:13 +02:00
2022-08-11 10:19:13 +02:00
public async $getNodesISPRanking ( groupBy : string , showTor : boolean ) {
2022-07-12 22:32:13 +02:00
try {
2022-07-27 13:20:54 +02:00
const orderBy = groupBy === 'capacity' ? ` CAST(SUM(capacity) as INT) ` : ` COUNT(DISTINCT nodes.public_key) ` ;
// Clearnet
let query = ` SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names,
COUNT ( DISTINCT nodes . public_key ) as nodesCount , CAST ( SUM ( capacity ) as INT ) as capacity
2022-07-16 11:32:48 +02:00
FROM nodes
2022-07-12 22:32:13 +02:00
JOIN geo_names ON geo_names . id = nodes . as_number
2022-07-16 11:32:48 +02:00
JOIN channels ON channels . node1_public_key = nodes . public_key OR channels . node2_public_key = nodes . public_key
2022-07-23 23:33:13 +02:00
GROUP BY geo_names . names
2022-07-27 13:20:54 +02:00
ORDER BY $ { orderBy } DESC
` ;
2022-07-12 22:32:13 +02:00
const [ nodesCountPerAS ] : any = await DB . query ( query ) ;
2022-07-27 13:20:54 +02:00
let total = 0 ;
2022-07-12 22:32:13 +02:00
const nodesPerAs : any [ ] = [ ] ;
2022-07-27 13:20:54 +02:00
for ( const asGroup of nodesCountPerAS ) {
if ( groupBy === 'capacity' ) {
total += asGroup . capacity ;
} else {
total += asGroup . nodesCount ;
}
}
// Tor
if ( showTor ) {
query = ` SELECT COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
FROM nodes
JOIN channels ON channels . node1_public_key = nodes . public_key OR channels . node2_public_key = nodes . public_key
ORDER BY $ { orderBy } DESC
` ;
const [ nodesCountTor ] : any = await DB . query ( query ) ;
total += groupBy === 'capacity' ? nodesCountTor [ 0 ] . capacity : nodesCountTor [ 0 ] . nodesCount ;
nodesPerAs . push ( {
ispId : null ,
name : 'Tor' ,
count : nodesCountTor [ 0 ] . nodesCount ,
share : Math.floor ( ( groupBy === 'capacity' ? nodesCountTor [ 0 ] . capacity : nodesCountTor [ 0 ] . nodesCount ) / total * 10000 ) / 100 ,
capacity : nodesCountTor [ 0 ] . capacity ,
} ) ;
}
2022-07-12 22:32:13 +02:00
for ( const as of nodesCountPerAS ) {
nodesPerAs . push ( {
2022-07-17 22:57:29 +02:00
ispId : as.ispId ,
2022-07-12 22:32:13 +02:00
name : JSON.parse ( as . names ) ,
count : as.nodesCount ,
2022-07-27 13:20:54 +02:00
share : Math.floor ( ( groupBy === 'capacity' ? as . capacity : as.nodesCount ) / total * 10000 ) / 100 ,
2022-07-16 11:32:48 +02:00
capacity : as.capacity ,
2022-07-27 13:20:54 +02:00
} ) ;
2022-07-12 22:32:13 +02:00
}
return nodesPerAs ;
} catch ( e ) {
logger . err ( ` Cannot get nodes grouped by AS. Reason: ${ e instanceof Error ? e.message : e } ` ) ;
throw e ;
}
}
2022-07-13 00:25:40 +02:00
2022-07-17 09:53:02 +02:00
public async $getNodesPerCountry ( countryId : string ) {
2022-07-13 00:25:40 +02:00
try {
2022-07-16 15:56:36 +02:00
const query = `
2022-08-01 09:59:20 +02:00
SELECT nodes . public_key , CAST ( COALESCE ( node_stats . capacity , 0 ) as INT ) as capacity , CAST ( COALESCE ( node_stats . channels , 0 ) as INT ) as channels ,
nodes . alias , UNIX_TIMESTAMP ( nodes . first_seen ) as first_seen , UNIX_TIMESTAMP ( nodes . updated_at ) as updated_at ,
2022-07-16 15:56:36 +02:00
geo_names_city . names as city
FROM node_stats
JOIN (
SELECT public_key , MAX ( added ) as last_added
FROM node_stats
GROUP BY public_key
) as b ON b . public_key = node_stats . public_key AND b . last_added = node_stats . added
2022-08-01 09:59:20 +02:00
RIGHT JOIN nodes ON nodes . public_key = node_stats . public_key
2022-07-18 01:11:20 +02:00
JOIN geo_names geo_names_country ON geo_names_country . id = nodes . country_id AND geo_names_country . type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city . id = nodes . city_id AND geo_names_city . type = 'city'
2022-07-17 09:53:02 +02:00
WHERE geo_names_country . id = ?
2022-07-16 15:56:36 +02:00
ORDER BY capacity DESC
2022-07-13 00:25:40 +02:00
` ;
2022-07-17 09:53:02 +02:00
const [ rows ] : any = await DB . query ( query , [ countryId ] ) ;
2022-07-16 15:56:36 +02:00
for ( let i = 0 ; i < rows . length ; ++ i ) {
rows [ i ] . city = JSON . parse ( rows [ i ] . city ) ;
}
2022-07-13 00:25:40 +02:00
return rows ;
} catch ( e ) {
2022-07-17 09:53:02 +02:00
logger . err ( ` Cannot get nodes for country id ${ countryId } . Reason: ${ e instanceof Error ? e.message : e } ` ) ;
2022-07-13 00:25:40 +02:00
throw e ;
}
}
2022-07-17 22:57:29 +02:00
public async $getNodesPerISP ( ISPId : string ) {
try {
const query = `
2022-08-01 09:59:20 +02:00
SELECT nodes . public_key , CAST ( COALESCE ( node_stats . capacity , 0 ) as INT ) as capacity , CAST ( COALESCE ( node_stats . channels , 0 ) as INT ) as channels ,
nodes . alias , UNIX_TIMESTAMP ( nodes . first_seen ) as first_seen , UNIX_TIMESTAMP ( nodes . updated_at ) as updated_at ,
2022-07-17 22:57:29 +02:00
geo_names_city . names as city , geo_names_country . names as country
FROM node_stats
JOIN (
SELECT public_key , MAX ( added ) as last_added
FROM node_stats
GROUP BY public_key
) as b ON b . public_key = node_stats . public_key AND b . last_added = node_stats . added
2022-07-23 23:33:13 +02:00
RIGHT JOIN nodes ON nodes . public_key = node_stats . public_key
2022-07-17 22:57:29 +02:00
JOIN geo_names geo_names_country ON geo_names_country . id = nodes . country_id AND geo_names_country . type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city . id = nodes . city_id AND geo_names_city . type = 'city'
2022-07-23 23:33:13 +02:00
WHERE nodes . as_number IN ( ? )
2022-07-17 22:57:29 +02:00
ORDER BY capacity DESC
` ;
2022-07-23 23:33:13 +02:00
const [ rows ] : any = await DB . query ( query , [ ISPId . split ( ',' ) ] ) ;
2022-07-17 22:57:29 +02:00
for ( let i = 0 ; i < rows . length ; ++ i ) {
rows [ i ] . country = JSON . parse ( rows [ i ] . country ) ;
rows [ i ] . city = JSON . parse ( rows [ i ] . city ) ;
}
return rows ;
} catch ( e ) {
logger . err ( ` Cannot get nodes for ISP id ${ ISPId } . Reason: ${ e instanceof Error ? e.message : e } ` ) ;
throw e ;
}
}
2022-07-17 11:10:17 +02:00
public async $getNodesCountries() {
try {
let query = ` SELECT geo_names.names as names, geo_names_iso.names as iso_code, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
FROM nodes
JOIN geo_names ON geo_names . id = nodes . country_id AND geo_names . type = 'country'
JOIN geo_names geo_names_iso ON geo_names_iso . id = nodes . country_id AND geo_names_iso . type = 'country_iso_code'
JOIN channels ON channels . node1_public_key = nodes . public_key OR channels . node2_public_key = nodes . public_key
GROUP BY country_id
ORDER BY COUNT ( DISTINCT nodes . public_key ) DESC
` ;
const [ nodesCountPerCountry ] : any = await DB . query ( query ) ;
query = ` SELECT COUNT(*) as total FROM nodes WHERE country_id IS NOT NULL ` ;
const [ nodesWithAS ] : any = await DB . query ( query ) ;
const nodesPerCountry : any [ ] = [ ] ;
for ( const country of nodesCountPerCountry ) {
nodesPerCountry . push ( {
name : JSON.parse ( country . names ) ,
iso : country.iso_code ,
count : country.nodesCount ,
share : Math.floor ( country . nodesCount / nodesWithAS [ 0 ] . total * 10000 ) / 100 ,
capacity : country.capacity ,
} )
}
return nodesPerCountry ;
} catch ( e ) {
logger . err ( ` Cannot get nodes grouped by AS. Reason: ${ e instanceof Error ? e.message : e } ` ) ;
throw e ;
}
}
2022-08-08 09:00:11 +02:00
/ * *
* Save or update a node present in the graph
* /
public async $saveNode ( node : ILightningApi.Node ) : Promise < void > {
try {
const sockets = ( node . addresses ? . map ( a = > a . addr ) . join ( ',' ) ) ? ? '' ;
const query = ` INSERT INTO nodes(
public_key ,
first_seen ,
updated_at ,
alias ,
color ,
sockets ,
status
)
VALUES ( ? , NOW ( ) , FROM_UNIXTIME ( ? ) , ? , ? , ? , 1 )
ON DUPLICATE KEY UPDATE updated_at = FROM_UNIXTIME ( ? ) , alias = ? , color = ? , sockets = ? , status = 1 ` ;
await DB . query ( query , [
node . pub_key ,
node . last_update ,
node . alias ,
node . color ,
sockets ,
node . last_update ,
node . alias ,
node . color ,
sockets ,
] ) ;
} catch ( e ) {
logger . err ( '$saveNode() error: ' + ( e instanceof Error ? e.message : e ) ) ;
}
}
/ * *
* Set all nodes not in ` nodesPubkeys ` as inactive ( status = 0 )
* /
public async $setNodesInactive ( graphNodesPubkeys : string [ ] ) : Promise < void > {
if ( graphNodesPubkeys . length === 0 ) {
return ;
}
try {
const result = await DB . query < ResultSetHeader > ( `
UPDATE nodes
SET status = 0
WHERE public_key NOT IN (
$ { graphNodesPubkeys . map ( pubkey = > ` " ${ pubkey } " ` ) . join ( ',' ) }
)
` );
if ( result [ 0 ] . changedRows ? ? 0 > 0 ) {
logger . info ( ` Marked ${ result [ 0 ] . changedRows } nodes as inactive because they are not in the graph ` ) ;
} else {
logger . debug ( ` Marked ${ result [ 0 ] . changedRows } nodes as inactive because they are not in the graph ` ) ;
}
} catch ( e ) {
logger . err ( '$setNodesInactive() error: ' + ( e instanceof Error ? e.message : e ) ) ;
}
}
2022-04-27 02:52:23 +04:00
}
export default new NodesApi ( ) ;