From 93b398a54feea3fd5f934a656f2e99d4aadc18d4 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 18 Apr 2022 18:22:00 +0400 Subject: [PATCH] Lightning explorer base structure --- lightning-backend/.gitignore | 44 ++++++ lightning-backend/mempool-config.sample.json | 27 ++++ .../src/api/lightning-api-abstract-factory.ts | 6 + .../src/api/lightning-api-factory.ts | 13 ++ .../src/api/lightning-api.interface.ts | 46 ++++++ lightning-backend/src/api/lnd/lnd-api.ts | 37 +++++ lightning-backend/src/config.ts | 84 ++++++++++ lightning-backend/src/database.ts | 51 ++++++ lightning-backend/src/index.ts | 25 +++ lightning-backend/src/logger.ts | 145 ++++++++++++++++++ 10 files changed, 478 insertions(+) create mode 100644 lightning-backend/.gitignore create mode 100644 lightning-backend/mempool-config.sample.json create mode 100644 lightning-backend/src/api/lightning-api-abstract-factory.ts create mode 100644 lightning-backend/src/api/lightning-api-factory.ts create mode 100644 lightning-backend/src/api/lightning-api.interface.ts create mode 100644 lightning-backend/src/api/lnd/lnd-api.ts create mode 100644 lightning-backend/src/config.ts create mode 100644 lightning-backend/src/database.ts create mode 100644 lightning-backend/src/index.ts create mode 100644 lightning-backend/src/logger.ts diff --git a/lightning-backend/.gitignore b/lightning-backend/.gitignore new file mode 100644 index 000000000..4aa51e0c1 --- /dev/null +++ b/lightning-backend/.gitignore @@ -0,0 +1,44 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# production config and external assets +*.json +!mempool-config.sample.json + +# compiled output +/dist +/tmp + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log +testem.log +/typings + +# e2e +/e2e/*.js +/e2e/*.map + +#System Files +.DS_Store +Thumbs.db diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json new file mode 100644 index 000000000..8df468cc6 --- /dev/null +++ b/lightning-backend/mempool-config.sample.json @@ -0,0 +1,27 @@ +{ + "MEMPOOL": { + "NETWORK": "mainnet", + "BACKEND": "lnd", + "HTTP_PORT": 8999, + "STDOUT_LOG_MIN_PRIORITY": "debug" + }, + "SYSLOG": { + "ENABLED": false, + "HOST": "127.0.0.1", + "PORT": 514, + "MIN_PRIORITY": "info", + "FACILITY": "local7" + }, + "LN_NODE_AUTH": { + "TSL_CERT_PATH": "", + "MACAROON_PATH": "" + }, + "DATABASE": { + "HOST": "127.0.0.1", + "PORT": 3306, + "SOCKET": "/var/run/mysql/mysql.sock", + "DATABASE": "mempool", + "USERNAME": "mempool", + "PASSWORD": "mempool" + } +} diff --git a/lightning-backend/src/api/lightning-api-abstract-factory.ts b/lightning-backend/src/api/lightning-api-abstract-factory.ts new file mode 100644 index 000000000..086498dd4 --- /dev/null +++ b/lightning-backend/src/api/lightning-api-abstract-factory.ts @@ -0,0 +1,6 @@ +import { ILightningApi } from './lightning-api.interface'; + +export interface AbstractLightningApi { + getNetworkInfo(): Promise; + getNetworkGraph(): Promise; +} diff --git a/lightning-backend/src/api/lightning-api-factory.ts b/lightning-backend/src/api/lightning-api-factory.ts new file mode 100644 index 000000000..4dfb3ae03 --- /dev/null +++ b/lightning-backend/src/api/lightning-api-factory.ts @@ -0,0 +1,13 @@ +import config from '../config'; +import { AbstractLightningApi } from './lightning-api-abstract-factory'; +import LndApi from './lnd/lnd-api'; + +function lightningApiFactory(): AbstractLightningApi { + switch (config.MEMPOOL.BACKEND) { + case 'lnd': + default: + return new LndApi(); + } +} + +export default lightningApiFactory(); diff --git a/lightning-backend/src/api/lightning-api.interface.ts b/lightning-backend/src/api/lightning-api.interface.ts new file mode 100644 index 000000000..4540185ff --- /dev/null +++ b/lightning-backend/src/api/lightning-api.interface.ts @@ -0,0 +1,46 @@ +export namespace ILightningApi { + export interface NetworkInfo { + average_channel_size: number; + channel_count: number; + max_channel_size: number; + median_channel_size: number; + min_channel_size: number; + node_count: number; + not_recently_updated_policy_count: number; + total_capacity: number; + } + + export interface NetworkGraph { + channels: Channel[]; + nodes: Node[]; + } + + export interface Channel { + id: string; + capacity: number; + policies: Policy[]; + transaction_id: string; + transaction_vout: number; + updated_at: string; + } + + interface Policy { + public_key: string; + } + + export interface Node { + alias: string; + color: string; + features: Feature[]; + public_key: string; + sockets: string[]; + updated_at: string; + } + + interface Feature { + bit: number; + is_known: boolean; + is_required: boolean; + type: string; + } +} diff --git a/lightning-backend/src/api/lnd/lnd-api.ts b/lightning-backend/src/api/lnd/lnd-api.ts new file mode 100644 index 000000000..ebf477ca4 --- /dev/null +++ b/lightning-backend/src/api/lnd/lnd-api.ts @@ -0,0 +1,37 @@ +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 config from '../../config'; +import logger from '../../logger'; + +class LndApi implements AbstractLightningApi { + private lnd: any; + constructor() { + try { + const tsl = fs.readFileSync(config.LN_NODE_AUTH.TSL_CERT_PATH).toString('base64'); + const macaroon = fs.readFileSync(config.LN_NODE_AUTH.MACAROON_PATH).toString('base64'); + + const { lnd } = lnService.authenticatedLndGrpc({ + cert: tsl, + macaroon: macaroon, + socket: 'localhost:10009', + }); + + this.lnd = lnd; + } catch (e) { + logger.err('Could not initiate the LND service handler: ' + (e instanceof Error ? e.message : e)); + process.exit(1); + } + } + + async getNetworkInfo(): Promise { + return await lnService.getNetworkInfo({ lnd: this.lnd }); + } + + async getNetworkGraph(): Promise { + return await lnService.getNetworkGraph({ lnd: this.lnd }); + } +} + +export default LndApi; diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts new file mode 100644 index 000000000..4e9e36246 --- /dev/null +++ b/lightning-backend/src/config.ts @@ -0,0 +1,84 @@ +const configFile = require('../mempool-config.json'); + +interface IConfig { + MEMPOOL: { + NETWORK: 'mainnet' | 'testnet' | 'signet'; + BACKEND: 'lnd' | 'cln' | 'ldk'; + HTTP_PORT: number; + STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; + }; + SYSLOG: { + ENABLED: boolean; + HOST: string; + PORT: number; + MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; + FACILITY: string; + }; + LN_NODE_AUTH: { + TSL_CERT_PATH: string; + MACAROON_PATH: string; + }; + DATABASE: { + HOST: string, + SOCKET: string, + PORT: number; + DATABASE: string; + USERNAME: string; + PASSWORD: string; + }; +} + +const defaults: IConfig = { + 'MEMPOOL': { + 'NETWORK': 'mainnet', + 'BACKEND': 'lnd', + 'HTTP_PORT': 8999, + 'STDOUT_LOG_MIN_PRIORITY': 'debug', + }, + 'SYSLOG': { + 'ENABLED': true, + 'HOST': '127.0.0.1', + 'PORT': 514, + 'MIN_PRIORITY': 'info', + 'FACILITY': 'local7' + }, + 'LN_NODE_AUTH': { + 'TSL_CERT_PATH': '', + 'MACAROON_PATH': '', + }, + 'DATABASE': { + 'HOST': '127.0.0.1', + 'SOCKET': '', + 'PORT': 3306, + 'DATABASE': 'mempool', + 'USERNAME': 'mempool', + 'PASSWORD': 'mempool' + }, +}; + +class Config implements IConfig { + MEMPOOL: IConfig['MEMPOOL']; + SYSLOG: IConfig['SYSLOG']; + LN_NODE_AUTH: IConfig['LN_NODE_AUTH']; + DATABASE: IConfig['DATABASE']; + + constructor() { + const configs = this.merge(configFile, defaults); + this.MEMPOOL = configs.MEMPOOL; + this.SYSLOG = configs.SYSLOG; + this.LN_NODE_AUTH = configs.LN_NODE_AUTH; + this.DATABASE = configs.DATABASE; + } + + merge = (...objects: object[]): IConfig => { + // @ts-ignore + return objects.reduce((prev, next) => { + Object.keys(prev).forEach(key => { + next[key] = { ...next[key], ...prev[key] }; + }); + return next; + }); + } +} + +export default new Config(); diff --git a/lightning-backend/src/database.ts b/lightning-backend/src/database.ts new file mode 100644 index 000000000..3816154cd --- /dev/null +++ b/lightning-backend/src/database.ts @@ -0,0 +1,51 @@ +import config from './config'; +import { createPool, Pool, PoolConnection } from 'mysql2/promise'; +import logger from './logger'; +import { PoolOptions } from 'mysql2/typings/mysql'; + + class DB { + constructor() { + if (config.DATABASE.SOCKET !== '') { + this.poolConfig.socketPath = config.DATABASE.SOCKET; + } else { + this.poolConfig.host = config.DATABASE.HOST; + } + } + private pool: Pool | null = null; + private poolConfig: PoolOptions = { + port: config.DATABASE.PORT, + database: config.DATABASE.DATABASE, + user: config.DATABASE.USERNAME, + password: config.DATABASE.PASSWORD, + connectionLimit: 10, + supportBigNumbers: true, + timezone: '+00:00', + }; + + public async query(query, params?) { + const pool = await this.getPool(); + return pool.query(query, params); + } + + public async checkDbConnection() { + try { + await this.query('SELECT ?', [1]); + logger.info('Database connection established.'); + } catch (e) { + logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e)); + process.exit(1); + } + } + + private async getPool(): Promise { + if (this.pool === null) { + this.pool = createPool(this.poolConfig); + this.pool.on('connection', function (newConnection: PoolConnection) { + newConnection.query(`SET time_zone='+00:00'`); + }); + } + return this.pool; + } +} + +export default new DB(); diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts new file mode 100644 index 000000000..23bd56cb7 --- /dev/null +++ b/lightning-backend/src/index.ts @@ -0,0 +1,25 @@ +import config from './config'; +import logger from './logger'; +import DB from './database'; +import lightningApi from './api/lightning-api-factory'; + +logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); + +class LightningServer { + constructor() { + this.init(); + } + + async init() { + await DB.checkDbConnection(); + + const networkInfo = await lightningApi.getNetworkInfo(); + logger.info(JSON.stringify(networkInfo)); + + const networkGraph = await lightningApi.getNetworkGraph(); + logger.info('Network graph channels: ' + networkGraph.channels.length); + logger.info('Network graph nodes: ' + networkGraph.nodes.length); + } +} + +const lightningServer = new LightningServer(); diff --git a/lightning-backend/src/logger.ts b/lightning-backend/src/logger.ts new file mode 100644 index 000000000..1e2c95ed1 --- /dev/null +++ b/lightning-backend/src/logger.ts @@ -0,0 +1,145 @@ +import config from './config'; +import * as dgram from 'dgram'; + +class Logger { + static priorities = { + emerg: 0, + alert: 1, + crit: 2, + err: 3, + warn: 4, + notice: 5, + info: 6, + debug: 7 + }; + static facilities = { + kern: 0, + user: 1, + mail: 2, + daemon: 3, + auth: 4, + syslog: 5, + lpr: 6, + news: 7, + uucp: 8, + local0: 16, + local1: 17, + local2: 18, + local3: 19, + local4: 20, + local5: 21, + local6: 22, + local7: 23 + }; + + // @ts-ignore + public emerg: ((msg: string) => void); + // @ts-ignore + public alert: ((msg: string) => void); + // @ts-ignore + public crit: ((msg: string) => void); + // @ts-ignore + public err: ((msg: string) => void); + // @ts-ignore + public warn: ((msg: string) => void); + // @ts-ignore + public notice: ((msg: string) => void); + // @ts-ignore + public info: ((msg: string) => void); + // @ts-ignore + public debug: ((msg: string) => void); + + private name = 'mempool'; + private client: dgram.Socket; + private network: string; + + constructor() { + let prio; + for (prio in Logger.priorities) { + if (true) { + this.addprio(prio); + } + } + this.client = dgram.createSocket('udp4'); + this.network = this.getNetwork(); + } + + private addprio(prio): void { + this[prio] = (function(_this) { + return function(msg) { + return _this.msg(prio, msg); + }; + })(this); + } + + private getNetwork(): string { + if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') { + return config.MEMPOOL.NETWORK; + } + return ''; + } + + private msg(priority, msg) { + let consolemsg, prionum, syslogmsg; + if (typeof msg === 'string' && msg.length > 0) { + while (msg[msg.length - 1].charCodeAt(0) === 10) { + msg = msg.slice(0, msg.length - 1); + } + } + const network = this.network ? ' <' + this.network + '>' : ''; + prionum = Logger.priorities[priority] || Logger.priorities.info; + consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`; + + if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) { + syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`; + this.syslog(syslogmsg); + } + if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) { + return; + } + if (priority === 'warning') { + priority = 'warn'; + } + if (priority === 'debug') { + priority = 'info'; + } + if (priority === 'err') { + priority = 'error'; + } + return (console[priority] || console.error)(consolemsg); + } + + private syslog(msg) { + let msgbuf; + msgbuf = Buffer.from(msg); + this.client.send(msgbuf, 0, msgbuf.length, config.SYSLOG.PORT, config.SYSLOG.HOST, function(err, bytes) { + if (err) { + console.log(err); + } + }); + } + + private leadZero(n: number): number | string { + if (n < 10) { + return '0' + n; + } + return n; + } + + private ts() { + let day, dt, hours, minutes, month, months, seconds; + dt = new Date(); + hours = this.leadZero(dt.getHours()); + minutes = this.leadZero(dt.getMinutes()); + seconds = this.leadZero(dt.getSeconds()); + month = dt.getMonth(); + day = dt.getDate(); + if (day < 10) { + day = ' ' + day; + } + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return months[month] + ' ' + day + ' ' + hours + ':' + minutes + ':' + seconds; + } +} + +export default new Logger();