2022-04-29 03:57:27 +04:00
import logger from '../../logger' ;
import DB from '../../database' ;
2022-08-04 11:30:32 +02:00
import nodesApi from './nodes.api' ;
2022-08-08 09:00:11 +02:00
import { ResultSetHeader } from 'mysql2' ;
import { ILightningApi } from '../lightning/lightning-api.interface' ;
import { Common } from '../common' ;
2022-04-29 03:57:27 +04:00
class ChannelsApi {
2022-05-01 15:35:28 +04:00
public async $getAllChannels ( ) : Promise < any [ ] > {
try {
const query = ` SELECT * FROM channels ` ;
const [ rows ] : any = await DB . query ( query ) ;
return rows ;
} catch ( e ) {
2022-05-07 11:32:15 +04:00
logger . err ( '$getAllChannels error: ' + ( e instanceof Error ? e.message : e ) ) ;
2022-05-01 15:35:28 +04:00
throw e ;
}
}
2022-07-23 15:43:38 +02:00
public async $getAllChannelsGeo ( publicKey? : string ) : Promise < any [ ] > {
2022-07-21 22:43:12 +02:00
try {
2022-07-23 15:43:38 +02:00
const params : string [ ] = [ ] ;
let query = ` SELECT nodes_1.public_key as node1_public_key, nodes_1.alias AS node1_alias,
2022-07-21 22:43:12 +02:00
nodes_1 . latitude AS node1_latitude , nodes_1 . longitude AS node1_longitude ,
nodes_2 . public_key as node2_public_key , nodes_2 . alias AS node2_alias ,
nodes_2 . latitude AS node2_latitude , nodes_2 . longitude AS node2_longitude ,
channels . capacity
FROM channels
JOIN nodes AS nodes_1 on nodes_1 . public_key = channels . node1_public_key
JOIN nodes AS nodes_2 on nodes_2 . public_key = channels . node2_public_key
WHERE nodes_1 . latitude IS NOT NULL AND nodes_1 . longitude IS NOT NULL
AND nodes_2 . latitude IS NOT NULL AND nodes_2 . longitude IS NOT NULL
` ;
2022-07-23 15:43:38 +02:00
if ( publicKey !== undefined ) {
query += ' AND (nodes_1.public_key = ? OR nodes_2.public_key = ?)' ;
params . push ( publicKey ) ;
params . push ( publicKey ) ;
}
const [ rows ] : any = await DB . query ( query , params ) ;
2022-07-21 22:43:12 +02:00
return rows . map ( ( row ) = > [
row . node1_public_key , row . node1_alias , row . node1_longitude , row . node1_latitude ,
row . node2_public_key , row . node2_alias , row . node2_longitude , row . node2_latitude ,
row . capacity ] ) ;
} catch ( e ) {
2022-07-22 00:04:02 +02:00
logger . err ( '$getAllChannelsGeo error: ' + ( e instanceof Error ? e.message : e ) ) ;
2022-07-21 22:43:12 +02:00
throw e ;
}
}
2022-05-09 18:21:42 +04:00
public async $searchChannelsById ( search : string ) : Promise < any [ ] > {
try {
const searchStripped = search . replace ( '%' , '' ) + '%' ;
const query = ` SELECT id, short_id, capacity FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10 ` ;
const [ rows ] : any = await DB . query ( query , [ searchStripped , searchStripped ] ) ;
return rows ;
} catch ( e ) {
logger . err ( '$searchChannelsById error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-01 15:35:28 +04:00
public async $getChannelsByStatus ( status : number ) : Promise < any [ ] > {
try {
const query = ` SELECT * FROM channels WHERE status = ? ` ;
const [ rows ] : any = await DB . query ( query , [ status ] ) ;
return rows ;
} catch ( e ) {
2022-05-07 11:32:15 +04:00
logger . err ( '$getChannelsByStatus error: ' + ( e instanceof Error ? e.message : e ) ) ;
2022-05-01 15:35:28 +04:00
throw e ;
}
}
2022-06-29 23:06:13 +02:00
public async $getClosedChannelsWithoutReason ( ) : Promise < any [ ] > {
try {
2022-07-06 11:58:06 +02:00
const query = ` SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL AND closing_transaction_id != '' ` ;
2022-06-29 23:06:13 +02:00
const [ rows ] : any = await DB . query ( query ) ;
return rows ;
} catch ( e ) {
logger . err ( '$getClosedChannelsWithoutReason error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-05 23:19:24 +04:00
public async $getChannelsWithoutCreatedDate ( ) : Promise < any [ ] > {
try {
const query = ` SELECT * FROM channels WHERE created IS NULL ` ;
const [ rows ] : any = await DB . query ( query ) ;
return rows ;
} catch ( e ) {
logger . err ( '$getChannelsWithoutCreatedDate error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-26 18:23:57 +04:00
public async $getChannel ( id : string ) : Promise < any > {
2022-05-01 03:01:27 +04:00
try {
2022-05-16 00:01:53 +04:00
const query = ` SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*, ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key WHERE (ns1.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node1_public_key) AND ns2.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node2_public_key)) AND channels.id = ? ` ;
2022-05-26 18:23:57 +04:00
const [ rows ] : any = await DB . query ( query , [ id ] ) ;
2022-05-15 19:22:14 +04:00
if ( rows [ 0 ] ) {
return this . convertChannel ( rows [ 0 ] ) ;
}
2022-05-01 03:01:27 +04:00
} catch ( e ) {
logger . err ( '$getChannel error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-07-06 14:56:10 +02:00
public async $getChannelsStats ( ) : Promise < any > {
try {
// Feedback from zerofeerouting:
// "I would argue > 5000ppm can be ignored. Channels charging more than .5% fee are ignored by CLN for example."
const ignoredFeeRateThreshold = 5000 ;
const ignoredBaseFeeThreshold = 5000 ;
// Capacity
let query = ` SELECT AVG(capacity) AS avgCapacity FROM channels WHERE status = 1 ORDER BY capacity ` ;
const [ avgCapacity ] : any = await DB . query ( query ) ;
query = ` SELECT capacity FROM channels WHERE status = 1 ORDER BY capacity ` ;
let [ capacity ] : any = await DB . query ( query ) ;
capacity = capacity . map ( capacity = > capacity . capacity ) ;
const medianCapacity = capacity [ Math . floor ( capacity . length / 2 ) ] ;
// Fee rates
query = ` SELECT node1_fee_rate FROM channels WHERE node1_fee_rate < ${ ignoredFeeRateThreshold } AND status = 1 ` ;
let [ feeRates1 ] : any = await DB . query ( query ) ;
feeRates1 = feeRates1 . map ( rate = > rate . node1_fee_rate ) ;
query = ` SELECT node2_fee_rate FROM channels WHERE node2_fee_rate < ${ ignoredFeeRateThreshold } AND status = 1 ` ;
let [ feeRates2 ] : any = await DB . query ( query ) ;
feeRates2 = feeRates2 . map ( rate = > rate . node2_fee_rate ) ;
let feeRates = ( feeRates1 . concat ( feeRates2 ) ) . sort ( ( a , b ) = > a - b ) ;
let avgFeeRate = 0 ;
for ( const rate of feeRates ) {
avgFeeRate += rate ;
}
avgFeeRate /= feeRates . length ;
const medianFeeRate = feeRates [ Math . floor ( feeRates . length / 2 ) ] ;
// Base fees
query = ` SELECT node1_base_fee_mtokens FROM channels WHERE node1_base_fee_mtokens < ${ ignoredBaseFeeThreshold } AND status = 1 ` ;
let [ baseFees1 ] : any = await DB . query ( query ) ;
baseFees1 = baseFees1 . map ( rate = > rate . node1_base_fee_mtokens ) ;
query = ` SELECT node2_base_fee_mtokens FROM channels WHERE node2_base_fee_mtokens < ${ ignoredBaseFeeThreshold } AND status = 1 ` ;
let [ baseFees2 ] : any = await DB . query ( query ) ;
baseFees2 = baseFees2 . map ( rate = > rate . node2_base_fee_mtokens ) ;
let baseFees = ( baseFees1 . concat ( baseFees2 ) ) . sort ( ( a , b ) = > a - b ) ;
let avgBaseFee = 0 ;
for ( const fee of baseFees ) {
avgBaseFee += fee ;
}
avgBaseFee /= baseFees . length ;
const medianBaseFee = feeRates [ Math . floor ( baseFees . length / 2 ) ] ;
return {
avgCapacity : parseInt ( avgCapacity [ 0 ] . avgCapacity , 10 ) ,
avgFeeRate : avgFeeRate ,
avgBaseFee : avgBaseFee ,
medianCapacity : medianCapacity ,
medianFeeRate : medianFeeRate ,
medianBaseFee : medianBaseFee ,
}
} catch ( e ) {
logger . err ( ` Cannot calculate channels statistics. Reason: ${ e instanceof Error ? e.message : e } ` ) ;
throw e ;
}
}
2022-05-09 18:21:42 +04:00
public async $getChannelsByTransactionId ( transactionIds : string [ ] ) : Promise < any [ ] > {
2022-05-07 11:32:15 +04:00
try {
2022-05-09 18:21:42 +04:00
transactionIds = transactionIds . map ( ( id ) = > '\'' + id + '\'' ) ;
2022-06-30 00:35:27 +02:00
const query = ` SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.* FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key WHERE channels.transaction_id IN ( ${ transactionIds . join ( ', ' ) } ) OR channels.closing_transaction_id IN ( ${ transactionIds . join ( ', ' ) } ) ` ;
2022-05-09 18:21:42 +04:00
const [ rows ] : any = await DB . query ( query ) ;
2022-05-15 19:22:14 +04:00
const channels = rows . map ( ( row ) = > this . convertChannel ( row ) ) ;
return channels ;
2022-05-07 11:32:15 +04:00
} catch ( e ) {
logger . err ( '$getChannelByTransactionId error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-16 01:36:59 +04:00
public async $getChannelsForNode ( public_key : string , index : number , length : number , status : string ) : Promise < any [ ] > {
2022-04-29 03:57:27 +04:00
try {
2022-08-04 11:30:32 +02:00
let channelStatusFilter ;
if ( status === 'open' ) {
channelStatusFilter = '< 2' ;
} else if ( status === 'closed' ) {
channelStatusFilter = '= 2' ;
2022-05-16 01:36:59 +04:00
}
2022-08-04 11:30:32 +02:00
// Channels originating from node
let query = `
SELECT node2 . alias , node2 . public_key , channels . status , channels . node1_fee_rate ,
channels . capacity , channels . short_id , channels . id
FROM channels
JOIN nodes AS node2 ON node2 . public_key = channels . node2_public_key
WHERE node1_public_key = ? AND channels . status $ { channelStatusFilter }
` ;
const [ channelsFromNode ] : any = await DB . query ( query , [ public_key , index , length ] ) ;
// Channels incoming to node
query = `
SELECT node1 . alias , node1 . public_key , channels . status , channels . node2_fee_rate ,
channels . capacity , channels . short_id , channels . id
FROM channels
JOIN nodes AS node1 ON node1 . public_key = channels . node1_public_key
WHERE node2_public_key = ? AND channels . status $ { channelStatusFilter }
` ;
const [ channelsToNode ] : any = await DB . query ( query , [ public_key , index , length ] ) ;
let allChannels = channelsFromNode . concat ( channelsToNode ) ;
allChannels . sort ( ( a , b ) = > {
return b . capacity - a . capacity ;
} ) ;
allChannels = allChannels . slice ( index , index + length ) ;
const channels : any [ ] = [ ]
for ( const row of allChannels ) {
const activeChannelsStats : any = await nodesApi . $getActiveChannelsStats ( row . public_key ) ;
channels . push ( {
status : row.status ,
capacity : row.capacity ? ? 0 ,
short_id : row.short_id ,
id : row.id ,
fee_rate : row.node1_fee_rate ? ? row . node2_fee_rate ? ? 0 ,
node : {
alias : row.alias.length > 0 ? row.alias : row.public_key.slice ( 0 , 20 ) ,
public_key : row.public_key ,
channels : activeChannelsStats.active_channel_count ? ? 0 ,
capacity : activeChannelsStats.capacity ? ? 0 ,
}
} ) ;
}
2022-05-15 19:22:14 +04:00
return channels ;
2022-04-29 03:57:27 +04:00
} catch ( e ) {
logger . err ( '$getChannelsForNode error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-15 19:22:14 +04:00
2022-05-16 01:36:59 +04:00
public async $getChannelsCountForNode ( public_key : string , status : string ) : Promise < any > {
try {
// Default active and inactive channels
let statusQuery = '< 2' ;
// Closed channels only
if ( status === 'closed' ) {
statusQuery = '= 2' ;
}
2022-08-04 11:30:32 +02:00
const query = `
SELECT COUNT ( * ) AS count
FROM channels
WHERE ( node1_public_key = ? OR node2_public_key = ? )
AND status $ { statusQuery }
` ;
2022-05-16 01:36:59 +04:00
const [ rows ] : any = await DB . query ( query , [ public_key , public_key ] ) ;
return rows [ 0 ] [ 'count' ] ;
} catch ( e ) {
logger . err ( '$getChannelsForNode error: ' + ( e instanceof Error ? e.message : e ) ) ;
throw e ;
}
}
2022-05-15 19:22:14 +04:00
private convertChannel ( channel : any ) : any {
return {
'id' : channel . id ,
'short_id' : channel . short_id ,
'capacity' : channel . capacity ,
'transaction_id' : channel . transaction_id ,
2022-05-16 01:36:59 +04:00
'transaction_vout' : channel . transaction_vout ,
2022-06-29 23:06:13 +02:00
'closing_transaction_id' : channel . closing_transaction_id ,
'closing_reason' : channel . closing_reason ,
2022-05-15 19:22:14 +04:00
'updated_at' : channel . updated_at ,
'created' : channel . created ,
'status' : channel . status ,
'node_left' : {
'alias' : channel . alias_left ,
'public_key' : channel . node1_public_key ,
2022-05-16 00:01:53 +04:00
'channels' : channel . channels_left ,
'capacity' : channel . capacity_left ,
2022-05-15 19:22:14 +04:00
'base_fee_mtokens' : channel . node1_base_fee_mtokens ,
'cltv_delta' : channel . node1_cltv_delta ,
'fee_rate' : channel . node1_fee_rate ,
'is_disabled' : channel . node1_is_disabled ,
'max_htlc_mtokens' : channel . node1_max_htlc_mtokens ,
'min_htlc_mtokens' : channel . node1_min_htlc_mtokens ,
'updated_at' : channel . node1_updated_at ,
} ,
'node_right' : {
'alias' : channel . alias_right ,
'public_key' : channel . node2_public_key ,
2022-05-16 00:01:53 +04:00
'channels' : channel . channels_right ,
'capacity' : channel . capacity_right ,
2022-05-15 19:22:14 +04:00
'base_fee_mtokens' : channel . node2_base_fee_mtokens ,
'cltv_delta' : channel . node2_cltv_delta ,
'fee_rate' : channel . node2_fee_rate ,
'is_disabled' : channel . node2_is_disabled ,
'max_htlc_mtokens' : channel . node2_max_htlc_mtokens ,
'min_htlc_mtokens' : channel . node2_min_htlc_mtokens ,
'updated_at' : channel . node2_updated_at ,
} ,
} ;
}
2022-08-08 09:00:11 +02:00
/ * *
* Save or update a channel present in the graph
* /
public async $saveChannel ( channel : ILightningApi.Channel ) : Promise < void > {
const [ txid , vout ] = channel . chan_point . split ( ':' ) ;
const policy1 : Partial < ILightningApi.RoutingPolicy > = channel . node1_policy || { } ;
const policy2 : Partial < ILightningApi.RoutingPolicy > = channel . node2_policy || { } ;
2022-08-09 11:07:13 +02:00
const query = ` INSERT INTO channels
(
id ,
short_id ,
capacity ,
transaction_id ,
transaction_vout ,
updated_at ,
status ,
node1_public_key ,
node1_base_fee_mtokens ,
node1_cltv_delta ,
node1_fee_rate ,
node1_is_disabled ,
node1_max_htlc_mtokens ,
node1_min_htlc_mtokens ,
node1_updated_at ,
node2_public_key ,
node2_base_fee_mtokens ,
node2_cltv_delta ,
node2_fee_rate ,
node2_is_disabled ,
node2_max_htlc_mtokens ,
node2_min_htlc_mtokens ,
node2_updated_at
)
VALUES ( ? , ? , ? , ? , ? , ? , 1 , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
ON DUPLICATE KEY UPDATE
capacity = ? ,
updated_at = ? ,
status = 1 ,
node1_public_key = ? ,
node1_base_fee_mtokens = ? ,
node1_cltv_delta = ? ,
node1_fee_rate = ? ,
node1_is_disabled = ? ,
node1_max_htlc_mtokens = ? ,
node1_min_htlc_mtokens = ? ,
node1_updated_at = ? ,
node2_public_key = ? ,
node2_base_fee_mtokens = ? ,
node2_cltv_delta = ? ,
node2_fee_rate = ? ,
node2_is_disabled = ? ,
node2_max_htlc_mtokens = ? ,
node2_min_htlc_mtokens = ? ,
node2_updated_at = ?
; ` ;
await DB . query ( query , [
Common . channelShortIdToIntegerId ( channel . channel_id ) ,
Common . channelIntegerIdToShortId ( channel . channel_id ) ,
channel . capacity ,
txid ,
vout ,
Common . utcDateToMysql ( channel . last_update ) ,
channel . node1_pub ,
policy1 . fee_base_msat ,
policy1 . time_lock_delta ,
policy1 . fee_rate_milli_msat ,
policy1 . disabled ,
policy1 . max_htlc_msat ,
policy1 . min_htlc ,
Common . utcDateToMysql ( policy1 . last_update ) ,
channel . node2_pub ,
policy2 . fee_base_msat ,
policy2 . time_lock_delta ,
policy2 . fee_rate_milli_msat ,
policy2 . disabled ,
policy2 . max_htlc_msat ,
policy2 . min_htlc ,
Common . utcDateToMysql ( policy2 . last_update ) ,
channel . capacity ,
Common . utcDateToMysql ( channel . last_update ) ,
channel . node1_pub ,
policy1 . fee_base_msat ,
policy1 . time_lock_delta ,
policy1 . fee_rate_milli_msat ,
policy1 . disabled ,
policy1 . max_htlc_msat ,
policy1 . min_htlc ,
Common . utcDateToMysql ( policy1 . last_update ) ,
channel . node2_pub ,
policy2 . fee_base_msat ,
policy2 . time_lock_delta ,
policy2 . fee_rate_milli_msat ,
policy2 . disabled ,
policy2 . max_htlc_msat ,
policy2 . min_htlc ,
Common . utcDateToMysql ( policy2 . last_update )
] ) ;
2022-08-08 09:00:11 +02:00
}
/ * *
* Set all channels not in ` graphChannelsIds ` as inactive ( status = 0 )
* /
public async $setChannelsInactive ( graphChannelsIds : string [ ] ) : Promise < void > {
if ( graphChannelsIds . length === 0 ) {
return ;
}
try {
const result = await DB . query < ResultSetHeader > ( `
UPDATE channels
SET status = 0
WHERE short_id NOT IN (
$ { graphChannelsIds . map ( id = > ` " ${ id } " ` ) . join ( ',' ) }
)
AND status != 2
` );
if ( result [ 0 ] . changedRows ? ? 0 > 0 ) {
logger . info ( ` Marked ${ result [ 0 ] . changedRows } channels as inactive because they are not in the graph ` ) ;
} else {
logger . debug ( ` Marked ${ result [ 0 ] . changedRows } channels as inactive because they are not in the graph ` ) ;
}
} catch ( e ) {
logger . err ( '$setChannelsInactive() error: ' + ( e instanceof Error ? e.message : e ) ) ;
}
}
2022-04-29 03:57:27 +04:00
}
export default new ChannelsApi ( ) ;