diff --git a/backend/package.json b/backend/package.json index d2d14fb0a..cd8c067fb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,7 +37,7 @@ "bolt07": "^1.8.1", "crypto-js": "^4.0.0", "express": "^4.18.0", - "ln-service": "^53.17.4", + "lightning": "^5.16.3", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.5.1", "socks-proxy-agent": "~7.0.0", diff --git a/backend/src/api/lightning/lightning-api-abstract-factory.ts b/backend/src/api/lightning/lightning-api-abstract-factory.ts index 2e78b52f8..026568c6d 100644 --- a/backend/src/api/lightning/lightning-api-abstract-factory.ts +++ b/backend/src/api/lightning/lightning-api-abstract-factory.ts @@ -3,5 +3,5 @@ import { ILightningApi } from './lightning-api.interface'; export interface AbstractLightningApi { $getNetworkInfo(): Promise; $getNetworkGraph(): Promise; - $getChanInfo(id: string): Promise; + $getInfo(): Promise; } diff --git a/backend/src/api/lightning/lightning-api.interface.ts b/backend/src/api/lightning/lightning-api.interface.ts index 26999c119..9b83b5473 100644 --- a/backend/src/api/lightning/lightning-api.interface.ts +++ b/backend/src/api/lightning/lightning-api.interface.ts @@ -21,17 +21,17 @@ export namespace ILightningApi { policies: Policy[]; transaction_id: string; transaction_vout: number; - updated_at: string; + updated_at?: string; } interface Policy { public_key: string; - base_fee_mtokens?: number; + base_fee_mtokens?: string; cltv_delta?: number; fee_rate?: number; is_disabled?: boolean; - max_htlc_mtokens?: number; - min_htlc_mtokens?: number; + max_htlc_mtokens?: string; + min_htlc_mtokens?: string; updated_at?: string; } @@ -41,13 +41,31 @@ export namespace ILightningApi { features: Feature[]; public_key: string; sockets: string[]; - updated_at: string; + updated_at?: string; } - interface Feature { + export interface Info { + chains: string[]; + color: string; + active_channels_count: number; + alias: string; + current_block_hash: string; + current_block_height: number; + features: Feature[]; + is_synced_to_chain: boolean; + is_synced_to_graph: boolean; + latest_block_at: string; + peers_count: number; + pending_channels_count: number; + public_key: string; + uris: any[]; + version: string; + } + + export interface Feature { bit: number; is_known: boolean; is_required: boolean; - type: string; + type?: string; } } diff --git a/backend/src/api/lightning/lnd/lnd-api.ts b/backend/src/api/lightning/lnd/lnd-api.ts index 7d4e8c96e..19d98744d 100644 --- a/backend/src/api/lightning/lnd/lnd-api.ts +++ b/backend/src/api/lightning/lnd/lnd-api.ts @@ -1,7 +1,7 @@ import { AbstractLightningApi } from '../lightning-api-abstract-factory'; import { ILightningApi } from '../lightning-api.interface'; import * as fs from 'fs'; -import * as lnService from 'ln-service'; +import { authenticatedLndGrpc, getWalletInfo, getNetworkGraph, getNetworkInfo } from 'lightning'; import config from '../../../config'; import logger from '../../../logger'; @@ -15,7 +15,7 @@ class LndApi implements AbstractLightningApi { const tls = fs.readFileSync(config.LND.TLS_CERT_PATH).toString('base64'); const macaroon = fs.readFileSync(config.LND.MACAROON_PATH).toString('base64'); - const { lnd } = lnService.authenticatedLndGrpc({ + const { lnd } = authenticatedLndGrpc({ cert: tls, macaroon: macaroon, socket: config.LND.SOCKET, @@ -29,15 +29,16 @@ class LndApi implements AbstractLightningApi { } async $getNetworkInfo(): Promise { - return await lnService.getNetworkInfo({ lnd: this.lnd }); + return await getNetworkInfo({ lnd: this.lnd }); + } + + async $getInfo(): Promise { + // @ts-ignore + return await getWalletInfo({ lnd: this.lnd }); } async $getNetworkGraph(): Promise { - return await lnService.getNetworkGraph({ lnd: this.lnd }); - } - - async $getChanInfo(id: string): Promise { - return await lnService.getChannel({ lnd: this.lnd, id }); + return await getNetworkGraph({ lnd: this.lnd }); } } diff --git a/backend/src/index.ts b/backend/src/index.ts index 74d6fb3da..4e86060af 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,5 +1,5 @@ import express from "express"; -import { Application, Request, Response, NextFunction, Express } from 'express'; +import { Application, Request, Response, NextFunction } from 'express'; import * as http from 'http'; import * as WebSocket from 'ws'; import cluster from 'cluster'; @@ -28,12 +28,12 @@ import { Common } from './api/common'; import poolsUpdater from './tasks/pools-updater'; import indexer from './indexer'; import priceUpdater from './tasks/price-updater'; -import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; -import nodeSyncService from './tasks/lightning/node-sync.service'; -import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; import nodesRoutes from './api/explorer/nodes.routes'; import channelsRoutes from './api/explorer/channels.routes'; import generalLightningRoutes from './api/explorer/general.routes'; +import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; +import nodeSyncService from './tasks/lightning/node-sync.service'; +import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; class Server { private wss: WebSocket.Server | undefined; @@ -137,9 +137,7 @@ class Server { if (config.LIGHTNING.ENABLED) { nodeSyncService.$startService() - .then(() => { - lightningStatsUpdater.$startService(); - }); + .then(() => lightningStatsUpdater.$startService()); } this.server.listen(config.MEMPOOL.HTTP_PORT, () => { diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts index c8b7f8c4c..c5a6c8a9d 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -245,7 +245,6 @@ class NodeSyncService { const fromChannel = chanNumber({ channel: channel.id }).number; try { - const d = new Date(Date.parse(channel.updated_at)); const query = `INSERT INTO channels ( id, @@ -360,7 +359,7 @@ class NodeSyncService { private async $saveNode(node: ILightningApi.Node): Promise { try { - const updatedAt = this.utcDateToMysql(node.updated_at); + const updatedAt = node.updated_at ? this.utcDateToMysql(node.updated_at) : '0000-00-00 00:00:00'; const sockets = node.sockets.join(','); const query = `INSERT INTO nodes( public_key, diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 83b9973d4..0047eae77 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -6,21 +6,43 @@ class LightningStatsUpdater { constructor() {} public async $startService() { - logger.info('Starting Stats service'); + 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; + } 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(); - // setTimeout(() => { + setTimeout(() => { setInterval(async () => { await this.$runTasks(); }, 1000 * 60 * 60); - //}, difference); + }, difference); await this.$runTasks(); } + private async $lightningIsSynced(): Promise { + const nodeInfo = await lightningApi.$getInfo(); + return nodeInfo.is_synced_to_chain && nodeInfo.is_synced_to_graph; + } + private async $runTasks() { await this.$populateHistoricalData(); await this.$logLightningStatsDaily(); @@ -28,8 +50,6 @@ class LightningStatsUpdater { } private async $logNodeStatsDaily() { - logger.info(`Running daily node stats update...`); - const currentDate = new Date().toISOString().split('T')[0]; try { const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`); @@ -38,6 +58,8 @@ class LightningStatsUpdater { return; } + logger.info(`Running daily node stats update...`); + 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`; const [nodes]: any = await DB.query(query); @@ -61,8 +83,6 @@ class LightningStatsUpdater { // We only run this on first launch private async $populateHistoricalData() { - logger.info(`Running historical stats population...`); - const startTime = '2018-01-13'; try { const [rows]: any = await DB.query(`SELECT COUNT(*) FROM lightning_stats`); @@ -70,6 +90,8 @@ class LightningStatsUpdater { if (rows[0]['COUNT(*)'] > 0) { return; } + logger.info(`Running historical stats population...`); + const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels ORDER BY created ASC`); let date: Date = new Date(startTime); @@ -138,8 +160,6 @@ class LightningStatsUpdater { } private async $logLightningStatsDaily() { - logger.info(`Running lightning daily stats log...`); - const currentDate = new Date().toISOString().split('T')[0]; try { const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`); @@ -148,6 +168,8 @@ class LightningStatsUpdater { return; } + logger.info(`Running lightning daily stats log...`); + const networkGraph = await lightningApi.$getNetworkGraph(); let total_capacity = 0; for (const channel of networkGraph.channels) {